@vectros-ai/cli 0.19.0 → 0.21.0

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.
package/README.md CHANGED
@@ -9,9 +9,13 @@ The Vectros provisioning CLI — the `vectros` binary:
9
9
  - **`vectros blueprint`** — author + inspect blueprints (`init` / `validate` / `plan` / `list`), creds-free.
10
10
  - **`vectros blueprint-test`** — apply a blueprint to a live env, assert it provisioned a working credential, then tear it down.
11
11
  - **`vectros context`** — create, list, and destroy app contexts.
12
- - **`vectros issuers`** — inspect registered third-party IdP issuers (token-exchange trust anchors) for a context.
12
+ - **`vectros issuers`** — register, update, delete, and inspect third-party IdP issuers (token-exchange trust anchors) for a context.
13
+ - **`vectros namespace`** — register a new scope namespace, and list/get existing ones.
13
14
  - **`vectros identity`** — manage tenant-wide principals (users, orgs, clients, and namespaces you register).
14
15
  - **`vectros role`** — define reusable, context-scoped access rules.
16
+ - **`vectros scripts`** — push, list, inspect, and delete the versioned script source a trigger
17
+ fires, or your own app executes directly.
18
+ - **`vectros assume`** — switch identity namespace values on your own token, per a role's `assumable` grant.
15
19
  - **`vectros access`** — bind principals to contexts, and inspect what a binding grants.
16
20
  - **`vectros key`** — issue, list, revoke, and rotate scoped API keys (`ssk_*`).
17
21
  - **`vectros keyring`** — hold several identities on one machine, pick which is active, and let other tools resolve keys from one place.
@@ -148,23 +152,61 @@ flags (`--base-url`, `--token`).
148
152
 
149
153
  ## `vectros issuers` — registered third-party IdP issuers
150
154
 
