@vectros-ai/cli 0.21.0 → 0.23.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
@@ -6,7 +6,8 @@
6
6
  The Vectros provisioning CLI — the `vectros` binary:
7
7
 
8
8
  - **`vectros bootstrap`** — provision a least-privilege MCP credential (+ optional blueprint).
9
- - **`vectros blueprint`** — author + inspect blueprints (`init` / `validate` / `plan` / `list`), creds-free.
9
+ - **`vectros blueprint`** — author + inspect blueprints (`init` / `validate` / `plan` / `list`, creds-free), and
10
+ **`apply`** one into a tenant without minting a key (for CI).
10
11
  - **`vectros blueprint-test`** — apply a blueprint to a live env, assert it provisioned a working credential, then tear it down.
11
12
  - **`vectros context`** — create, list, and destroy app contexts.
12
13
  - **`vectros issuers`** — register, update, delete, and inspect third-party IdP issuers (token-exchange trust anchors) for a context.
@@ -73,7 +74,28 @@ On a machine with no local record of a prior mint (e.g. an ephemeral CI
73
74
  container re-running `--yes` for the first time) against a blueprint whose
74
75
  service principal already exists in the tenant, `--yes` alone isn't enough —
75
76
  add `--confirm-existing-principal` to make that expected case explicit and
76
- non-interactive; see AUTHORING.md's "idempotency contract" for why.
77
+ non-interactive; see AUTHORING.md's "idempotency contract" for why. For a CI re-apply that has no use for the
78
+ key, `vectros blueprint apply` (below) is the better fit.
79
+
80
+ **An issuer that needs verification.** A blueprint's `issuers` entry registered without `restrictedToDomain` starts
81
+ `pending_verification` on platform 0.45.0 and later: it accepts no token until you prove you control the identity
82
+ provider, which takes a real login there after someone adds a claim rule, so no bootstrap can finish it. A finished
83
+ apply is still a successful apply and exits `0`. The summary says `pending verification` next to the issuer, and the
84
+ apply's last output, on stderr, is the challenge (the claim, the one-time value and its expiry), how to add the rule
85
+ at Auth0, Okta, Microsoft Entra ID or Keycloak, and the exact `vectros issuers verify` command that finishes it.
86
+ Re-running bootstrap shows the same challenge until the issuer is verified, then says nothing about it. If the
87
+ challenge has expired while the issuer was still pending it can never be verified: the output says so, and names
88
+ the delete command and the re-run; nothing is deleted for you. Pass `--require-verified-issuers` to make a pending
89
+ issuer exit `4` instead (the apply itself still completes, and a one-line notice of the exit code follows the
90
+ challenge), for a pipeline that must not go green over an issuer that cannot yet sign anyone in. Domain-scoped issuers (`restrictedToDomain`) are active at once and never pending.
91
+
92
+ Exit codes: `0` applied (also `--help`, and answering no at "Apply this plan?"); `1` failed: a bad flag, an
93
+ unknown or malformed blueprint, a missing or invalid `--set` input, a declared issuer scope the platform did not
94
+ apply, or any error while applying (the message says what did and did not run); `2` refused by a gate: the scope
95
+ gate or the capability gate rejected the blueprint, an identity it references could not be resolved, or it declares
96
+ a tenant-wide namespace without the opt-in (the message says what, if anything, was already provisioned); `3` a
97
+ confirmation it did not get (an existing service principal, a key that cannot be recovered); `4` applied in full with
98
+ `--require-verified-issuers` and an issuer still pending verification.
77
99
 
78
100
  A bundled blueprint ships with **sample seed records** so the credential works
79
101
  the moment it's minted. For a **production** context that's synthetic clutter:
@@ -82,9 +104,9 @@ scoped key — but creates no sample records, leaving the context clean for real
82
104
  data. (Interactively, a blueprint with seeds asks before adding them.) This
83
105
  differs from `--blank`, which is credential-only and drops the schemas too.
84
106
 
85
- ## `vectros blueprint` — authoring (creds-free)
107
+ ## `vectros blueprint` — authoring (creds-free), then `apply`
86
108
 
