detached f8f6441

Merge pull request 'feat(api): add branch prefixes to tokens' (#6) from feat/token-branch-prefixes into main

Reviewed-on: https://git.bnema.dev/bnema/ermite-api/pulls/6
commit
f8f64413e63c36bc1e22f202347efde2c8fc3b05
author
bnema · · 2026-10-12 15:52 UTC
merge of
fad97fd + 6e2e13e · diff against the first parent
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
2 files · +52 -10
M gen/ermite/token.pb.go +38 -9
@@ -133,9 +133,15 @@ type Token struct {
133133 CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
134134 // LastUsedAt is when the token last authenticated a request. Unset if never
135135 // used. It may lag behind the real time by a short delay.
136- LastUsedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=last_used_at,json=lastUsedAt,proto3" json:"last_used_at,omitempty"`
137- unknownFields protoimpl.UnknownFields
138- sizeCache protoimpl.SizeCache
136+ LastUsedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=last_used_at,json=lastUsedAt,proto3" json:"last_used_at,omitempty"`
137+ // BranchPrefixes limits the branches the token may write. Each prefix ends
138+ // with "/" (for example "agent/claude/"). Empty means every branch. A limited
139+ // token can push, force-push and delete only branches under its prefixes; it
140+ // cannot push tags or other refs, create repositories, or merge into or
141+ // delete a branch outside its prefixes.
142+ BranchPrefixes []string `protobuf:"bytes,8,rep,name=branch_prefixes,json=branchPrefixes,proto3" json:"branch_prefixes,omitempty"`
143+ unknownFields protoimpl.UnknownFields
144+ sizeCache protoimpl.SizeCache
139145 }
140146
141147 func (x *Token) Reset() {
@@ -217,6 +223,13 @@ func (x *Token) GetLastUsedAt() *timestamppb.Timestamp {
217223 return nil
218224 }
219225
226+func (x *Token) GetBranchPrefixes() []string {
227+ if x != nil {
228+ return x.BranchPrefixes
229+ }
230+ return nil
231+}
232+
220233 // CreateTokenRequest describes the token to create.
221234 type CreateTokenRequest struct {
222235 state protoimpl.MessageState `protogen:"open.v1"`
@@ -230,9 +243,16 @@ type CreateTokenRequest struct {
230243 Repos []string `protobuf:"bytes,3,rep,name=repos,proto3" json:"repos,omitempty"`
231244 // ExpiresAt is when the token stops working. Unset means no expiry. It must
232245 // be in the future.
233- ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
234- unknownFields protoimpl.UnknownFields
235- sizeCache protoimpl.SizeCache
246+ ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
247+ // BranchPrefixes limits the branches the token may write, for example
248+ // "agent/claude/". Each prefix must end with "/" and be a valid branch name
249+ // once the "/" is removed; at most 8 are allowed. Empty means every branch;
250+ // a limited token cannot push tags or create repositories. A token that has
251+ // prefixes can only create tokens whose prefixes each lie within its own.
252+ // Fails with invalid_argument when a prefix is malformed.
253+ BranchPrefixes []string `protobuf:"bytes,5,rep,name=branch_prefixes,json=branchPrefixes,proto3" json:"branch_prefixes,omitempty"`
254+ unknownFields protoimpl.UnknownFields
255+ sizeCache protoimpl.SizeCache
236256 }
237257
238258 func (x *CreateTokenRequest) Reset() {
@@ -293,6 +313,13 @@ func (x *CreateTokenRequest) GetExpiresAt() *timestamppb.Timestamp {
293313 return nil
294314 }
295315
316+func (x *CreateTokenRequest) GetBranchPrefixes() []string {
317+ if x != nil {
318+ return x.BranchPrefixes
319+ }
320+ return nil
321+}
322+
296323 // CreateTokenResponse holds the new token and its secret.
297324 type CreateTokenResponse struct {
298325 state protoimpl.MessageState `protogen:"open.v1"`
@@ -549,7 +576,7 @@ var File_ermite_token_proto protoreflect.FileDescriptor
549576
550577 const file_ermite_token_proto_rawDesc = "" +
551578 "\n" +
552- "\x12ermite/token.proto\x12\x06ermite\x1a\x1fgoogle/protobuf/timestamp.proto\"\x9e\x02\n" +
579+ "\x12ermite/token.proto\x12\x06ermite\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc7\x02\n" +
553580 "\x05Token\x12\x0e\n" +
554581 "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" +
555582 "\x05label\x18\x02 \x01(\tR\x05label\x12%\n" +
@@ -560,13 +587,15 @@ const file_ermite_token_proto_rawDesc = "" +
560587 "\n" +
561588 "created_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12<\n" +
562589 "\flast_used_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\n" +
563- "lastUsedAt\"\xa2\x01\n" +
590+ "lastUsedAt\x12'\n" +
591+ "\x0fbranch_prefixes\x18\b \x03(\tR\x0ebranchPrefixes\"\xcb\x01\n" +
564592 "\x12CreateTokenRequest\x12\x14\n" +
565593 "\x05label\x18\x01 \x01(\tR\x05label\x12%\n" +
566594 "\x06scopes\x18\x02 \x03(\x0e2\r.ermite.ScopeR\x06scopes\x12\x14\n" +
567595 "\x05repos\x18\x03 \x03(\tR\x05repos\x129\n" +
568596 "\n" +
569- "expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"R\n" +
597+ "expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12'\n" +
598+ "\x0fbranch_prefixes\x18\x05 \x03(\tR\x0ebranchPrefixes\"R\n" +
570599 "\x13CreateTokenResponse\x12#\n" +
571600 "\x05token\x18\x01 \x01(\v2\r.ermite.TokenR\x05token\x12\x16\n" +
572601 "\x06secret\x18\x02 \x01(\tR\x06secret\"O\n" +
@@ -133,9 +133,15 @@ type Token struct {
133 CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`133 CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
134 // LastUsedAt is when the token last authenticated a request. Unset if never134 // LastUsedAt is when the token last authenticated a request. Unset if never
135 // used. It may lag behind the real time by a short delay.135 // used. It may lag behind the real time by a short delay.
136- LastUsedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=last_used_at,json=lastUsedAt,proto3" json:"last_used_at,omitempty"`136+ LastUsedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=last_used_at,json=lastUsedAt,proto3" json:"last_used_at,omitempty"`
137- unknownFields protoimpl.UnknownFields137+ // BranchPrefixes limits the branches the token may write. Each prefix ends
138- sizeCache protoimpl.SizeCache138+ // with "/" (for example "agent/claude/"). Empty means every branch. A limited
139+ // token can push, force-push and delete only branches under its prefixes; it
140+ // cannot push tags or other refs, create repositories, or merge into or
141+ // delete a branch outside its prefixes.
142+ BranchPrefixes []string `protobuf:"bytes,8,rep,name=branch_prefixes,json=branchPrefixes,proto3" json:"branch_prefixes,omitempty"`
143+ unknownFields protoimpl.UnknownFields
144+ sizeCache protoimpl.SizeCache
139 }145 }
140 146
141 func (x *Token) Reset() {147 func (x *Token) Reset() {
@@ -217,6 +223,13 @@ func (x *Token) GetLastUsedAt() *timestamppb.Timestamp {
217 return nil223 return nil
218 }224 }
219 225
226+func (x *Token) GetBranchPrefixes() []string {
227+ if x != nil {
228+ return x.BranchPrefixes
229+ }
230+ return nil
231+}
232+
220 // CreateTokenRequest describes the token to create.233 // CreateTokenRequest describes the token to create.
221 type CreateTokenRequest struct {234 type CreateTokenRequest struct {
222 state protoimpl.MessageState `protogen:"open.v1"`235 state protoimpl.MessageState `protogen:"open.v1"`
@@ -230,9 +243,16 @@ type CreateTokenRequest struct {
230 Repos []string `protobuf:"bytes,3,rep,name=repos,proto3" json:"repos,omitempty"`243 Repos []string `protobuf:"bytes,3,rep,name=repos,proto3" json:"repos,omitempty"`
231 // ExpiresAt is when the token stops working. Unset means no expiry. It must244 // ExpiresAt is when the token stops working. Unset means no expiry. It must
232 // be in the future.245 // be in the future.
233- ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`246+ ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
234- unknownFields protoimpl.UnknownFields247+ // BranchPrefixes limits the branches the token may write, for example
235- sizeCache protoimpl.SizeCache248+ // "agent/claude/". Each prefix must end with "/" and be a valid branch name
249+ // once the "/" is removed; at most 8 are allowed. Empty means every branch;
250+ // a limited token cannot push tags or create repositories. A token that has
251+ // prefixes can only create tokens whose prefixes each lie within its own.
252+ // Fails with invalid_argument when a prefix is malformed.
253+ BranchPrefixes []string `protobuf:"bytes,5,rep,name=branch_prefixes,json=branchPrefixes,proto3" json:"branch_prefixes,omitempty"`
254+ unknownFields protoimpl.UnknownFields
255+ sizeCache protoimpl.SizeCache
236 }256 }
237 257
238 func (x *CreateTokenRequest) Reset() {258 func (x *CreateTokenRequest) Reset() {
@@ -293,6 +313,13 @@ func (x *CreateTokenRequest) GetExpiresAt() *timestamppb.Timestamp {
293 return nil313 return nil
294 }314 }
295 315
316+func (x *CreateTokenRequest) GetBranchPrefixes() []string {
317+ if x != nil {
318+ return x.BranchPrefixes
319+ }
320+ return nil
321+}
322+
296 // CreateTokenResponse holds the new token and its secret.323 // CreateTokenResponse holds the new token and its secret.
297 type CreateTokenResponse struct {324 type CreateTokenResponse struct {
298 state protoimpl.MessageState `protogen:"open.v1"`325 state protoimpl.MessageState `protogen:"open.v1"`
@@ -549,7 +576,7 @@ var File_ermite_token_proto protoreflect.FileDescriptor
549 576
550 const file_ermite_token_proto_rawDesc = "" +577 const file_ermite_token_proto_rawDesc = "" +
551 "\n" +578 "\n" +
552- "\x12ermite/token.proto\x12\x06ermite\x1a\x1fgoogle/protobuf/timestamp.proto\"\x9e\x02\n" +579+ "\x12ermite/token.proto\x12\x06ermite\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc7\x02\n" +
553 "\x05Token\x12\x0e\n" +580 "\x05Token\x12\x0e\n" +
554 "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" +581 "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" +
555 "\x05label\x18\x02 \x01(\tR\x05label\x12%\n" +582 "\x05label\x18\x02 \x01(\tR\x05label\x12%\n" +
@@ -560,13 +587,15 @@ const file_ermite_token_proto_rawDesc = "" +
560 "\n" +587 "\n" +
561 "created_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12<\n" +588 "created_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12<\n" +
562 "\flast_used_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\n" +589 "\flast_used_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\n" +
563- "lastUsedAt\"\xa2\x01\n" +590+ "lastUsedAt\x12'\n" +
591+ "\x0fbranch_prefixes\x18\b \x03(\tR\x0ebranchPrefixes\"\xcb\x01\n" +
564 "\x12CreateTokenRequest\x12\x14\n" +592 "\x12CreateTokenRequest\x12\x14\n" +
565 "\x05label\x18\x01 \x01(\tR\x05label\x12%\n" +593 "\x05label\x18\x01 \x01(\tR\x05label\x12%\n" +
566 "\x06scopes\x18\x02 \x03(\x0e2\r.ermite.ScopeR\x06scopes\x12\x14\n" +594 "\x06scopes\x18\x02 \x03(\x0e2\r.ermite.ScopeR\x06scopes\x12\x14\n" +
567 "\x05repos\x18\x03 \x03(\tR\x05repos\x129\n" +595 "\x05repos\x18\x03 \x03(\tR\x05repos\x129\n" +
568 "\n" +596 "\n" +
569- "expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"R\n" +597+ "expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12'\n" +
598+ "\x0fbranch_prefixes\x18\x05 \x03(\tR\x0ebranchPrefixes\"R\n" +
570 "\x13CreateTokenResponse\x12#\n" +599 "\x13CreateTokenResponse\x12#\n" +
571 "\x05token\x18\x01 \x01(\v2\r.ermite.TokenR\x05token\x12\x16\n" +600 "\x05token\x18\x01 \x01(\v2\r.ermite.TokenR\x05token\x12\x16\n" +
572 "\x06secret\x18\x02 \x01(\tR\x06secret\"O\n" +601 "\x06secret\x18\x02 \x01(\tR\x06secret\"O\n" +
M proto/ermite/token.proto +14 -1
@@ -58,13 +58,19 @@ message Token {
5858 // LastUsedAt is when the token last authenticated a request. Unset if never
5959 // used. It may lag behind the real time by a short delay.
6060 google.protobuf.Timestamp last_used_at = 7;
61+ // BranchPrefixes limits the branches the token may write. Each prefix ends
62+ // with "/" (for example "agent/claude/"). Empty means every branch. A limited
63+ // token can push, force-push and delete only branches under its prefixes; it
64+ // cannot push tags or other refs, create repositories, or merge into or
65+ // delete a branch outside its prefixes.
66+ repeated string branch_prefixes = 8;
6167 }
6268
6369 // TokenService manages stored tokens. Every RPC needs the token:admin scope.
6470 //
6571 // A token can only create tokens that are within its own limits: scopes that
6672 // it holds, repositories that it may access and an expiry no later than its
67-// own. The root token (ERMITE_ADMIN_TOKEN) is not a stored token and is never
73+// own (including its branch prefixes). The root token (ERMITE_ADMIN_TOKEN) is not a stored token and is never
6874 // listed.
6975 service TokenService {
7076 // CreateToken creates a token and returns its secret. The secret is shown
@@ -92,6 +98,13 @@ message CreateTokenRequest {
9298 // ExpiresAt is when the token stops working. Unset means no expiry. It must
9399 // be in the future.
94100 google.protobuf.Timestamp expires_at = 4;
101+ // BranchPrefixes limits the branches the token may write, for example
102+ // "agent/claude/". Each prefix must end with "/" and be a valid branch name
103+ // once the "/" is removed; at most 8 are allowed. Empty means every branch;
104+ // a limited token cannot push tags or create repositories. A token that has
105+ // prefixes can only create tokens whose prefixes each lie within its own.
106+ // Fails with invalid_argument when a prefix is malformed.
107+ repeated string branch_prefixes = 5;
95108 }
96109
97110 // CreateTokenResponse holds the new token and its secret.
@@ -58,13 +58,19 @@ message Token {
58 // LastUsedAt is when the token last authenticated a request. Unset if never58 // LastUsedAt is when the token last authenticated a request. Unset if never
59 // used. It may lag behind the real time by a short delay.59 // used. It may lag behind the real time by a short delay.
60 google.protobuf.Timestamp last_used_at = 7;60 google.protobuf.Timestamp last_used_at = 7;
61+ // BranchPrefixes limits the branches the token may write. Each prefix ends
62+ // with "/" (for example "agent/claude/"). Empty means every branch. A limited
63+ // token can push, force-push and delete only branches under its prefixes; it
64+ // cannot push tags or other refs, create repositories, or merge into or
65+ // delete a branch outside its prefixes.
66+ repeated string branch_prefixes = 8;
61 }67 }
62 68
63 // TokenService manages stored tokens. Every RPC needs the token:admin scope.69 // TokenService manages stored tokens. Every RPC needs the token:admin scope.
64 //70 //
65 // A token can only create tokens that are within its own limits: scopes that71 // A token can only create tokens that are within its own limits: scopes that
66 // it holds, repositories that it may access and an expiry no later than its72 // it holds, repositories that it may access and an expiry no later than its
67-// own. The root token (ERMITE_ADMIN_TOKEN) is not a stored token and is never73+// own (including its branch prefixes). The root token (ERMITE_ADMIN_TOKEN) is not a stored token and is never
68 // listed.74 // listed.
69 service TokenService {75 service TokenService {
70 // CreateToken creates a token and returns its secret. The secret is shown76 // CreateToken creates a token and returns its secret. The secret is shown
@@ -92,6 +98,13 @@ message CreateTokenRequest {
92 // ExpiresAt is when the token stops working. Unset means no expiry. It must98 // ExpiresAt is when the token stops working. Unset means no expiry. It must
93 // be in the future.99 // be in the future.
94 google.protobuf.Timestamp expires_at = 4;100 google.protobuf.Timestamp expires_at = 4;
101+ // BranchPrefixes limits the branches the token may write, for example
102+ // "agent/claude/". Each prefix must end with "/" and be a valid branch name
103+ // once the "/" is removed; at most 8 are allowed. Empty means every branch;
104+ // a limited token cannot push tags or create repositories. A token that has
105+ // prefixes can only create tokens whose prefixes each lie within its own.
106+ // Fails with invalid_argument when a prefix is malformed.
107+ repeated string branch_prefixes = 5;
95 }108 }
96 109
97 // CreateTokenResponse holds the new token and its secret.110 // CreateTokenResponse holds the new token and its secret.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 18

↑↓ moveenter openesc close