151
- Read the third-party identity-provider registrations a context trusts for
155
+ Manage the third-party identity-provider registrations a context trusts for
152
156
  [RFC 8693 token exchange](https://www.rfc-editor.org/rfc/rfc8693) — the
153
157
  `issuer`/`jwksUri`/`audience` a blueprint's `issuers[]` entries register, plus
154
- any self-signup policy attached to one. Read-only: registering an issuer is
155
- done through a blueprint (`bootstrap`/`blueprint-test`), not this command.
158
+ any self-signup policy attached to one. Everything a blueprint's `issuers:`
159
+ block can do is available standalone here too — adding a second IdP, or
160
+ editing one issuer's `selfSignupPolicies` after initial setup, no longer
161
+ requires hand-authoring a blueprint and re-running a full apply.
156
162
 
157
163
  ```bash
164
+ vectros issuers register --context my-app --issuer-id auth0-prod \
165
+ --issuer https://your-tenant.us.auth0.com/ \
166
+ --jwks-uri https://your-tenant.us.auth0.com/.well-known/jwks.json \
167
+ --audience https://api.your-app.example.com \
168
+ --captured-claim name --captured-claim phone_number
158
169
  vectros issuers list --context my-app
159
170
  vectros issuers get primary --context my-app
171
+ vectros issuers update primary --context my-app --status suspended
172
+ vectros issuers delete primary --context my-app # re-types the id to confirm
160
173
  ```
161
174
 
162
175
  | Subcommand | Flags | Notes |
163
176
  |---|---|---|
177
+ | `register` | `--context`, `--issuer-id`, `--issuer`, `--jwks-uri`, `--audience` (all required); `--sub-claim`, `--email-claim`, `--userinfo-uri`, `--self-signup-policy <type:roleId>` (repeatable), `--captured-claim <name>` (repeatable), `--tenant test\|live` | Idempotent by `--issuer-id` within the tenant — a repeat returns the existing registration unchanged, not an error. `--captured-claim` names additional OIDC claims to capture beyond the email claim on every successful token exchange, stored as your tenant's golden identity-provider-asserted copy for that user; omit it to capture nothing beyond email. At most 20 names of at most 64 characters, no duplicates. |
178
+ | `update <issuerId>` | `--context <contextId>` (required); `--sub-claim`, `--email-claim`, `--userinfo-uri`, `--status active\|suspended`, `--self-signup-policy <type:roleId>` (repeatable), `--clear-self-signup-policies`, `--captured-claim <name>` (repeatable), `--clear-captured-claims`, `--tenant test\|live` | Omitting `--captured-claim` leaves the stored list UNCHANGED; `--clear-captured-claims` sends an explicit empty list (capture nothing beyond email). The two are mutually exclusive, and the same absent-vs-clear split applies to `--self-signup-policy`. Only ever sends the SAFE, mutable fields — `issuer`/`jwks-uri`/`audience`/`contextId` are trust-anchor fields, immutable via this route on the platform side; rotating one is `delete` + `register`. |
179
+ | `delete <issuerId>` | `--context <contextId>` (required), `--force`, `--tenant test\|live` | Re-types the issuer id to confirm (mirrors `context destroy`); `--force` skips the prompt and is required when stdin isn't a TTY. Refused server-side while any user was ever created or matched through this issuer. |
164
180
  | `list` | `--context <contextId>` (required), `--tenant test\|live` | Every issuer registered in that context. `--context` has no default — an issuer read is confined to the context you name; there's no tenant-wide "every issuer in every context" view. |
165
- | `get <issuerId>` | `--context <contextId>` (required), `--tenant test\|live` | One issuer's full registration, including its self-signup policies. |
181
+ | `get <issuerId>` | `--context <contextId>` (required), `--tenant test\|live` | One issuer's full registration, including its self-signup policies and captured claims. |
166
182
 
167
- Both accept `--json`, plus the common auth flags (`--base-url`, `--token`).
183
+ All five accept `--json`, plus the common auth flags (`--base-url`, `--token`).
184
+
185
+ ## `vectros namespace` — scope-namespace registrations
186
+
187
+ Register and inspect the namespaces a `scope:<namespace>` value can belong
188
+ to — whether values in that namespace must resolve to a real identity entity
189
+ (`entityBacked`, the same field `identity create --type <namespace>`
190
+ relies on) or are free-form strings. Registering a brand-new namespace (e.g.
191
+ a `practice`/`team` entity dimension) was blueprint-apply-only before this
192
+ command existed.
193
+
194
+ ```bash
195
+ vectros namespace register practice --context my-app --specificity-rank 1500 --entity-backed
196
+ vectros namespace list # tenant-wide registrations
197
+ vectros namespace list --context my-app # just my-app's own
198
+ vectros namespace get practice --context my-app
199
+ ```
200
+
201
+ | Subcommand | Flags | Notes |
202
+ |---|---|---|
203
+ | `register <name>` | `--context`, `--specificity-rank <n>` (both required); `--entity-backed`, `--default-schema-id <id>` | NOT idempotent — a repeat name 400s (unlike `issuers register`). `--context` has no tenant-wide option here — a confined credential may only register against its own bound context. |
204
+ | `list` | `--context <contextId>` (optional) | Omit for every tenant-wide registration; supply for one context's own. |
205
+ | `get <name>` | `--context <contextId>` (optional) | Same two-mode read as `list`. |
206
+
207
+ No `update`/`delete` — both routes require a genuine root API key on the
208
+ platform side, which this CLI never holds, so neither could ever succeed
209
+ here. All three subcommands accept `--json`.
168
210
 
169
211
  ## `vectros identity` — tenant-wide principals
170
212
 
@@ -190,12 +232,13 @@ vectros identity invite --context my-app --role editor --email bob@example.com \
190
232
  | | `--type user` only: `--email <email>`, `--service` (mint a SERVICE principal; default HUMAN) | `--name`/`--scope` are rejected for `--type user`. |
191
233
  | | entity namespaces only: `--name <name>`, `--scope <ns:value>` (repeatable, up to 2 parent ownership edges) | `--email`/`--service` are rejected for an entity namespace. |
192
234
  | | both: `--metadata <json>` | Free-form JSON object. |
193
- | `list` | `--type <user\|namespace>` (required), `--external-id <id>` (filter), `--limit <n>` | Omit `--limit` and the CLI drains every page for the complete set; pass `--limit` (1–100) and it returns exactly one page of that size instead. |
235
+ | `list` | `--type <user\|namespace>` (required), `--external-id <id>` (filter), `--limit <n>` | Scoped to your own context — see `list-all` below for a user created but never granted access anywhere. Omit `--limit` and the CLI drains every page for the complete set; pass `--limit` (1–100) and it returns exactly one page of that size instead. |
236
+ | `list-all` | `--external-id <id>` (filter), `--tenant test\|live`, `--limit <n>` | **Users only, and requires an account owner login.** Lists every user in the tenant, regardless of app-context access — for recovering a user `identity list` can never show you: one created but not (yet) granted access anywhere, so no context-scoped view can ever reach it, and a `create` retry with the same `--external-id` just collides with the same invisible row. Find it here by `--external-id`, then grant it access or `identity delete` it with an ordinary credential. |
194
237
  | `get` | `--type <user\|namespace>` (required), `--id <vectrosId>` (required) | `--id` is the Vectros-assigned id from `create`/`list`, not the `--external-id`. |
195
238
  | `delete` | `--type <user\|namespace>` (required), `--id <vectrosId>` (required) | |
196
239
  | `invite` | `--context <c>`, `--role <roleId>`, `--email <e>` (all required); `--first-name <n>`, `--accept-url <url>`, `--no-send-email` | Onboards a person into a context by email (`POST /v1/users/invite`) — no root key required, your `vectros login` session already carries enough scope. `--role` is an existing Role's own id (no separate lookup). `--accept-url` is required unless `--no-send-email` is given, in which case the response carries the raw `inviteToken`/`acceptLink` for you to deliver yourself. |
197
240
 
198
- `create`/`list`/`get`/`invite` accept `--json`; `delete` does not.
241
+ `create`/`list`/`list-all`/`get`/`invite` accept `--json`; `delete` does not.
199
242
 
200
243
  ## `vectros role` — reusable, context-scoped access rules
201
244
 
@@ -210,6 +253,10 @@ vectros role create --context my-app --role-id editor --name Editor \
210
253
  --actions records:cru,search:r
211
254
  vectros role create --context my-app --role-id own-org --name "Own org" \
212
255
  --actions records:r --data-scope '{"scope:org":["<orgId>", null]}'
256
+ vectros role create --context my-app --role-id hr-admin --name "HR Admin" \
257
+ --actions records:cru --assumable '{"scope:org":["org_engineering", "org_sales"]}'
258
+ vectros role create --context my-app --role-id delegator --name Delegator \
259
+ --actions profiles:c --assignable-role editor --assignable-role own-org
213
260
  vectros role list --context my-app
214
261
  vectros role get --context my-app --role-id editor
215
262
  vectros role delete --context my-app --role-id editor
@@ -217,13 +264,87 @@ vectros role delete --context my-app --role-id editor
217
264
 
218
265
  | Subcommand | Flags | Notes |
219
266
  |---|---|---|
220
- | `create` | `--context <c>`, `--role-id <id>`, `--name <n>`, `--actions <csv>` (all required); `--data-scope <json>`, `--description <text>` | Idempotent by `roleId`. `--data-scope` confines the clause to owned data — a JSON map of ownership dimension → allowed values, ANDed across keys (e.g. `{"userId":["${{ self.userId }}"]}`, or `{"scope:org":["<orgId>", null]}` — the `null` entry additionally admits scope-less/tenant-shared records). Keys must be `userId` or `scope:<namespace>` (the legacy flat `orgId`/`clientId` keys are rejected server-side). |
221
- | `list` | `--context <c>` (required) | |
222
- | `get` | `--context <c>`, `--role-id <id>` (both required) | Also shows the role's `assumable` grant (which values, per namespace, a holder of this role may become via `POST /v1/auth/token/assume`), if any is set. `role create` has no `--assumable` flag today — author it via a blueprint's `roleAssumable:` block or a direct API call. |
223
- | `delete` | `--context <c>`, `--role-id <id>` (both required) | No cascade — any access profile still referencing the role fails to mint (not silently downgraded) until reassigned. |
267
+ | `create` | `--context <c>`, `--role-id <id>`, `--name <n>`, `--actions <csv>` (all required); `--data-scope <json>`, `--assumable <json>`, `--assignable-role <roleId>` (repeatable), `--description <text>`, `--tenant test\|live` | Idempotent by `roleId`. `--assignable-role` restricts WHICH named roles this clause may compose into a delegated AccessProfile — orthogonal to `--data-scope`, which bounds data reach rather than role composition. Omit it for no restriction; the platform rejects an empty list, so there is no way to spell "compose nothing". Once a clause carries the restriction it can compose only the roles it names, so name every roleId it still needs before adding the flag. `access explain` reports the restriction for clauses that carry one. `--data-scope` confines the clause to owned data — a JSON map of ownership dimension → allowed values, ANDed across keys (e.g. `{"userId":["${{ self.userId }}"]}`, or `{"scope:org":["<orgId>", null]}` — the `null` entry additionally admits scope-less/tenant-shared records). Keys must be `userId` or `scope:<namespace>` (the legacy flat `orgId`/`clientId` keys are rejected server-side). `--assumable` is a SEPARATE, role-level grant — a JSON map of `scope:<namespace>` → allowed values naming which values a holder of this role may switch into via `vectros assume`; holding broad `--data-scope` reach in a namespace does not by itself grant assuming any value in it. |
268
+ | `list` | `--context <c>` (required); `--tenant test\|live` | Add `--tenant test` to read a context that lives in the test tenant — a blueprint applied there is otherwise unreadable. |
269
+ | `get` | `--context <c>`, `--role-id <id>` (both required); `--tenant test\|live` | Also shows the role's `assumable` grant, if any is set. |
270
+ | `delete` | `--context <c>`, `--role-id <id>` (both required); `--tenant test\|live` | No cascade — any access profile still referencing the role fails to mint (not silently downgraded) until reassigned. |
224
271
 
225
272
  `create`/`list`/`get` accept `--json`; `delete` does not.
226
273
 
274
+ ## `vectros scripts` — the script source a trigger fires, or your app runs directly
275
+
276
+ A stored script has two consumers, and this is how the source gets there for both. A **trigger rule**
277
+ names it in `scriptRef` and runs it under the trigger's own grant; that `scriptRef` is validated when
278
+ the rule is DECLARED rather than when it fires, so a script has to exist before any rule can name it.
279
+ **Your own app** runs a stored version directly through `POST /v1/scripts/execute`, under its own
280
+ credential, committing everything the script wrote as a single transaction.
281
+
282
+ Direct execution is separately granted: the calling role needs `scripts:x`, and without it the call
283
+ is a `403` that cannot say the missing grant is the reason. The platform accepts it bare (every
284
+ script in the context) or qualified to one name (`scripts:x:create-case`). A **blueprint** may use
285
+ only the qualified form — its scope gate refuses a bare `scripts:x` and there is no wildcard — so a
286
+ blueprint that ships a script grants exactly the one it ships. See *The scope gate* below, and
287
+ [`AUTHORING.md`](./AUTHORING.md) for where that grant goes in a blueprint.
288
+
289
+ ```bash
290
+ vectros scripts push notify-assignee --context my-app --source-file ./notify.js
291
+ vectros scripts push notify-assignee --context my-app --source-file ./notify.js --if-changed
292
+ vectros scripts list --context my-app # every script, newest first
293
+ vectros scripts list --context my-app --name notify-assignee # every version of one name
294
+ vectros scripts get 3b1c9f2e7a5d… --context my-app # one version, with its source
295
+ vectros scripts delete 3b1c9f2e7a5d… --context my-app # ONE version (prompts)
296
+ vectros scripts delete 3b1c9f2e7a5d… --context my-app --force # ...or skip the prompt, e.g. in CI
297
+ ```
298
+
299
+ | Subcommand | Flags | Notes |
300
+ |---|---|---|
301
+ | `push` | `--context <c>`, `--source-file <path>` (both required); `--input-contract <text>` or `--input-contract-file <path>`, `--if-changed`, `--tenant <test\|live>` | **Always stores a new version by default** — pushing is not idempotent, and each push gets the next server-assigned version number, which the output reports. `--if-changed` reads the current version first and pushes only on a genuine difference; use it where the same push re-runs, e.g. in CI. `--source-file` is capped at 300,000 UTF-8 bytes and the input contract at 50,000, both checked locally so an oversized push fails immediately instead of sending the body. The input contract is descriptive only — nothing validates a firing against it. Nothing parses the source either, so a syntax error surfaces the first time the version runs. |
302
+ | `list` | `--context <c>` (required); `--name <name>`, `--tenant <test\|live>` | With `--name`, every version of that name, oldest first; without, every script in the context, newest first. The human view shows a byte count instead of the source — a listing would otherwise print the full text of every version. `--json` includes the source. |
303
+ | `get` | `--context <c>` (required) + the version id; `--tenant <test\|live>` | Shows one version, including its full source. Version ids are opaque — a deterministic hex string, not a prefixed or random id — so copy them from `scripts list`. |
304
+ | `delete` | `--context <c>` (required) + the version id; `--force`, `--tenant <test\|live>` | Deletes ONE version; other versions of the same name are untouched. **Irreversible — the stored source is the only copy** — so it asks you to type the version id back before proceeding. `--force` skips that, and is REQUIRED in a non-interactive shell rather than the prompt being silently waived there. `--json` emits `{id, deleted}` on stdout; the warning always goes to stderr, so it stays visible without breaking a pipe. ⚠️ Deleting a name's LATEST version reuses its identity — the next push of that name recomputes the same version number and the same id, so a pinned reference silently resolves to different source. |
305
+
306
+ There is no `scripts update` — a stored version is immutable, and the platform's update route always
307
+ rejects. Supersede a version by pushing a new one.
308
+
309
+ `--context` is required on all four. Scripts are context-scoped and a trigger resolves its
310
+ `scriptRef` inside its own context, so a defaulted context would push where nobody asked and leave
311
+ the rule that wanted it still unresolvable. `--tenant` defaults to `live`; pass `--tenant test` to
312
+ reach the versions a `blueprint-test --tenant test` run created.
313
+
314
+ ⚠️ **`delete` is not reference-checked.** Nothing verifies whether a trigger rule still points at the
315
+ version being removed, and the two outcomes differ: a rule pinned to that version fails on every
316
+ subsequent firing, while a rule on `version: latest` silently resolves to the PREVIOUS version and
317
+ carries on running older code with nothing to indicate it. This CLI has no verb that lists trigger
318
+ rules, so it cannot answer that question for you — check against the API before removing a version a
319
+ rule may still use.
320
+
321
+ A blueprint can ship a script instead of pushing one — see `scripts:` in
322
+ [`@vectros-ai/blueprints`](https://www.npmjs.com/package/@vectros-ai/blueprints). An apply is
323
+ convergent (an unchanged source pushes nothing) and never deletes: removing a `scripts:` entry
324
+ leaves every version in place, which is why this command group exists to remove one deliberately.
325
+
326
+ ## `vectros assume` — switch identity namespace values on your own token
327
+
328
+ Re-mints your own verb token with one or more `identity.<namespace>` values
329
+ switched — the "org switcher" flow, named (like the platform endpoint it
330
+ calls) after AWS STS `AssumeRole`. Only works if a role you currently hold
331
+ explicitly grants assuming the requested value(s) via that role's own
332
+ `--assumable` grant (checked live against your CURRENT roles, never a value
333
+ frozen into your token at mint time).
334
+
335
+ ```bash
336
+ vectros assume --context my-app --scope org:org_sales
337
+ ```
338
+
339
+ | Flags | Notes |
340
+ |---|---|
341
+ | `--context <c>` (required), `--scope <ns:value>` (repeatable, at least one required) | Each `--scope` becomes one `scope:<namespace>` entry in the request. No artificial cap on how many namespaces you name — the platform requires a SINGLE role to jointly grant the whole combination and rejects an unsatisfiable one itself. |
342
+
343
+ Prints the re-minted `access_token`/`expires_in`, plus the resolved `identity` and `allowedActions` the platform returns alongside it — what you actually assumed, without decoding the token (accepts `--json`) — there
344
+ is no `--to-keyring` here, since the result is an ephemeral bearer token
345
+ with the same remaining lifetime as the verb token this command itself
346
+ mints to make the call, not a durable secret the keyring is built to hold.
347
+
227
348
  ## `vectros access` — bind principals to contexts
228
349
 
229
350
  The binding layer between an identity and a context: `access grant` joins a
@@ -249,11 +370,11 @@ shares this grammar.
249
370
 
250
371
  | Subcommand | Flags | Notes |
251
372
  |---|---|---|
252
- | `grant` | `--principal <p>`, `--context <c>` (required); exactly one of `--role <roleId>` (repeatable — 2+ compose additively) / `--actions <csv>`; `--scope <ns:value>` (repeatable), `--identity-overrides <json>` | Idempotent **upsert** — re-granting an existing binding updates it to the newly-declared shape (role(s)/actions/overrides), it does not no-op. Repeating `--role` composes: `--role case-handler --role hr-admin` grants each role's own clauses, concatenated in the order given — never merged, so each clause keeps meaning exactly what its own author wrote. `--scope`/`--identity-overrides` extend the principal's stamped identity (e.g. `--scope org:<id>` → `scope:org`) — a credential can only stamp ownership its identity carries, so granting write access to org-scoped data requires that scope on the profile. When both are given, `--identity-overrides` wins on key conflicts. |
253
- | `revoke` | `--principal <p>`, `--context <c>` (required) | |
254
- | `list` | Exactly one of `--context <c>` / `--principal <p>` | `--context` lists the context's members; `--principal` lists every context that principal is bound to. |
255
- | `get` | `--principal <p>`, `--context <c>` (required) | |
256
- | `explain` | `--principal <p>`, `--context <c>` (required) | Resolves the binding's clauses (following every referenced role and concatenating their clauses, if bound via one or more) and renders a concrete **access matrix** — a canned set of record-ownership archetypes (yours alone, another user's, org-shared, scope-less, …) evaluated against the clauses. A client-side preview for authoring-time sanity, not the platform's own evaluator: it models READ reachability only — a `c`/`u`/`d` action shown as granted is advisory, not a guarantee a specific write will succeed, since the platform's write rule (a clause must explicitly name a dimension to place a value into it) diverges from its read rule (an unmentioned dimension never narrows reads). |
373
+ | `grant` | `--principal <p>`, `--context <c>` (required); exactly one of `--role <roleId>` (repeatable — 2+ compose additively) / `--actions <csv>`; `--scope <ns:value>` (repeatable), `--identity-overrides <json>`, `--tenant test\|live` | Idempotent **upsert** — re-granting an existing binding updates it to the newly-declared shape (role(s)/actions/overrides), it does not no-op. Repeating `--role` composes: `--role case-handler --role hr-admin` grants each role's own clauses, concatenated in the order given — never merged, so each clause keeps meaning exactly what its own author wrote. `--scope`/`--identity-overrides` extend the principal's stamped identity (e.g. `--scope org:<id>` → `scope:org`) — a credential can only stamp ownership its identity carries, so granting write access to org-scoped data requires that scope on the profile. When both are given, `--identity-overrides` wins on key conflicts. |
374
+ | `revoke` | `--principal <p>`, `--context <c>` (required); `--tenant test\|live` | |
375
+ | `list` | Exactly one of `--context <c>` / `--principal <p>`; `--tenant test\|live` | `--context` lists the context's members; `--principal` lists every context that principal is bound to. |
376
+ | `get` | `--principal <p>`, `--context <c>` (required); `--tenant test\|live` | |
377
+ | `explain` | `--principal <p>`, `--context <c>` (required); `--tenant test\|live` | Resolves the binding's clauses (following every referenced role and concatenating their clauses, if bound via one or more) and renders a concrete **access matrix** — a canned set of record-ownership archetypes (yours alone, another user's, org-shared, scope-less, …) evaluated against the clauses. A client-side preview for authoring-time sanity, not the platform's own evaluator: it models READ reachability only — a `c`/`u`/`d` action shown as granted is advisory, not a guarantee a specific write will succeed, since the platform's write rule (a clause must explicitly name a dimension to place a value into it) diverges from its read rule (an unmentioned dimension never narrows reads). |
257
378
 