87
- Author and inspect blueprints without an account. You'll run these repeatedly,
109
+ Author and inspect blueprints without an account (everything here except `apply`, which is the next section and needs credentials). You'll run these repeatedly,
88
110
  so install once and drop the `npx` prefix (`npm i -g @vectros-ai/cli`, or prefix
89
111
  each command with `npx -y @vectros-ai/cli`). The inner loop:
90
112
 
@@ -92,18 +114,82 @@ each command with `npx -y @vectros-ai/cli`). The inner loop:
92
114
  vectros blueprint init my-thing # scaffold a commented YAML starter
93
115
  # …edit ./my-thing.blueprint.yaml…
94
116
  vectros blueprint validate ./my-thing.blueprint.yaml # structural + scope-gate + lint
95
- vectros blueprint plan ./my-thing.blueprint.yaml # preview what bootstrap would provision
96
- vectros bootstrap --blueprint ./my-thing.blueprint.yaml # apply it
117
+ vectros blueprint plan ./my-thing.blueprint.yaml # preview what bootstrap or apply would provision
118
+ vectros blueprint apply ./my-thing.blueprint.yaml --tenant test # provision it, no key (see below)
119
+ vectros bootstrap --blueprint ./my-thing.blueprint.yaml # provision it AND mint a key for your MCP client
97
120
  ```
98
121
 
99
122
  - `init` — write a starter (`--from <bundled>` copies an exemplar; `--out`, `--force`).
100
123
  - `validate` — structural parse → scope gate → linter. Accepts a file **or** a bundled name.
101
124
  - `plan` — validate, then a terraform-style preview; mints nothing.
102
125
  - `list` — the bundled library.
126
+ - `apply` — provision a blueprint into a tenant, minting no key; needs credentials (next section).
103
127
 
104
128
  Full guide: [AUTHORING.md](./AUTHORING.md). Format reference:
105
129
  [`@vectros-ai/blueprints`](https://www.npmjs.com/package/@vectros-ai/blueprints).
106
130
 
131
+ ## `vectros blueprint apply` — provision a blueprint, mint no key
132
+
133
+ ```bash
134
+ vectros blueprint apply ./my-thing.blueprint.yaml --tenant test
135
+ # a CI re-apply, from a machine that has never seen this context:
136
+ vectros blueprint apply ./my-thing.blueprint.yaml --tenant live --yes --confirm-existing-principal
137
+ ```
138
+
139
+ `apply` runs the same pipeline as `vectros bootstrap` (the same credential, the same scope and capability gates,
140
+ the same loader) with the key step switched off: the context, its service principal and access profile, schemas,
141
+ roles, issuers, triggers and scripts are created or reconciled (and, only if you pass `--seed`, the blueprint's sample
142
+ seed records are written), but **no scoped key is minted,
143
+ nothing is written to a keyring, and no MCP client config is touched**. (Short-lived bootstrap tokens are minted
144
+ to call the platform, as with `bootstrap`; none is a scoped key.) That is what makes it fit for re-applying
145
+ a changed blueprint from CI: a key is named per machine, so a fresh runner would name (and mint) a new one on every
146
+ run. Use `bootstrap` when you also want the credential.
147
+
148
+ - **The target is explicit.** `--tenant test|live` is required; there is no default, so a run never lands on
149
+ `live` by omission. Before it changes anything `apply` prints the account, tenant, context and API it is
150
+ pointed at, then the plan (which has no key step), then asks to proceed.
151
+ - **It never runs unattended without `--yes`.** With no terminal, `--yes` is required (exit `2`, before anything
152
+ is read).
153
+ - **Deletion is opt-in.** `apply` deletes nothing unless you pass `--prune`. With it, each trigger in the context
154
+ whose stored `provisionedBy` marker equals this blueprint's `name` and whose principal's external id is the one
155
+ this blueprint would derive, and which the file no longer declares, is removed (the rule and its access profile;
156
+ the principal is kept). Both are **stored markers, not verified identities**, and the name comes from the
157
+ blueprint **file** (after any `${{ inputs.* }}` substitution, so `--set` and `--values` can change it): the scope gate bounds what a blueprint may grant, not what it may delete, so a blueprint that
158
+ reuses another one's `name` and omits some of its triggers will remove them. Rows with no marker are left
159
+ alone and reported; another blueprint's rows are left alone. If a removal it matched fails (a failed read or
160
+ delete), `apply` exits `1` and names the trigger that may still be live. Without `--prune` the summary says
161
+ undeclared triggers were left in place. (Apply still replaces an access profile's scopes and a role's rules in
162
+ place, as `bootstrap` does.)
163
+ - **An existing service principal needs `--confirm-existing-principal`** (with `--yes`, or with no terminal),
164
+ which is every apply once the principal exists, on any machine: `apply` never reads a local key record, so a
165
+ machine that ran `bootstrap` is not exempt. It lets `apply` rewrite that principal's access profile, and covers the
166
+ blueprint's own service principal only (the principals a blueprint's triggers use are ensured without this check,
167
+ as with `bootstrap`). In a pipeline this flag is standing consent: it also waves through a blueprint whose
168
+ `servicePrincipal.externalId` collides with an unrelated identity, so keep the blueprint's source reviewed. Every
169
+ run that proceeds prints which principal it reused (id, external id, created time) and why.
170
+ - **Sample seed records are opt-in: `apply` writes none unless you pass `--seed`,** in either tenant, and never asks.
171
+ This is the opposite of `bootstrap` (which seeds unless `--no-seed`) on purpose: a blueprint's seeds are
172
+ illustrative, not real data, and a re-apply from a pipeline must not write them by omission. When the blueprint
173
+ declares sample records (or you pass `--seed`), the plan and the summary say plainly whether they will be or were
174
+ written. `--no-seed` is refused here, because a flag that does nothing invites drift:
175
+ `apply does not seed by default: pass --seed to write the sample records`.
176
+ - **Exit codes:** `0` applied (also `--help`, and declining the confirmation); `1` failed while applying, or
177
+ `--prune` could not remove a trigger it matched (steps earlier in the plan may already have been applied; a re-run
178
+ converges); `2` refused, before anything was changed (a usage error, no `--yes` without a terminal, no credential,
179
+ an unknown or malformed blueprint, a missing `--set` input, an invalid base URL, the scope or capability gate, a
180
+ tenant-wide namespace without the opt-in), or, after the context and principal were provisioned, an identity the
181
+ blueprint references that could not be resolved; `3` an existing principal was not confirmed; `4` applied in full
182
+ but `--require-verified-issuers` was given and an issuer is pending verification.
183
+
184
+ Flags it shares with `bootstrap` (`--set`, `--values`, `--allow-tenant-wide-namespaces`,
185
+ `--allow-script-adoption`, `--require-verified-issuers`, `--token`, `--base-url`) behave the same. `--tenant`,
186
+ `--yes` and `--confirm-existing-principal` are shared but behave as described above (`--tenant` is required here,
187
+ `--yes` is required without a terminal, and the principal flag applies to every re-apply). The flags that concern a
188
+ key or your MCP client (`--rotate`, `--client`, `--no-merge`, `--activate`, `--no-activate`, `--use-scoped-token`,
189
+ `--test`) belong to `bootstrap` and are refused with a pointer; the blueprint is an argument, not `--blueprint`, and
190
+ there is no `--blank`. A script pushed onto an existing hand-authored name (`--allow-script-adoption`) is checked
191
+ when the scripts are pushed, partway through, not up front. `vectros blueprint apply --help` lists everything.
192
+
107
193
  ## `vectros blueprint-test` — verify it actually provisions (creds-required)
108
194
 
109
195
  `validate`/`plan` check a blueprint statically; `blueprint-test` proves it
@@ -170,17 +256,22 @@ vectros issuers list --context my-app
170
256
  vectros issuers get primary --context my-app
171
257
  vectros issuers update primary --context my-app --status suspended
172
258
  vectros issuers delete primary --context my-app # re-types the id to confirm
259
+ vectros issuers verify auth0-prod --context my-app --idp-token-file ./idp-token.txt
173
260
  ```
