@vectros-ai/cli 0.20.0 → 0.22.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
@@ -13,6 +13,8 @@ The Vectros provisioning CLI — the `vectros` binary:
13
13
  - **`vectros namespace`** — register a new scope namespace, and list/get existing ones.
14
14
  - **`vectros identity`** — manage tenant-wide principals (users, orgs, clients, and namespaces you register).
15
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.
16
18
  - **`vectros assume`** — switch identity namespace values on your own token, per a role's `assumable` grant.
17
19
  - **`vectros access`** — bind principals to contexts, and inspect what a binding grants.
18
20
  - **`vectros key`** — issue, list, revoke, and rotate scoped API keys (`ssk_*`).
@@ -162,7 +164,8 @@ requires hand-authoring a blueprint and re-running a full apply.
162
164
  vectros issuers register --context my-app --issuer-id auth0-prod \
163
165
  --issuer https://your-tenant.us.auth0.com/ \
164
166
  --jwks-uri https://your-tenant.us.auth0.com/.well-known/jwks.json \
165
- --audience https://api.your-app.example.com
167
+ --audience https://api.your-app.example.com \
168
+ --captured-claim name --captured-claim phone_number
166
169
  vectros issuers list --context my-app
167
170
  vectros issuers get primary --context my-app
168
171
  vectros issuers update primary --context my-app --status suspended
@@ -171,11 +174,11 @@ vectros issuers delete primary --context my-app # re-types the id to confirm
171
174
 
172
175
  | Subcommand | Flags | Notes |
173
176
  |---|---|---|
174
- | `register` | `--context`, `--issuer-id`, `--issuer`, `--jwks-uri`, `--audience` (all required); `--sub-claim`, `--email-claim`, `--userinfo-uri`, `--self-signup-policy <type:roleId>` (repeatable), `--tenant test\|live` | Idempotent by `--issuer-id` within the tenant — a repeat returns the existing registration unchanged, not an error. |
175
- | `update <issuerId>` | `--context <contextId>` (required); `--sub-claim`, `--email-claim`, `--userinfo-uri`, `--status active\|suspended`, `--self-signup-policy <type:roleId>` (repeatable), `--clear-self-signup-policies`, `--tenant test\|live` | 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`. |
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. `--jwks-uri`/`--userinfo-uri` must be `https://` — an `http://` value is refused with 400. A tenant may hold at most 50 registrations at once; registering past that cap is also refused with 400. A `--self-signup-policy` naming a role that authors, updates, or deletes scripts (`scripts:c`/`u`/`d`) is refused — a self-signup role may still read or execute scripts. |
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`. `--sub-claim` is refused with 400 once any user has ever bound through this issuer — supplying the value it already has is still a no-op. `--userinfo-uri` must be `https://`, same as at registration. |
176
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. |
177
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. |
178
- | `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. |
179
182
 
180
183
  All five accept `--json`, plus the common auth flags (`--base-url`, `--token`).
181
184
 
@@ -252,6 +255,8 @@ vectros role create --context my-app --role-id own-org --name "Own org" \
252
255
  --actions records:r --data-scope '{"scope:org":["<orgId>", null]}'
253
256
  vectros role create --context my-app --role-id hr-admin --name "HR Admin" \
254
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
255
260
  vectros role list --context my-app
256
261
  vectros role get --context my-app --role-id editor
257
262
  vectros role delete --context my-app --role-id editor
@@ -259,13 +264,63 @@ vectros role delete --context my-app --role-id editor
259
264
 
260
265
  | Subcommand | Flags | Notes |
261
266
  |---|---|---|
262
- | `create` | `--context <c>`, `--role-id <id>`, `--name <n>`, `--actions <csv>` (all required); `--data-scope <json>`, `--assumable <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). `--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. |
263
- | `list` | `--context <c>` (required) | |
264
- | `get` | `--context <c>`, `--role-id <id>` (both required) | Also shows the role's `assumable` grant, if any is set. |
265
- | `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. |
266
271
 
267
272
  `create`/`list`/`get` accept `--json`; `delete` does not.
