@colony2/gitvend 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,303 @@
1
+ # Gitvend secret-provider plugin for JobDB
2
+
3
+ The JobDB plugin gives workers short-lived Gitvend credentials for their jobs.
4
+ You configure permissions per tenant and additional permissions for the target
5
+ cell repository. JobDB starts the plugin when needed and passes verified job and
6
+ lease information. The plugin signs tokens locally; the gateway needs only the
7
+ matching public key. Workers never receive the GitHub token or private signing key.
8
+
9
+ Set up a [Gitvend gateway](gateway.md) first, or configure it alongside the plugin.
10
+ The plugin runs on the JobDB host; it does not replace the gateway.
11
+ It returns only requested, authorized secret names:
12
+
13
+ | Secret name | JWT permissions |
14
+ | --- | --- |
15
+ | `gitvend_base_token` | The configured tenant `base_permissions`, unchanged. |
16
+ | `gitvend_cell_token` | Base permissions plus matching `target_permissions`, narrowed to the exact target cell repository. |
17
+
18
+ These are signed JWT **tokens**, not signing keys. Both lists accept the standard
19
+ [Gitvend permission syntax](permission-syntax.md), including `r`, `w`, `d`, `c`,
20
+ ref selectors, and denies. A base-token request does not need cell metadata.
21
+ Matching denies still win across both lists and the gateway's deny rules.
22
+
23
+ ## Install
24
+
25
+ On the host where JobDB runs, install the package:
26
+
27
+ ```sh
28
+ npm install -g @colony2/gitvend
29
+ jobdb-gitvend-secret-provider --help
30
+ ```
31
+
32
+ The package supplies prebuilt `gitvend` and `jobdb-gitvend-secret-provider`
33
+ executables for Linux/macOS on amd64/arm64. npm requires Node.js 22+, `tar`, and
34
+ enabled install scripts. For installation without Node.js, download a
35
+ [release archive](https://github.com/colony-2/gitvend/releases) containing both
36
+ executables, verify its checksum, and put them on `PATH`. Older releases may
37
+ contain only the gateway; use a release that includes the plugin.
38
+
39
+ The npm installer places a native executable at the plugin command path, so
40
+ JobDB does not need Node.js or `PATH` in the plugin environment.
41
+
42
+ Use a JobDB version with secret-provider plugin support. JobDB must be able to
43
+ execute the installed command and read the issuer configuration and signing key.
44
+
45
+ ## Configure the issuer
46
+
47
+ Configuration uses YAML. Unknown fields, duplicate keys and multiple documents
48
+ are rejected. JSON-shaped mappings also load as YAML.
49
+
50
+ Generate a signing key on the trusted JobDB host:
51
+
52
+ ```sh
53
+ mkdir -p gitvend-issuer
54
+ gitvend keygen --private gitvend-issuer/signing.key --public gitvend-issuer/signing.pub
55
+ ```
56
+
57
+ Save this as `gitvend-issuer/issuer.yaml`, replacing `YOUR_TENANT_ID` with the
58
+ actual JobDB tenant ID:
59
+
60
+ ```yaml
61
+ issuer: jobdb
62
+ audience: gitvend
63
+ key_id: jobdb
64
+ signing_key_file: signing.key
65
+ cell_metadata_field: repo
66
+ tenants:
67
+ YOUR_TENANT_ID:
68
+ base_permissions:
69
+ - github.com/*:r
70
+ target_permissions:
71
+ - github.com/colony-2/*:rwc
72
+ ```
73
+
74
+ This permits all GitHub reads available through the gateway, plus read/write/create
75
+ on the target cell under `colony-2`. Use narrower base rules when tenants must not
76
+ share private repository access. The [complete issuer example](../examples/jobdb-gitvend-issuer.yaml)
77
+ also shows token lifetime and size settings.
78
+
79
+ `signing_key_file` resolves relative to the issuer file. Keep the private key on
80
+ the JobDB host and copy only `signing.pub` to the gateway. Add a gateway `keys`
81
+ entry with ID `jobdb`, issuer `jobdb`, and the public key's path. Its owner
82
+ allowlist and the upstream provider's allowed owners must cover the repositories
83
+ in your policy. The audience must also match. Configure upstream GitHub credentials
84
+ on the gateway separately.
85
+
86
+ Validate before registering the plugin:
87
+
88
+ ```sh
89
+ jobdb-gitvend-secret-provider validate --config gitvend-issuer/issuer.yaml
90
+ ```
91
+
92
+ Expected output: `Configuration and signing key are valid.` This checks the local
93
+ configuration and private key; it does not test GitHub access or gateway connectivity.
94
+
95
+ ## Tenant permissions
96
+
97
+ Example policy for more restricted reads:
98
+
99
+ ```yaml
100
+ base_permissions:
101
+ - github.com/public-org/*:r
102
+ - github.com/example-org/shared-library:r
103
+ target_permissions:
104
+ - github.com/example-org/cell-*:rwc
105
+ principals:
106
+ - trusted-job-submitter
107
+ ```
108
+
109
+ `base_permissions` applies to both tokens, including the target repository.
110
+ `target_permissions` grants additional authority only for the repository selected
111
+ by metadata. For example, this tenant policy allows all accessible GitHub reads
112
+ and read/write/create on the target cell if it belongs to `colony-2`:
113
+
114
+ ```yaml
115
+ base_permissions:
116
+ - github.com/*:r
117
+ target_permissions:
118
+ - github.com/colony-2/*:rwc
119
+ ```
120
+
121
+ For `AppMetadata.repo = "https://github.com/colony-2/my-cell.git"`, the cell JWT
122
+ contains `github.com/*:r` and `github.com/colony-2/my-cell:rwc`. It does not grant
123
+ writes or creation to any other cell. A target rule such as
124
+ `github.com/colony-2/*#work/*:rw` retains its branch selector after narrowing.
125
+ Use a separate repository-wide `:c` rule for creation when writes are branch-scoped;
126
+ Gitvend rejects `c` with a ref selector.
127
+
128
+ An empty/omitted base list disables the base token. An empty/omitted target list,
129
+ or no matching target allow rule, disables the cell token. A tenant must have
130
+ at least one rule, with at most 128 rules across both lists. Optional `principals`
131
+ restricts both tokens to those exact broker-verified principals; omitting it
132
+ authorizes all principals, including absent principals, within that tenant.
133
+
134
+ Creation still requires the gateway provider's `allow_creation: true` and a
135
+ readable/writable branch. Its `creation_defaults` chooses visibility and GitHub
136
+ features; the issuer cannot override those defaults. See
137
+ [gateway creation settings](gateway.md#repository-creation-defaults).
138
+
139
+ ### Migrating the original issuer config
140
+
141
+ Rename `read_permissions` to `base_permissions`. Replace `cell_write_repositories`
142
+ with `target_permissions`, adding `:rw` to each old repository pattern to preserve
143
+ its permissions, or `:rwc` to also allow creation. Update workers to request
144
+ `gitvend_base_token` instead of `gitvend_read_token`; `gitvend_cell_token` is unchanged.
145
+ Old config fields are rejected, and the old secret name is omitted rather than
146
+ silently returning a token that may now carry write permissions. Deploy the
147
+ new config and executable together; validate the config before restarting JobDB's
148
+ plugin process.
149
+
150
+ ## Register with JobDB
151
+
152
+ Find the installed command with `command -v jobdb-gitvend-secret-provider`.
153
+ Save a `providers.json` file using absolute paths for that executable and your
154
+ issuer configuration:
155
+
156
+ ```json
157
+ {
158
+ "version": 1,
159
+ "providers": [
160
+ {
161
+ "id": "gitvend",
162
+ "type": "plugin",
163
+ "command": "/absolute/path/to/jobdb-gitvend-secret-provider",
164
+ "args": ["--config", "/absolute/path/to/gitvend-issuer/issuer.yaml"]
165
+ }
166
+ ]
167
+ }
168
+ ```
169
+
170
+ ```sh
171
+ jobdb sqlite --db ./jobdb.db --secret-providers-file /absolute/path/to/providers.json
172
+ ```
173
+
174
+ Other providers, including SQLite, can coexist in the same provider file; the first entry for each secret name wins. See
175
+ [the provider guide](https://github.com/colony-2/jobdb/blob/f403447cb58a7195e32a55fb7f417e09da70f388/docs/GUIDE-SECRET-PROVIDERS.md) for collection behavior.
176
+ An omission here cannot veto a grant from a different provider.
177
+
178
+ The worker requests either or both names through JobDB's normal secret-resolution
179
+ API and uses the returned JWT as the Gitvend HTTP Basic password. It still uses
180
+ the gateway's Git URL, for example
181
+ `https://gitvend.example/github/example-org/cell-one.git`, with Git protocol v2.
182
+ Follow the [agent authentication instructions](gateway.md#configure-git-authentication)
183
+ to configure the Git credential helper.
184
+
185
+ ## Cell metadata and authorization
186
+
187
+ The default `cell_metadata_field` is `repo`, matching c2j's target repository
188
+ metadata. It is a top-level field in `SecretResolveRequest.AppMetadata`, e.g.:
189
+
190
+ ```json
191
+ {
192
+ "cell_id": "cell-123",
193
+ "cell_name": "cell-one",
194
+ "repo": "https://github.com/example-org/cell-one.git"
195
+ }
196
+ ```
197
+
198
+ Accepted repository forms are `https://host/owner/repo[.git]` and
199
+ `host/owner/repo[.git]`. Owner/repository casing is normalized. Credentials, query
200
+ strings, fragments, encoded paths, extra path segments and permission syntax are
201
+ rejected. Configure a different field if your application uses another name;
202
+ its value must still identify a repository, not an opaque cell ID. SSH URLs and
203
+ cell-ID-to-repository lookups are not implemented.
204
+
205
+ The provider matches that repository against the **verified tenant's** configured
206
+ target rules, then replaces each matching rule's repository pattern with the
207
+ exact repository while preserving its actions, ref selector and deny flag.
208
+ Missing, malformed or unauthorized metadata omits the cell token while leaving
209
+ an independently authorized base token available. Unknown names and tenants are
210
+ also omitted.
211
+
212
+ Application metadata may be submitter-controlled. The configured target patterns
213
+ are an authorization ceiling: authorized jobs in that tenant may select any
214
+ matching cell. If individual jobs or principals must be confined to different
215
+ cells within a tenant, a trusted job/principal-to-cell authorization lookup is
216
+ needed before widening this model. Metadata by itself does not establish such
217
+ a binding. `cell_id` and `cell_name` do not override `repo`.
218
+
219
+ ## Private repository reads
220
+
221
+ The plugin does not discover GitHub visibility or user membership. `github.com/*:r` authorizes reads across the host, intersected
222
+ with Gitvend's signing-key owner ceiling, gateway routes/denies, and the actual
223
+ upstream GitHub token's permissions. Permission strings use `github.com`, not
224
+ the route alias `github`.
225
+
226
+ This broad setting works when all tenants receiving it are allowed to read
227
+ everything available to those Gitvend upstream credentials. The upstream token
228
+ cannot provide tenant-specific isolation: Gitvend selects it by provider route,
229
+ not by the JobDB tenant or principal. With a shared upstream token, a tenant can
230
+ read other tenants' private repositories if all those checks permit them.
231
+
232
+ For different tenant read scopes, configure explicit tenant read patterns. For
233
+ credential-level separation, use separate gateway deployments/credentials or
234
+ non-overlapping owner routes and key ceilings as appropriate. A pattern does
235
+ not mean “public repositories only”: matching private repositories are readable
236
+ if the upstream token allows them. The plugin has no “public repositories only” permission option.
237
+
238
+ ## Lifetime and operation
239
+
240
+ The signed expiry and returned `SecretValue.ExpiresAt` are identical, and
241
+ `LeaseBound` is true. The provider uses the earlier of `AuthorizedNotAfter` and
242
+ `LeaseExpiresAt`, subtracts gateway leeway and the clock margin, and rounds down
243
+ to a whole second. It also caps the encoded lifetime at `max_lifetime_seconds`.
244
+ Defaults are 900 seconds, 30 seconds of gateway leeway, a 1-second clock margin,
245
+ and a 4096-byte token limit. Match these settings to the gateway; the provider
246
+ cannot discover its configuration. Set the margin for your actual clock skew.
247
+
248
+ Missing verified job/lease identity or deadlines fails issuance. Too little
249
+ remaining lifetime returns `ErrSecretsUnavailable`. The request context bounds
250
+ resolution work, not token lifetime. Backend renewal cannot extend a token minted
251
+ through an older presenting capability; obtain a renewed handle and use JobDB's
252
+ fresh secret resolution to refresh credentials. Already delivered JWTs remain
253
+ valid until their expiry plus gateway leeway even if the job is canceled earlier.
254
+
255
+ Every resolution issues fresh tokens. `policy_revision` is included for gateway
256
+ auditing; the audit subject identifies the tenant/job/lease context without
257
+ exposing the raw identifiers.
258
+
259
+ ## Configuration reloads and key rotation
260
+
261
+ Configuration and signing-key changes reload automatically on Linux and macOS,
262
+ including atomic file replacements. A request uses one complete configuration
263
+ and key snapshot; subsequent requests use the replacement after validation.
264
+ Removed tenants and permissions affect newly issued tokens.
265
+
266
+ A malformed, missing or unreadable replacement leaves the last valid snapshot
267
+ active. The plugin reports a fixed error message and retries every second until
268
+ loading succeeds, including when a new signing-key path does not exist yet.
269
+ Startup still requires valid files and access to watch their directories. For coordinated key
270
+ rotation, write a new key file first, then atomically replace the config with its
271
+ new path and key ID; provision the corresponding gateway public key beforehand.
272
+
273
+ Reloading does not revoke already issued JWTs or replace credentials cached by
274
+ JobDB workflows. Use fresh secret resolution to obtain updated permissions.
275
+ Diagnostics go to stderr without keys, JWTs, raw configuration, or raw errors;
276
+ the JobDB host may suppress plugin output. Watching uses local filesystem events,
277
+ with no periodic polling after successful loads. Network filesystems and changes
278
+ to symlink targets outside the watched directories require a different update
279
+ mechanism or restarting the plugin host.
280
+
281
+ ## Command reference
282
+
283
+ | Command | Purpose |
284
+ | --- | --- |
285
+ | `jobdb-gitvend-secret-provider --help` | Show setup instructions. |
286
+ | `jobdb-gitvend-secret-provider validate --config issuer.yaml` | Validate local configuration and the signing key. |
287
+ | `jobdb-gitvend-secret-provider --config issuer.yaml` | Invocation registered with JobDB; JobDB launches this process. |
288
+ | `jobdb-gitvend-secret-provider version` | Show the installed version. |
289
+ | `jobdb-gitvend-secret-provider completion --help` | Set up shell completion. |
290
+
291
+ ## Troubleshooting
292
+
293
+ | Symptom | What to check |
294
+ | --- | --- |
295
+ | Command is missing after npm install | Use a release that includes the plugin, check `PATH`, and ensure npm install scripts ran. After fixing download/install-script errors, run `npm rebuild @colony2/gitvend`. |
296
+ | Configuration validation fails | Check YAML field names, permission syntax, tenant IDs, and the signing-key path relative to the issuer file. |
297
+ | Neither token is returned | Check the verified tenant/principal, requested secret names, and job lease authority. Unknown names and unauthorized tenants are omitted. |
298
+ | Base token works but cell token is missing | Check `AppMetadata.repo`, `cell_metadata_field`, and whether a target allow rule matches that exact repository. |
299
+ | Gateway rejects a token | Check issuer, key ID, audience, signing key, clock tolerance, lifetime limits, and gateway owner allowlists. |
300
+ | Cell push cannot create a repository | The target rules need `c` plus a readable/writable branch. Check the gateway's `allow_creation`, upstream permissions, and creation settings. |
301
+ | Config changed but a worker still has old permissions | Obtain fresh secrets through JobDB. Already issued JWTs retain their permissions until expiry, and invalid replacements leave the last valid config active. |
302
+
303
+ For source builds, tests, or SDK integration, see the [developer guide](development.md).
@@ -0,0 +1,136 @@
1
+ # JWT permission syntax, version 1
2
+
3
+ Status: implemented version 1. This defines the compact permission strings used by the [Git gateway design](git-proxy-design.md). Permissions are signed inside the JWT and evaluated locally; no permission-name or role lookup is required.
4
+
5
+ ## Shape
6
+
7
+ ```text
8
+ [!]host/namespace/repository[#ref-selector]:actions
9
+ ```
10
+
11
+ Examples:
12
+
13
+ ```text
14
+ github.com/fooorg/*:r
15
+ github.com/fooorg/blue*:rw
16
+ github.com/*:r
17
+ github.com/fooorg/red#(main|master|link):rw
18
+ github.com/fooorg/red#agents/agent-47/*:rw
19
+ github.com/fooorg/red#refs/tags/v*:r
20
+ github.com/fooorg/agent-47-*:c
21
+ !github.com/fooorg/*#(main|master):wd
22
+ ```
23
+
24
+ The JWT contains an array of strings, not a comma-separated string. Each string is one rule. The optional leading `!` makes a deny rule; otherwise it is an allow rule. Rule order has no meaning.
25
+
26
+ ## Actions
27
+
28
+ | Letter | Permission |
29
+ | --- | --- |
30
+ | `r` | Read objects by hash in the matching repository and discover/resolve matching refs. A ref selector restricts discovery, not hash reads. |
31
+ | `w` | Create or update matching refs, including history rewrites, subject to upstream restrictions. Does not delete refs or create repositories. |
32
+ | `d` | Delete matching refs. Does not delete the repository. |
33
+ | `c` | Create matching repositories using the route's server-controlled creation defaults (private by default). Does not itself grant reads or ref writes. |
34
+
35
+ Letters may be combined, such as `rw`, `rwd`, or `rwc`. Reject empty, duplicate or unknown action letters; issuers serialize in `rwdc` order. `w` and `d` do not imply `r`: a write also requires effective read/discovery permission on its destination, possibly from another rule. This permits a broad `:r` plus a narrow `:w` without repeating read grants. No separate force-push letter is claimed: distinguishing history rewrites from fast-forwards remains outside the initial streaming gate.
36
+
37
+ `c` is valid only without a `#ref-selector`, including in combinations and denies. Repository creation has no branch identity. Actual create-on-discovery additionally requires repository read permission and an effective `r`/`w` overlap for at least one possible branch. Creation never grants authority to write other refs.
38
+
39
+ ## Repository matching
40
+
41
+ The host is an exact, lowercase configured Git authority, optionally with a configured port. No host wildcards, scheme, userinfo, query or client-chosen server URL. For example, `github.com` maps to the configured GitHub provider, API endpoint, credential and allowed owners. Multiple credential routes must be resolved deterministically by host/namespace; ambiguous routes fail configuration validation. A host-wide grant cannot enable an unconfigured owner or exceed its issuer's local authority ceiling.
42
+
43
+ Repository paths are canonical `namespace/repository` names, without the transport `.git` suffix. GitHub paths have an owner and a repository; later adapters may allow nested namespaces. Match against the adapter's canonical identity, never against the raw incoming URL. Hostnames are case-insensitive; repository case handling follows the adapter; branch and tag names are case-sensitive. Redirects and aliases are not permission matches.
44
+
45
+ | Pattern | Meaning |
46
+ | --- | --- |
47
+ | `fooorg/red` | That exact repository. |
48
+ | `fooorg/*` | Every repository directly in `fooorg`. |
49
+ | `fooorg/blue*` | Repositories directly in `fooorg` whose names begin with `blue`, including `blue` itself. |
50
+ | `*/red` | Repository `red` in any one-level namespace. |
51
+ | `fooorg/**` | Repositories in `fooorg` and recursively nested namespaces, for adapters that support them. |
52
+ | `*` immediately after the host | Whole-host shorthand: all canonical repository paths on that configured host, at any namespace depth. Equivalent to `**` in this position. |
53
+
54
+ In ordinary path segments, `*` matches zero or more characters but never `/`. A standalone `**` path segment spans namespace levels; `fooorg/**/red` can match `fooorg/red` and `fooorg/team/red`. `**` is not allowed embedded in a segment. Every matched target must still be a valid repository identity; a glob never names a namespace itself as a repository.
55
+
56
+ The whole-host `host/*` form is an explicit shorthand, not a general rule that ordinary path stars cross slashes. It supports the requested `github.com/*:r` spelling without broadening `github.com/fooorg/*` to unrelated namespaces.
57
+
58
+ ## Ref selectors
59
+
60
+ | Selector | Meaning |
61
+ | --- | --- |
62
+ | No `#` | Whole repository: all supported branches and tags for `r`, `w` and `d`; repository-level scope for `c`. |
63
+ | `#main` | Exactly `refs/heads/main`. |
64
+ | `#(main|master|link)` | Those three branch names. |
65
+ | `#agents/agent-47/*` | Branches with that prefix, including nested suffixes. |
66
+ | `#*` | All branches, not tags. |
67
+ | `#refs/heads/release/*` | Explicit fully qualified branch selector. |
68
+ | `#refs/tags/v*` | Tags whose names begin with `v`. |
69
+
70
+ Without an explicit `refs/heads/` or `refs/tags/` prefix, the selector names a branch. Ref wildcards match the **whole branch or tag name**, so `*` may span `/` here. `**` has no additional meaning in ref selectors and is rejected to avoid competing spellings. This intentionally differs from namespace path segments: the slash in a branch name is part of the branch name, not a repository boundary.
71
+
72
+ Only branch and tag namespaces are supported initially. Explicit selectors beginning `refs/` must use a literal supported namespace prefix; `#refs/*` and selectors for provider-managed refs are invalid. To match a branch literally named `refs/tags/example`, use `#refs/heads/refs/tags/example`.
73
+
74
+ An unrestricted repository `:rw` therefore includes tag creation/update. Issuers that intend branch-only writes must use `#*:rw`, and branch-restricted work should use a narrower selector. Repository-wide grants deliberately opt into broader scope; tags are otherwise hidden and unwritable without a matching grant. `HEAD` is handled by the gateway's symbolic-ref visibility rules, not as a separate writable ref.
75
+
76
+ ## Alternatives and literal characters
77
+
78
+ `(a|b|c)` means a choice of glob fragments, not a regular expression. It can appear inside a namespace/repository segment or ref selector. For example, `(blue|green)*` and `#(main|release/*)` are valid. Repository alternatives cannot contain `/`; use a separate rule for different path structures. Ref alternatives may contain `/`.
79
+
80
+ All matches cover the entire canonical field. `red` does not match `infra-red`; `main` does not match `main-old`; `.` is literal. There are no regex operators, character classes, nested groups or implicit substring matches. Reject empty alternatives, unmatched parentheses, unescaped bare `|`, `?`, `[` or `]`, and trailing escapes.
81
+
82
+ Backslash escapes grammar characters for literal matching, including `(`, `)`, `|`, `#`, `:`, `!` and backslash itself. Parse delimiters while respecting escapes; use the action-separating colon after the resource/ref portion, not a colon in a host port. JSON must itself escape backslashes: a selector for a branch literally named `topic(test)` is encoded as `"...#topic\\(test\\):r"`. Escaping does not legalize characters prohibited by the forge or Git. Do not percent-decode permission strings or interpret them as URLs.
83
+
84
+ ## Combining rules
85
+
86
+ For each internal action, allow if at least one matching allow grants it and no matching deny removes it. Default deny. Specificity and ordering do not override a deny. Apply configured issuer/host/owner limits and gateway hard restrictions in addition; upstream permissions remain a final independent restriction.
87
+
88
+ | Internal action | Allow condition | Deny condition |
89
+ | --- | --- | --- |
90
+ | `repo.read` | Any repository-matching `r` rule, with or without a ref selector. | A repository-matching `r` deny **without** a ref selector. |
91
+ | `ref.discover` | A repository/ref-matching `r` allow. | A repository/ref-matching `r` deny. |
92
+ | `ref.create`, `ref.update` | A repository/ref-matching `w` allow. | A repository/ref-matching `w` deny. |
93
+ | `ref.delete` | A repository/ref-matching `d` allow. | A repository/ref-matching `d` deny. |
94
+ | `repo.create` | A repository-matching `c` allow. | A repository-matching `c` deny. |
95
+
96
+ Reading a named ref requires both `repo.read` and `ref.discover`. A ref mutation requires those two plus the corresponding write action. An allowed direct hash fetch only needs `repo.read`. A repository-wide `!…:r` consequently blocks all ordinary Git use of that repository, including writes that depend on read access. A branch-scoped `!…#secret/*:r` hides matching refs and blocks writes to them, but **does not** prevent fetching their objects by known hash. That is the previously selected read model, not a parser exception.
97
+
98
+ Examples of overlap:
99
+
100
+ - `github.com/fooorg/*:r` plus `github.com/fooorg/blue*:w`: discover all refs throughout the organization; create/update refs only in `blue*` repositories.
101
+ - `github.com/fooorg/red#main:r` plus `github.com/fooorg/red#agents/47/*:rw`: discover main and this agent's branches; only the agent branches are writable; hash reads remain repository-wide.
102
+ - `github.com/*:r` plus `github.com/fooorg/red#main:rw`: all configured GitHub repositories remain readable/discoverable. The narrower rule adds main writes; it does not narrow the broad read grant.
103
+ - `github.com/fooorg/*:rwd` plus `!github.com/fooorg/*#(main|master):wd`: keep those branches readable while preventing creation, updates and deletion, even if another allow grants them.
104
+
105
+ Grants are evaluated against the current repository/ref name at request time. They cover future matching repositories and branches, subject to configured owner access. If a repository is deleted and recreated under the same name, the same matching grant still applies. There is no repository enrollment or ID pinning; the grant contains only `v` and `permissions`.
106
+
107
+ ## JWT example
108
+
109
+ The following is the custom claim fragment; the full token also requires the identity, audience, lifetime and signature fields from the main design:
110
+
111
+ ```json
112
+ {
113
+ "grant": {
114
+ "v": 1,
115
+ "permissions": [
116
+ "github.com/fooorg/red#main:r",
117
+ "github.com/fooorg/red#agents/47/*:rw",
118
+ "github.com/fooorg/agent-47-*:rc",
119
+ "github.com/fooorg/agent-47-*#agents/47/*:rw",
120
+ "!github.com/fooorg/*#(main|master):wd"
121
+ ]
122
+ }
123
+ }
124
+ ```
125
+
126
+ This can read main, discover/write the agent's branches, and provision matching scratch repositories. Creation uses the route's creation defaults, including visibility (private by default). The `rc` rule also deliberately grants all-ref discovery in those scratch repositories; remove `r` there and put it only on selected branches if narrower discovery is desired.
127
+
128
+ ## Implementation and acceptance
129
+
130
+ Compile the signed strings into an immutable matcher after JWT verification and schema validation. Use a dedicated lexer/parser and bounded glob/alternative automata; do not feed raw token patterns into a regex engine or shell, and do not expand alternatives into an unbounded Cartesian product. Bound token bytes, rule count, pattern length and group count. Cache compiled grants by verified token digest, never just `sub` or a client-chosen `jti`, with expiry and relevant local configuration revision in the cache validity conditions.
131
+
132
+ Derive internal action checks from the table above. Apply the same canonicalization and matcher to ref discovery, direct receive-pack commands and provisioning. Creation eligibility is a nonempty language intersection: permitted branch `r` and `w` patterns minus denies must overlap. Compute that with bounded automata; do not sample candidate branch names or ignore denies. If computation exceeds configured complexity bounds, reject the grant/creation eligibility with a clear error.
133
+
134
+ Add a policy-explanation command for issuer/operator use that accepts a grant and concrete repository/ref/action and returns matching allows/denies and the decision. It should also flag obviously broad repository or host grants that make narrower grants redundant, without changing their meaning.
135
+
136
+ Acceptance cases must cover whole-host shorthand, segment boundaries, nested namespaces, branch slashes, alternatives, escapes, case rules, tags versus branches, overlapping grants/denies, denied branch hash fetches, unknown syntax, duplicate action letters, future matching names, recreated repository names and creation eligibility. Reject a malformed rule by rejecting the entire JWT grant, never by silently dropping that rule.
@@ -0,0 +1,31 @@
1
+ listen: 127.0.0.1:8443
2
+ tls_cert: /run/secrets/gitvend.crt
3
+ tls_key: /run/secrets/gitvend.key
4
+ audience: gitvend
5
+ keys:
6
+ - id: orchestrator-1
7
+ issuer: orchestrator
8
+ public_key_file: var/signing.pub
9
+ owners:
10
+ github.com:
11
+ - fooorg
12
+ providers:
13
+ - alias: github
14
+ host: github.com
15
+ kind: github
16
+ git_base_url: https://github.com
17
+ api_base_url: https://api.github.com
18
+ allowed_owners:
19
+ - fooorg
20
+ credential:
21
+ kind: token
22
+ secret_ref: env:GITHUB_TOKEN
23
+ allow_creation: true
24
+ creation_defaults:
25
+ visibility: private
26
+ has_issues: false
27
+ has_projects: false
28
+ has_wiki: false
29
+ has_pull_requests: false
30
+ denies:
31
+ - '!github.com/fooorg/*#(main|master):wd'
@@ -0,0 +1,9 @@
1
+ {
2
+ "v": 1,
3
+ "permissions": [
4
+ "github.com/fooorg/project#main:r",
5
+ "github.com/fooorg/project#agents/47/*:rw",
6
+ "github.com/fooorg/agent-47-*:c",
7
+ "github.com/fooorg/agent-47-*#agents/47/*:rw"
8
+ ]
9
+ }
@@ -0,0 +1,16 @@
1
+ issuer: jobdb
2
+ audience: gitvend
3
+ key_id: jobdb
4
+ signing_key_file: signing.key
5
+ policy_revision: "1"
6
+ max_token_bytes: 4096
7
+ max_lifetime_seconds: 900
8
+ gateway_leeway_seconds: 30
9
+ clock_margin_seconds: 1
10
+ cell_metadata_field: repo
11
+ tenants:
12
+ example:
13
+ base_permissions:
14
+ - github.com/*:r
15
+ target_permissions:
16
+ - github.com/colony-2/*:rwc
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colony2/gitvend",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Git HTTPS gateway with signed repository and branch permissions",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://github.com/colony-2/gitvend",
@@ -24,7 +24,8 @@
24
24
  "security"
25
25
  ],
26
26
  "bin": {
27
- "gitvend": "bin/cli.js"
27
+ "gitvend": "bin/cli.js",
28
+ "jobdb-gitvend-secret-provider": "bin/jobdb-gitvend-secret-provider"
28
29
  },
29
30
  "scripts": {
30
31
  "postinstall": "node scripts/postinstall.js",
@@ -46,6 +47,8 @@
46
47
  "scripts/",
47
48
  "README.md",
48
49
  "THIRD_PARTY_NOTICES.md",
49
- "LICENSES/"
50
+ "LICENSES/",
51
+ "docs/",
52
+ "examples/"
50
53
  ]
51
54
  }
@@ -44,7 +44,8 @@ async function install({
44
44
  const { name, assetName, baseUrl } = releaseAsset(pkg, platform, arch);
45
45
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${name}-`));
46
46
  const vendorDir = path.join(packageRoot, "vendor");
47
- let stagedBinary;
47
+ const binaries = [name, "jobdb-gitvend-secret-provider"];
48
+ const staged = [];
48
49
  try {
49
50
  const archivePath = path.join(tempDir, assetName);
50
51
  const checksumsPath = path.join(tempDir, "checksums.txt");
@@ -53,17 +54,26 @@ async function install({
53
54
  await downloadFile(`${baseUrl}/${assetName}`, archivePath);
54
55
  await downloadFile(`${baseUrl}/checksums.txt`, checksumsPath, { maxBytes: 1024 * 1024 });
55
56
  verifyChecksum(archivePath, checksumsPath, assetName);
56
- // Extract only the executable, not documentation or other archive entries.
57
- execFileSync("tar", ["-xzf", archivePath, "-C", extractDir, name], { stdio: "pipe" });
58
- const binary = path.join(extractDir, name);
59
- if (!fs.lstatSync(binary).isFile()) throw new Error("Release binary must be a regular file");
57
+ // Extract and validate both commands before replacing either installed binary.
58
+ execFileSync("tar", ["-xzf", archivePath, "-C", extractDir, ...binaries], { stdio: "pipe" });
59
+ for (const name of binaries) {
60
+ if (!fs.lstatSync(path.join(extractDir, name)).isFile()) {
61
+ throw new Error("Release binary must be a regular file");
62
+ }
63
+ }
60
64
  fs.mkdirSync(vendorDir, { recursive: true });
61
- stagedBinary = path.join(vendorDir, `.gitvend-${crypto.randomBytes(8).toString("hex")}`);
62
- fs.copyFileSync(binary, stagedBinary, fs.constants.COPYFILE_EXCL);
63
- fs.chmodSync(stagedBinary, 0o755);
64
- fs.renameSync(stagedBinary, path.join(vendorDir, name));
65
+ for (const name of binaries) {
66
+ // Launch the plugin directly: JobDB restricts PATH and owns its process.
67
+ const destinationDir = name === "gitvend" ? vendorDir : path.join(packageRoot, "bin");
68
+ fs.mkdirSync(destinationDir, { recursive: true });
69
+ const temporary = path.join(destinationDir, `.${name}-${crypto.randomBytes(8).toString("hex")}`);
70
+ staged.push([temporary, path.join(destinationDir, name)]);
71
+ fs.copyFileSync(path.join(extractDir, name), temporary, fs.constants.COPYFILE_EXCL);
72
+ fs.chmodSync(temporary, 0o755);
73
+ }
74
+ for (const [temporary, destination] of staged) fs.renameSync(temporary, destination);
65
75
  } finally {
66
- if (stagedBinary) fs.rmSync(stagedBinary, { force: true });
76
+ for (const [temporary] of staged) fs.rmSync(temporary, { force: true });
67
77
  fs.rmSync(tempDir, { recursive: true, force: true });
68
78
  }
69
79
  }