174
261
 
175
262
  | Subcommand | Flags | Notes |
176
263
  |---|---|---|
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`. |
264
+ | `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), `--restricted-to-domain <domain>`, `--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. `--restricted-to-domain` scopes the registration to a domain already verified for your account (verified once, in the developer portal): a token then matches only when its `hd` claim equals that domain, so the registration is never blocked by an unrelated registration of the same issuer and audience. Google Workspace emits `hd` natively; other identity providers need a claims-mapping rule that emits a claim named exactly `hd`, otherwise none of their tokens will match. An unverified domain is refused with 400; a value that is not a DNS name (including a blank one) is refused locally. `register` never changes an existing registration, so when the response does not carry the requested scope (an existing `--issuer-id` with a different one, or a platform that predates the field) it fails with exit 1 and names `issuers update`, which does apply it; a repeat with the same scope is still an idempotent success. |
265
+ | `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`, `--restricted-to-domain <domain>`, `--clear-restricted-to-domain`, `--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. Omitting `--restricted-to-domain` leaves the stored scope UNCHANGED; `--clear-restricted-to-domain` removes it, and a blank `--restricted-to-domain ''` is refused locally rather than read as a clear. The two are mutually exclusive, and a new domain must already be verified for your account. The platform can refuse to clear a scope (for example when another registration already holds the unrestricted `(issuer, audience)` pair); its message says why. ⚠️ Scoping an existing issuer stops matching every token that carries no matching `hd` claim, including those of users who signed in before. |
179
266
  | `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. |
180
267
  | `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. |