268
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 permanent — the stored source is the only copy.** The platform refuses the delete
315
+ (409, naming the rule) if a trigger rule still references the version being removed — directly, or
316
+ via `version: latest` when the version is the current newest of its name — so it either succeeds
317
+ cleanly or is refused with an explanation, never silently.
318
+
319
+ A blueprint can ship a script instead of pushing one — see `scripts:` in
320
+ [`@vectros-ai/blueprints`](https://www.npmjs.com/package/@vectros-ai/blueprints). An apply is
321
+ convergent (an unchanged source pushes nothing) and never deletes: removing a `scripts:` entry
322
+ leaves every version in place, which is why this command group exists to remove one deliberately.
323
+
269
324
  ## `vectros assume` — switch identity namespace values on your own token
270
325
 
271
326
  Re-mints your own verb token with one or more `identity.<namespace>` values
@@ -283,7 +338,7 @@ vectros assume --context my-app --scope org:org_sales
283
338
  |---|---|
284
339
  | `--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. |
285
340
 
286
- Prints the re-minted `access_token`/`expires_in` (accepts `--json`) — there
341
+ 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
287
342
  is no `--to-keyring` here, since the result is an ephemeral bearer token
288
343
  with the same remaining lifetime as the verb token this command itself
289
344
  mints to make the call, not a durable secret the keyring is built to hold.
@@ -313,11 +368,11 @@ shares this grammar.
313
368
 
314
369
  | Subcommand | Flags | Notes |
315
370
  |---|---|---|
316
- | `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. |
317
- | `revoke` | `--principal <p>`, `--context <c>` (required) | |
318
- | `list` | Exactly one of `--context <c>` / `--principal <p>` | `--context` lists the context's members; `--principal` lists every context that principal is bound to. |
319
- | `get` | `--principal <p>`, `--context <c>` (required) | |
320
- | `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). |
371
+ | `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. |
372
+ | `revoke` | `--principal <p>`, `--context <c>` (required); `--tenant test\|live` | |
373
+ | `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. |
374
+ | `get` | `--principal <p>`, `--context <c>` (required); `--tenant test\|live` | |
375
+ | `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). |
321
376
 
322
377
  `grant`/`list`/`get`/`explain` accept `--json`; `revoke` does not.
323
378
 
@@ -473,11 +528,11 @@ CLI runs under an elevated ephemeral bridge token. So the trust boundary is
473
528
  **this binary, not the blueprint**: a hardcoded **scope gate** bounds every
474
529
  mint to a data-plane allowlist (`records` / `schemas` / `search` / `documents`
475
530
  / `folders` / `inference` / `entities`). Most control-plane scope (keys /
476
- app-contexts / users / billing / admin / namespaces) or a `*` wildcard
477
- **hard-rejects** — the CLI mints nothing and exits non-zero. There is
531
+ app-contexts / users / billing / admin / namespaces / triggers) or a
532
+ `*` wildcard **hard-rejects** — the CLI mints nothing and exits non-zero. There is
478
533
  intentionally **no override flag**.
479
534
 
480
- **One narrow, deliberate exception:** `profiles` (which configures who has
535
+ **Two narrow, deliberate exceptions. The first:** `profiles` (which configures who has
481
536
  access to what — control-plane by nature) admits its `r`/`u`/`d` operations,
482
537
  because the platform enforces an unconditional check that a write to an
483
538
  AccessProfile can never grant more scope than the requesting credential
@@ -495,6 +550,16 @@ requires. A literal-qualified `profiles:c:usr_<id>` naming another principal
495
550
  still hard-rejects regardless of `member-lifecycle`, same as every other
496
551
  control-plane scope.
497
552
 
553
+ **The second exception:** `scripts:x:<script-name>` — execute one named script — is admitted, so a
554
+ blueprint that ships a script in its `scripts:` block can also grant permission to run it. Execution
555
+ consults no data scope at all, and every write the script makes is still checked one by one against
556
+ the caller's own permissions, so this widens *what can be composed*, never *what data is reachable*.
557
+ Everything else on the resource still hard-rejects: a bare `scripts:x`, because it would mean "run
558
+ every script in this context, including ones added later that your reviewer never saw"; `scripts:x:*`,
559
+ because there is **no wildcard form** — the qualifier is compared to the script name exactly, so such
560
+ a grant would store cleanly and then refuse every execution; and `scripts:c`, authoring, which stays
561
+ a design-time admin surface. `triggers` is unchanged.
562
+
498
563
  ### Capabilities (`granted_capabilities`) — a separate, equally-gated dimension
499
564
 
500
565
  A blueprint's `accessProfile` and role clauses may also declare
@@ -4,9 +4,8 @@
4
4
  published bundle. Their licenses require that these notices travel with the code,
5
5
  so they are reproduced in full below.
6
6
 
7
- This file is GENERATED by `pipelines/third_party_notices.mjs` from `packages/cli/tsup.config.ts`'s
8
- `noExternal` list. Do not edit it by hand — run
9
- `node pipelines/third_party_notices.mjs --write`. CI fails if it is stale, because a
7
+ This file is GENERATED from `tsup.config.ts`'s `noExternal` list by an internal build
8
+ script. Do not edit it by hand. CI fails if it is stale, because a
10
9
  notices file that silently drifts is worse than none: it asserts a compliance that
11
10
  nobody checked.
12
11