258
379
  `grant`/`list`/`get`/`explain` accept `--json`; `revoke` does not.
259
380
 
@@ -409,11 +530,11 @@ CLI runs under an elevated ephemeral bridge token. So the trust boundary is
409
530
  **this binary, not the blueprint**: a hardcoded **scope gate** bounds every
410
531
  mint to a data-plane allowlist (`records` / `schemas` / `search` / `documents`
411
532
  / `folders` / `inference` / `entities`). Most control-plane scope (keys /
412
- app-contexts / users / billing / admin / namespaces) or a `*` wildcard
413
- **hard-rejects** — the CLI mints nothing and exits non-zero. There is
533
+ app-contexts / users / billing / admin / namespaces / triggers) or a
534
+ `*` wildcard **hard-rejects** — the CLI mints nothing and exits non-zero. There is
414
535
  intentionally **no override flag**.
415
536
 
416
- **One narrow, deliberate exception:** `profiles` (which configures who has
537
+ **Two narrow, deliberate exceptions. The first:** `profiles` (which configures who has
417
538
  access to what — control-plane by nature) admits its `r`/`u`/`d` operations,
418
539
  because the platform enforces an unconditional check that a write to an
419
540
  AccessProfile can never grant more scope than the requesting credential
@@ -431,16 +552,26 @@ requires. A literal-qualified `profiles:c:usr_<id>` naming another principal
431
552
  still hard-rejects regardless of `member-lifecycle`, same as every other
432
553
  control-plane scope.
433
554
 
555
+ **The second exception:** `scripts:x:<script-name>` — execute one named script — is admitted, so a
556
+ blueprint that ships a script in its `scripts:` block can also grant permission to run it. Execution
557
+ consults no data scope at all, and every write the script makes is still checked one by one against
558
+ the caller's own permissions, so this widens *what can be composed*, never *what data is reachable*.
559
+ Everything else on the resource still hard-rejects: a bare `scripts:x`, because it would mean "run
560
+ every script in this context, including ones added later that your reviewer never saw"; `scripts:x:*`,
561
+ because there is **no wildcard form** — the qualifier is compared to the script name exactly, so such
562
+ a grant would store cleanly and then refuse every execution; and `scripts:c`, authoring, which stays
563
+ a design-time admin surface. `triggers` is unchanged.
564
+
434
565
  ### Capabilities (`granted_capabilities`) — a separate, equally-gated dimension
435
566
 
436
567
  A blueprint's `accessProfile` and role clauses may also declare
437
568
  `capabilities: [...]` — named platform effects (like creating or removing an
438
569
  identity in your tenant) that reach across a partition boundary in a way no
439
570
  `resource:ops` verb can express. This rides a field entirely separate from
440
- `allowedActions`, and it has its own allowlist: **only `member-lifecycle` is
441
- admitted today.** Every other capability name — including ones the platform's
442
- own registry recognizes — is refused, the same fail-closed, deliberately-widen
443
- posture as the scope gate above. `vectros blueprint validate`/`plan` and
571
+ `allowedActions`, and it has its own allowlist: **`member-lifecycle` and
572
+ `delegate-principal-stamp` are admitted today.** Every other capability name —
573
+ including ones the platform's own registry recognizes — is refused, the same
574
+ fail-closed, deliberately-widen posture as the scope gate above. `vectros blueprint validate`/`plan` and
444
575
  `bootstrap` all report a rejected capability the same way they report a
445
576
  rejected scope: nothing mints, and the CLI exits non-zero.
446
577