181
- | `get <issuerId>` | `--context <contextId>` (required), `--tenant test\|live` | One issuer's full registration, including its self-signup policies and captured claims. |
268
+ | `get <issuerId>` | `--context <contextId>` (required), `--tenant test\|live` | One issuer's full registration, including its self-signup policies, captured claims and `restrictedToDomain`. |
269
+ | `verify <issuerId>` | `--context <contextId>` (required); `--idp-token-file <path>` (or `-` for stdin), `--idp-token <jwt>`, `--tenant test\|live` | Proves you control the identity provider, activating a `pending_verification` registration (0.45.0+): send a JWT from a real login that carries the registration's one-time value in the claim it names. The JWT can also be piped on stdin, and is checked once and never printed or stored. It is NOT `--token`, which stays your Vectros credential; prefer `--idp-token-file` or stdin, since a value on the command line is visible in your shell history and the process list. Exit `0` verified, `1` this request did not report success (the message says why; `issuers get` shows where the registration stands, which is worth a look before retrying, because a timeout can leave it verified), `2` a bad input (no network is touched). `--idp-token-file` takes a path: a value that looks like a token is refused without being repeated, and so is anything over 64 KB. Given only a pipe, it reads the token from stdin and says so on stderr; `--idp-token-file -` is for a pipe and is refused on a terminal, where a pasted token would be displayed. The provider's OpenID Connect discovery document must list exactly the `jwksUri` you registered. |
182
270
 
183
- All five accept `--json`, plus the common auth flags (`--base-url`, `--token`).
271
+ All six accept `--json`, plus the common auth flags (`--base-url`, `--token`). `register` and `get` print the
272
+ challenge of a `pending_verification` registration (the claim, the one-time value, its expiry) with the same provider
273
+ instructions and the exact `verify` command; a registration made without `--restricted-to-domain` starts that way on
274
+ platform 0.45.0 and later, and `register` exits `0` because a pending issuer is its expected result.
184
275
 
185
276
  ## `vectros namespace` — scope-namespace registrations
186
277
 
@@ -311,12 +402,10 @@ rejects. Supersede a version by pushing a new one.
311
402
  the rule that wanted it still unresolvable. `--tenant` defaults to `live`; pass `--tenant test` to
312
403
  reach the versions a `blueprint-test --tenant test` run created.
313
404
 
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.
405
+ ⚠️ **`delete` is permanent — the stored source is the only copy.** The platform refuses the delete
406
+ (409, naming the rule) if a trigger rule still references the version being removed — directly, or
407
+ via `version: latest` when the version is the current newest of its name — so it either succeeds
408
+ cleanly or is refused with an explanation, never silently.
320
409
 
321
410
  A blueprint can ship a script instead of pushing one — see `scripts:` in
322
411
  [`@vectros-ai/blueprints`](https://www.npmjs.com/package/@vectros-ai/blueprints). An apply is
@@ -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