@vectros-ai/cli 0.23.0 → 0.25.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/AUTHORING.md CHANGED
@@ -55,8 +55,10 @@ A name must be 2–50 chars: a lowercase letter, then lowercase letters / digits
55
55
  only. This is the trust boundary: `keys`/`users`/`admin`/`app-contexts`/
56
56
  `namespaces` and the `*` wildcard are **hard-rejected, no override**.
57
57
  `profiles` is a narrower exception — `r`/`u`/`d` are admitted, and `c` only
58
- when qualified `profiles:c:self` (create a profile for yourself; a bare
59
- `profiles:c`, or one naming another principal, still hard-rejects).
58
+ as `profiles:c:self` (create a profile for yourself) or as a bare
59
+ `profiles:c` on a clause that also declares the `member-lifecycle`
60
+ capability; a bare `profiles:c` without it, and `profiles:c:usr_<id>` naming
61
+ another principal, hard-reject.
60
62
  `scripts` is the other — `scripts:x:<script-name>` (execute one named
61
63
  script) is admitted, and nothing else on that resource is: a bare
62
64
  `scripts:x`, a wildcard `scripts:x:*`, and `scripts:c` (authoring) all
@@ -301,10 +303,11 @@ namespaces:
301
303
  # (user, record, document, entity, self, tenant,
302
304
  # context, scope, versions, lookup) are rejected.
303
305
  # org/client are NOT in that set -- reserved namespace
304
- # names, not built-ins, registered like any other. They
305
- # already exist tenant-wide at rank 1000/2000 (below) --
306
- # pick a different specificityRank, and note a
307
- # context-owned registration shadows the tenant-wide
306
+ # names, not built-ins, registered like any other.
307
+ # Signup does not register them. An older account may
308
+ # still hold tenant-wide rows at rank 1000/2000 (below),
309
+ # so pick a different specificityRank there; a
310
+ # context-owned registration shadows a tenant-wide
308
311
  # one for this context's own callers.
309
312
  specificityRank: 500 # 0..1_000_000, unique among this blueprint's namespaces
310
313
  entityBacked: true # optional
@@ -549,8 +552,9 @@ Omit it and the call is a 403 that cannot tell you the grant is what's missing.
549
552
  See *The scope gate* for why the bare and wildcard forms are refused.
550
553
 
551
554
  **Triggers are the one block an apply reconciles by absence.** Remove an entry
552
- and re-apply, and its rule and access profile are deleted, so a trigger you
553
- removed cannot keep acting. Its service principal is kept, so re-adding the same
555
+ and re-run `vectros bootstrap`, or run `vectros blueprint apply --prune`, and its rule and
556
+ access profile are deleted, so a trigger you removed cannot keep acting.
557
+ `vectros blueprint apply` without `--prune` deletes nothing. Its service principal is kept, so re-adding the same
554
558
  trigger reattaches to one identity. Removing a `scripts:` entry deletes nothing
555
559
  — see *The idempotency contract*, below, for why.
556
560
 
package/CHANGELOG.md CHANGED
@@ -3,6 +3,104 @@
3
3
  All notable changes to `@vectros-ai/cli` are documented here.
4
4
  This project adheres to [Semantic Versioning](https://semver.org).
5
5
 
6
+ ## 0.25.0 — 2026-10-12
7
+
8
+ ### Breaking changes
9
+
10
+ - **⚠️ BREAKING: `vectros provider-config create|update` no longer have `--platform-token-factor` and
11
+ `--platform-output-weight`, and `PLATFORM_BILLED` is no longer a billing mode.** A provider you configure bills you
12
+ directly, so `CUSTOMER_DIRECT` is the only mode, and `--billing-mode` is optional on `create` and defaults to it. A
13
+ script that passes either removed flag stops with commander's `unknown option` error (exit 1).
14
+ `--billing-mode PLATFORM_BILLED` stops locally (exit 2, nothing sent) with a message that the mode is retired, and any
15
+ other value is refused locally with a message that the mode must be `CUSTOMER_DIRECT`. The key-value output of `get`,
16
+ `create` and `update` no longer lists the two rates. **Migration:** delete the two flags from your scripts; for a
17
+ `PLATFORM_BILLED` config that already exists, `vectros provider-config update <id> --billing-mode CUSTOMER_DIRECT`
18
+ moves it to the only supported mode.
19
+
20
+ ### Added
21
+
22
+ - **`vectros identity create|list|get|delete|invite` accept `--tenant test|live`**, as `identity list-all`, `context`,
23
+ `key` and the other tenant-aware verbs do.
24
+
25
+ ### Changed
26
+
27
+ - **`vectros blueprint --help`, `AUTHORING.md` and the comments `vectros blueprint init` writes into the scaffold state what the scope gate enforces.** They say a
28
+ bare `profiles:c` is accepted only when the same clause declares the `member-lifecycle` capability (and is rejected
29
+ without it, and as `profiles:c:usr_<id>` always), that `scripts:x:<script-name>` is admitted, and that
30
+ `member-lifecycle` and `delegate-principal-stamp` are the two capabilities the gate admits. The scaffold's content is
31
+ otherwise the same.
32
+ - **`vectros provider-config` help and README state what the platform accepts for a config you set up yourself
33
+ (platform 0.47.0 and later).** Such a config takes only `--billing-mode CUSTOMER_DIRECT`, the
34
+ `byo-model-inference:customer-key` waiver type and a plain `https://` endpoint on port 443; the platform refuses any
35
+ other waiver type and any other port with a `400`. On `update` only a change to one of these is refused; a value the
36
+ config already holds is accepted. Changing an endpoint's host needs `--api-key-secret-file` again. An account owner
37
+ signs the risk waiver in the developer portal (Model Providers); the CLI does not sign it. An account that has accepted
38
+ the Business Associate Agreement with Vectros in the developer portal also needs an owner to attest each config in the
39
+ portal before a call can use it; the CLI does not attest configs.
40
+ - **`vectros provider-config create --alias` and `update --alias` state the alias rule (platform 0.47.0 and later).** A
41
+ new or changed alias is 1 to 64 lowercase letters, digits, `-` or `_`; the platform answers `400` with the rule
42
+ otherwise. An alias a config already holds is accepted. `update --provider-type` help also states that changing the
43
+ type of an existing config needs `--endpoint` stated in the same command (`--endpoint ''` means the provider's
44
+ default); the platform answers `400` otherwise.
45
+ - **`vectros context destroy` says a blueprint's service principal stays.** The teardown covers the context's own data;
46
+ the service principal a blueprint bootstrap created is tenant-wide, so it is left in place. The command's help names `identity delete`, and its success output names `identity list-all` and `identity delete`
47
+ (with `--tenant test` for a test-tenant context) for the cleanup.
48
+ - **`vectros --version` reports SDK 0.47.0.**
49
+ - **The bundled `@vectros-ai/blueprints` is 0.20.2** (documentation and comments only; behaviour is unchanged).
50
+ - **Source comments state their rules themselves.** Comments that pointed at internal design documents now
51
+ say what the rule is, so the source maps that ship with the package read on their own. No behavior changes.
52
+
53
+ ### Fixed
54
+
55
+ - **`vectros access grant --actions` and `vectros join --actions` work.** Both sent an empty `roleIds` list next to the
56
+ inline actions, and the platform answers that with `400` ("'roleIds' must name at least one role when present"), so
57
+ only `--role` worked. The request now carries `roleIds` only when you pass `--role`, and `scopes` only when you pass
58
+ `--actions`.
59
+ - **`--principal me` (and `join --as me`, `access explain --principal me`) resolve your own principal in the tenant you
60
+ target (needs platform 0.47.0 or later).** Your principal in the test tenant has a
61
+ different id from your live user, so `access grant --principal me --tenant test` can now join you to your own context.
62
+ On an earlier platform the live tenant behaves as before and the test tenant typically stops with a message that names
63
+ `vectros identity list-all --tenant test` and the flag to pass the id with. The "your user id" line of `whoami`, which
64
+ shows the live tenant, uses the same lookup.
65
+ - **A login session that has expired or been revoked says so and exits 2.** A command that needs sign-in says the session has expired or was revoked and names `vectros login`;
66
+ it exited 1 with the raw `token endpoint HTTP 400` response. The "no credential" message names the same command.
67
+ - **A refresh token the sign-in service rotates is kept,** so the next command finds its session valid.
68
+ - **`vectros bootstrap` and `blueprint apply` check for an existing service principal before creating anything.** A
69
+ refusal leaves nothing behind and a re-run starts clean; the plan preview lists the principal before the context. If
70
+ creating the context then fails (for example a plan limit), the principal this run created stays, and the re-run asks
71
+ you to confirm using it: pass `--confirm-existing-principal`.
72
+
73
+ ## 0.24.0 — 2026-10-01
74
+
75
+ ### Added
76
+
77
+ - **`vectros provider-config <create|list|get|update|delete>`** manages your bring-your-own-model provider
78
+ configs (owner-gated). `create` and `list` take `--tenant test|live`; `get`, `update` and `delete` address a
79
+ config by its id. The provider credential is read from `--api-key-secret-file <path>` (`-` for stdin); there is
80
+ no flag that takes the credential on the command line. `update` is a partial patch: an omitted field, including
81
+ `--api-key-secret-file`, keeps the stored value.
82
+ - **`vectros domains <register|list|delete|verify>`** manages domain ownership verification by DNS TXT record
83
+ (owner-gated) — the proof of control that `issuers register --restricted-to-domain` and webhook targets rely on.
84
+ `register` prints the exact TXT record to publish and the `verify` command to run next; registering a domain you
85
+ already hold returns a `409`. `verify` performs one DNS lookup per call and never polls. Domains are shared
86
+ across your live and test tenants, so none of the four verbs take `--tenant`. A verified domain that a webhook
87
+ uses is re-checked periodically; `list` and `verify` report a domain whose re-checks keep failing as
88
+ `REVOKED`.
89
+
90
+ ### Changed
91
+
92
+ - **`vectros --version` reports SDK 0.46.0 and blueprints 0.20.1.**
93
+
94
+ ### Fixed
95
+
96
+ - **`issuers update --clear-restricted-to-domain` is described as the platform applies it.** The platform refuses
97
+ to clear a domain scope unless the registration already holds the unrestricted `(issuer, audience)` pair, which
98
+ a registration created scoped to a domain does not. To get an unrestricted issuer, `issuers delete` the scoped
99
+ registration, register one without `--restricted-to-domain`, and `issuers verify` it. A context holds one
100
+ active issuer, and the delete is refused once any user has been created or matched through the issuer; in that
101
+ case register the new issuer under a different app context. The README, the flag's help and the hints shown for
102
+ a blank `--restricted-to-domain` state this.
103
+
6
104
  ## 0.23.0 — 2026-09-22
7
105
 
8
106
  ### Added
package/README.md CHANGED
@@ -11,8 +11,10 @@ The Vectros provisioning CLI — the `vectros` binary:
11
11
  - **`vectros blueprint-test`** — apply a blueprint to a live env, assert it provisioned a working credential, then tear it down.
12
12
  - **`vectros context`** — create, list, and destroy app contexts.
13
13
  - **`vectros issuers`** — register, update, delete, and inspect third-party IdP issuers (token-exchange trust anchors) for a context.
14
+ - **`vectros provider-config`** — create, list, get, update, and delete BYOM (bring-your-own-model) tenant-level provider configs.
15
+ - **`vectros domains`** — register a domain, list your registrations, and prove DNS TXT ownership.
14
16
  - **`vectros namespace`** — register a new scope namespace, and list/get existing ones.
15
- - **`vectros identity`** — manage tenant-wide principals (users, orgs, clients, and namespaces you register).
17
+ - **`vectros identity`** — manage principals: users (tenant-wide) and identity entities in the namespaces you register (`org`, `client`, or your own).
16
18
  - **`vectros role`** — define reusable, context-scoped access rules.
17
19
  - **`vectros scripts`** — push, list, inspect, and delete the versioned script source a trigger
18
20
  fires, or your own app executes directly.
@@ -198,18 +200,18 @@ the result (the app-context + each schema are readable back, and the freshly
198
200
  minted `ssk_*` authenticates as a scoped key with the requested actions), then
199
201
  **tears down** what it created — schemas, the AccessProfile, seed records, and
200
202
  the key. Teardown is *created-only*: anything that already existed before the run
201
- is left untouched, so it's safe to point at a shared staging tenant.
203
+ is left untouched, so it can be pointed at a tenant other people use.
202
204
 
203
205
  ```bash
204
- # Apply → assert → teardown against staging (sign in first, or pass --token).
205
- vectros blueprint-test ./my-thing.blueprint.yaml --env staging
206
+ # Apply → assert → teardown against your test tenant (sign in first, or pass --token).
207
+ vectros blueprint-test ./my-thing.blueprint.yaml --tenant test
206
208
 
207
209
  # Leave the provisioned graph in place to inspect it.
208
210
  vectros blueprint-test task-management --keep
209
211
  ```
210
212
 
211
213
  Exit `0` = applied and every assertion passed; `1` = an assertion failed or apply
212
- errored; `2` = no credential, or the blueprint requested control-plane scope.
214
+ errored; `2` = no credential, or the scope gate or the capability gate refused the blueprint.
213
215
  The app-context is intentionally **not** deleted (it's idempotent and reused).
214
216
 
215
217
  ## `vectros context` — app contexts
@@ -223,7 +225,7 @@ vectros context create my-app --name "My App" # idempotent — re-running conv
223
225
  vectros context list # every context in the tenant
224
226
  vectros context list --mine # only contexts you hold an access profile in
225
227
  vectros context get my-app
226
- vectros context destroy my-app # irreversible — asks you to re-type the id
228
+ vectros context destroy my-app # irreversible — asks you to re-type the id (a blueprint's service principal stays: see `identity delete`)
227
229
  ```
228
230
 
229
231
  | Subcommand | Flags | Notes |
@@ -231,7 +233,7 @@ vectros context destroy my-app # irreversible — asks you to r
231
233
  | `create <contextId>` | `--name <name>` (defaults to the id), `--tenant test\|live` | Idempotent by `contextId`; re-running converges. |
232
234
  | `list` | `--mine`, `--tenant test\|live` | Default lists every context in the tenant; `--mine` narrows to contexts you hold an active access profile in. |
233
235
  | `get <contextId>` | `--tenant test\|live` | |
234
- | `destroy <contextId>` | `--force`, `--tenant test\|live` | **Irreversible.** Permanently erases the context and ALL of its records, documents, folders, schemas, roles, and access profiles, and revokes its scoped keys up front. Interactively you must re-type the context id to confirm; `--force` skips the prompt and is **required** when stdin is not a TTY (scripts/CI). Reports `status: purging` — the teardown itself runs asynchronously. |
236
+ | `destroy <contextId>` | `--force`, `--tenant test\|live` | **Irreversible.** Permanently erases the context and ALL of its records, documents, folders, schemas, roles, and access profiles, and revokes its scoped keys up front. Interactively you must re-type the context id to confirm; `--force` skips the prompt and is **required** when stdin is not a TTY (scripts/CI). Reports `status: purging` — the teardown itself runs asynchronously. A blueprint's service principal belongs to the whole tenant and is NOT removed; delete it with `identity delete` (find it with `identity list-all`). |
235
237
 
236
238
  All four accept `--json` for machine-readable output, plus the common auth
237
239
  flags (`--base-url`, `--token`).
@@ -262,7 +264,7 @@ vectros issuers verify auth0-prod --context my-app --idp-token-file ./idp-token.
262
264
  | Subcommand | Flags | Notes |
263
265
  |---|---|---|
264
266
  | `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. |
267
+ | `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. From platform 0.45.0 the platform refuses to clear the scope unless the registration already holds the unrestricted `(issuer, audience)` pair, which a registration created scoped to a domain normally does not, since it never proved control of that pair. To get an unrestricted issuer instead, `delete` the scoped registration, `register` a new one without `--restricted-to-domain`, and `verify` it. A context holds one active issuer, and `delete` is refused once any user has been created or matched through the issuer; in that case register the new issuer under a different app context. ⚠️ Scoping an existing issuer stops matching every token that carries no matching `hd` claim, including those of users who signed in before. |
266
268
  | `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. |
267
269
  | `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. |
268
270
  | `get <issuerId>` | `--context <contextId>` (required), `--tenant test\|live` | One issuer's full registration, including its self-signup policies, captured claims and `restrictedToDomain`. |
@@ -273,6 +275,93 @@ challenge of a `pending_verification` registration (the claim, the one-time valu
273
275
  instructions and the exact `verify` command; a registration made without `--restricted-to-domain` starts that way on
274
276
  platform 0.45.0 and later, and `register` exits `0` because a pending issuer is its expected result.
275
277
 
278
+ ## `vectros provider-config` — BYOM tenant-level provider configs
279
+
280
+ Manage per-tenant "bring your own model" provider configs — a customer-supplied inference
281
+ endpoint + credential the platform can route to, instead of (or alongside) Vectros's own
282
+ platform-billed models. A tenant may hold more than one, each independently selected by its
283
+ `alias`. There is no SDK support for this route (it is a dev-portal-only surface, like
284
+ `vectros context`) — every verb here calls the owner-gated developer route directly.
285
+
286
+ ```bash
287
+ vectros provider-config create --tenant live --alias primary \
288
+ --provider-type ANTHROPIC_NATIVE \
289
+ --waiver-type byo-model-inference:customer-key \
290
+ --api-key-secret-file ./anthropic-key.txt
291
+ vectros provider-config list --tenant live
292
+ vectros provider-config get pc_abc123
293
+ vectros provider-config update pc_abc123 --status DISABLED
294
+ vectros provider-config delete pc_abc123 # re-types the id to confirm
295
+ ```
296
+
297
+ | Subcommand | Flags | Notes |
298
+ |---|---|---|
299
+ | `create` | `--alias`, `--provider-type ANTHROPIC_NATIVE\|OPENAI_COMPATIBLE`, `--waiver-type`, `--api-key-secret-file <path>` (all required); `--billing-mode CUSTOMER_DIRECT` (optional: the default, and the only value), `--endpoint`, `--declared-processing-region`, `--default-provider-model-id`, `--status`, `--tenant test\|live` | NOT idempotent — a repeat `--alias` in the same tenant 400s. `--alias` is 1 to 64 lowercase letters, digits, `-` or `_`; the platform refuses anything else with a `400` (callers must send it exactly, the match is case-sensitive). `--endpoint` is required for `OPENAI_COMPATIBLE` (no default — every self-hosted deployment is a distinct URL) and an optional override for `ANTHROPIC_NATIVE`; it must be an `https://` URL whose host resolves to a public address. `--status ACTIVE` (the default) requires the tenant already hold a granted waiver for `--waiver-type`. |
300
+ | `list` | `--tenant test\|live` | Every config in one tenant — there is no combined test+live view. |
301
+ | `get <id>` | | One config's metadata. The secret is never returned by the platform, on this or any other verb. |
302
+ | `update <id>` | Same fields as `create` except `--tenant`, all optional | Partial update — an omitted field leaves the stored value unchanged. A new `--alias` must follow the same rule as on `create`; an alias the config already holds is accepted, so a config created before the rule keeps working. Changing `--provider-type` of an existing config needs `--endpoint` stated in the same command (`--endpoint ''` means the provider's default); without it the platform answers `400`, because a stored endpoint would otherwise keep overriding the Anthropic default. One exception to the omitted-field rule: `--endpoint` naming a different host also needs `--api-key-secret-file` (the platform answers `400` rather than send the stored key to a new host). |
303
+ | `delete <id>` | `--force` | Re-types the id to confirm (mirrors `issuers delete`); `--force` skips the prompt and is required when stdin is not a TTY. |
304
+
305
+ **There is deliberately no `--api-key-secret <value>` flag** — a live provider credential on the
306
+ command line is visible in shell history and the process list. Pass `--api-key-secret-file
307
+ <path>` (`-` for stdin instead of a file), the same shape `issuers verify
308
+ --idp-token-file` already uses for a different live credential.
309
+
310
+ **Billing mode.** `CUSTOMER_DIRECT` (your provider bills you) is the only billing mode: Vectros does not bill
311
+ for a model provider you configure. `--billing-mode PLATFORM_BILLED` is refused locally as retired, and the old
312
+ `--platform-token-factor` and `--platform-output-weight` flags no longer exist (the CLI reports them as unknown
313
+ options). A script that passed `--billing-mode CUSTOMER_DIRECT` keeps working, and the flag may be left out.
314
+
315
+ **What the platform accepts for a config you set up yourself (platform 0.47.0 and later).** An account that sets a
316
+ config up itself is refused by the platform with a `400` for anything but `--billing-mode CUSTOMER_DIRECT`, the
317
+ `byo-model-inference:customer-key` waiver type, and a plain `https://` endpoint on port 443 (no credentials,
318
+ query or fragment). On `update`, only a **change** to one of those is refused; a value the config already
319
+ holds is accepted. Changing an endpoint's host needs the key again (`--api-key-secret-file`). The key must
320
+ be printable ASCII, at most 4096 characters. A tenant holds at most 25 provider configs.
321
+
322
+ The CLI does not sign the risk waiver: an account owner signs it in the developer portal, under **Model
323
+ Providers**, for each environment (live or test). A config is usable only while that waiver is in force for
324
+ your account. It is not in force when it is neither signed nor granted to your account, when it was signed
325
+ against a text that has since changed, or when it has been revoked. A waiver that Vectros granted to your tenant,
326
+ for which your tenant has no signed acceptance, is not affected by any of these. The portal shows which of these
327
+ applies. An account that has accepted the Business Associate Agreement with Vectros in the developer portal needs one more thing before a call can
328
+ use a config: an account owner attests that config in the developer portal (Model Providers). Until then the
329
+ platform refuses the call with a `403` that says it needs a BAA-holder attestation. The CLI does not attest
330
+ configs.
331
+
332
+ All five accept `--json`, plus the common auth flags (`--base-url`, `--token`).
333
+
334
+ ## `vectros domains` — domain ownership verification
335
+
336
+ Prove control of a domain via a DNS TXT record — the same proof-of-control a `vectros issuers
337
+ register --restricted-to-domain` scope, or a webhook target, is checked against. Verification is
338
+ Account-level (shared across your live and test tenants), so none of these commands take
339
+ `--tenant`.
340
+
341
+ ```bash
342
+ vectros domains register --domain example.com
343
+ # → prints the TXT record to publish and the exact verify command
344
+ vectros domains list
345
+ vectros domains verify dom_abc123
346
+ vectros domains delete dom_abc123 # re-types the id to confirm
347
+ ```
348
+
349
+ | Subcommand | Flags | Notes |
350
+ |---|---|---|
351
+ | `register` | `--domain <domain>` (required) | NOT idempotent — registering a domain you already hold 409s, naming the existing id/status and (if still pending) the `verify` command to run instead. Prints the exact DNS TXT record to publish. |
352
+ | `list` | | Every domain registered for your account, across both tenants. |
353
+ | `verify <id>` | | Performs ONE DNS TXT lookup and reports `VERIFIED`, `FAILED` (never proved) or `REVOKED` (proved once, since lost) — always safe to re-run, never polls for you (the platform's TXT lookup can only succeed once your DNS provider has actually propagated the record). Exit `0` verified, `1` not verified — the platform reports `200` either way, so the exit code (not the HTTP status) is what a script should branch on. |
354
+ | `delete <id>` | `--force` | Re-types the id to confirm; refused server-side (`409`) while any active webhook still references the domain. |
355
+
356
+ All four accept `--json`, plus the common auth flags (`--base-url`, `--token`). A blueprint can
357
+ only REFERENCE an already-verified domain (as `issuers`' `restrictedToDomain` already does) — it
358
+ cannot declare/register one.
359
+
360
+ A verified domain that a webhook uses is re-checked periodically. After repeated consecutive failed
361
+ re-checks it becomes `REVOKED` and the webhooks that use it are suspended; `verify` on a `REVOKED`
362
+ domain restores it to `VERIFIED` once the TXT record is published again, and each suspended webhook is
363
+ then re-enabled individually.
364
+
276
365
  ## `vectros namespace` — scope-namespace registrations
277
366
 
278
367
  Register and inspect the namespaces a `scope:<namespace>` value can belong
@@ -299,13 +388,18 @@ No `update`/`delete` — both routes require a genuine root API key on the
299
388
  platform side, which this CLI never holds, so neither could ever succeed
300
389
  here. All three subcommands accept `--json`.
301
390
 
302
- ## `vectros identity` — tenant-wide principals
391
+ ## `vectros identity` — principals
303
392
 
304
393
  Manage the principals a context's access profiles and roles reference: users
305
394
  (the one fixed principal surface, `--type user`) and namespaced identity
306
395
  entities (`org`, `client`, or any namespace you registered, via `--type
307
- <namespace>`). Identities are **tenant-wide and orthogonal to context** — a
308
- blueprint or role *references* one by id, it never creates one.
396
+ <namespace>`). Users, and entities in a tenant-wide namespace, are **tenant-wide**:
397
+ `context destroy` leaves them in place. An entity in a namespace registered to one
398
+ context (a blueprint registers its namespaces to its own context by default) belongs
399
+ to that context and is erased with it. A blueprint can declare identities in its
400
+ `identities:` block; apply ensures every declared identity exists (created under the bootstrap credential,
401
+ before the context's own credentials), and where an entity lives follows its namespace, as above. A role
402
+ references an identity by id.
309
403
 
310
404
  ```bash
311
405
  vectros identity create --type user --external-id alice@example.com --email alice@example.com
@@ -319,22 +413,22 @@ vectros identity invite --context my-app --role editor --email bob@example.com \
319
413
 
320
414
  | Subcommand | Flags | Notes |
321
415
  |---|---|---|
322
- | `create` | `--type <user\|namespace>` (required), `--external-id <id>` (required — your stable idempotency key) | Idempotent, upsert-by-`externalId`. |
416
+ | `create` | `--type <user\|namespace>` (required), `--external-id <id>` (required — your stable idempotency key), `--tenant test\|live` | Idempotent, upsert-by-`externalId`. |
323
417
  | | `--type user` only: `--email <email>`, `--service` (mint a SERVICE principal; default HUMAN) | `--name`/`--scope` are rejected for `--type user`. |
324
418
  | | entity namespaces only: `--name <name>`, `--scope <ns:value>` (repeatable, up to 2 parent ownership edges) | `--email`/`--service` are rejected for an entity namespace. |
325
419
  | | both: `--metadata <json>` | Free-form JSON object. |
326
- | `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. |
420
+ | `list` | `--type <user\|namespace>` (required), `--external-id <id>` (filter), `--limit <n>`, `--tenant test\|live` | 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. |
327
421
  | `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. |
328
- | `get` | `--type <user\|namespace>` (required), `--id <vectrosId>` (required) | `--id` is the Vectros-assigned id from `create`/`list`, not the `--external-id`. |
329
- | `delete` | `--type <user\|namespace>` (required), `--id <vectrosId>` (required) | |
330
- | `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. |
422
+ | `get` | `--type <user\|namespace>` (required), `--id <vectrosId>` (required), `--tenant test\|live` | `--id` is the Vectros-assigned id from `create`/`list`, not the `--external-id`. |
423
+ | `delete` | `--type <user\|namespace>` (required), `--id <vectrosId>` (required), `--tenant test\|live` | The way to remove the service principal a blueprint bootstrap created: `context destroy` removes the context's data but leaves that principal, because principals belong to the whole tenant. Find its id with `identity list-all`, and delete it only if nothing else uses it. |
424
+ | `invite` | `--context <c>`, `--role <roleId>`, `--email <e>` (all required); `--first-name <n>`, `--accept-url <url>`, `--no-send-email`, `--tenant test\|live` | 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` (and an `acceptLink` when you passed `--accept-url`) for you to deliver yourself. Whenever it is supplied, the accept URL must be an `https://` URL with a domain-name host, never an IP literal. |
331
425
 
332
426
  `create`/`list`/`list-all`/`get`/`invite` accept `--json`; `delete` does not.
333
427
 
334
428
  ## `vectros role` — reusable, context-scoped access rules
335
429
 
336
430
  A Role is a reusable, identity-agnostic scope rule bound to a principal later
337
- via `vectros access grant --role <roleId>` — the same rule can be shared
431
+ via `vectros access grant --principal <p> --context <c> --role <roleId>` — the same rule can be shared
338
432
  across many principals. `role create` builds a **single-clause** role from a
339
433
  flat `--actions` list; author a multi-clause role (several rules unioned
340
434
  together) in a blueprint's `roles:` block instead.
@@ -455,7 +549,7 @@ vectros access revoke --principal usr_<id> --context my-app
455
549
 
456
550
  `--principal` accepts `usr_<userId>`, `key_<keyId>`, or the alias `me` (your
457
551
  own signed-in user, resolved from your login) — every `access` subcommand
458
- shares this grammar.
552
+ shares this grammar. In the **test tenant** your principal has a different id from your live user, so `me` resolves it per tenant: add `--tenant test` and it still works, e.g. `vectros access grant --principal me --context <context-id> --role <role> --tenant test`. This needs platform 0.47.0 or later; on an earlier platform the command typically stops and points you to `vectros identity list-all --tenant test` (an owner login), which shows the id to pass as `--principal usr_<id>`.
459
553
 
460
554
  | Subcommand | Flags | Notes |
461
555
  |---|---|---|
@@ -470,7 +564,7 @@ shares this grammar.
470
564
  ## `vectros join` — enroll and keep the credential, in one step
471
565
 
472
566
  `join` is a **composition verb**: everything it does already exists as a
473
- discrete command (`access grant`, `key issue`, `keyring add`) — `join` runs
567
+ discrete command (`access grant`, then `key issue --to-keyring`) — `join` runs
474
568
  them in order against one context and records the outcome as a keyring
475
569
  entry, so `vectros switch <alias>` (and the MCP server / recall hook) can act
476
570
  as that principal from then on.
@@ -488,11 +582,12 @@ vectros join my-app --as usr_<id> --role editor --alias teammate
488
582
  | `--actions <csv>` | Inline single-clause grant. XOR with `--role`. |
489
583
  | `--as me\|usr_<id>` (default `me`) | Which principal to enroll — `me` resolves to your own signed-in user. |
490
584
  | `--scope <ns:value>` | Repeatable. Places the member under a scope for this context (an identity override on the profile) so their key can WRITE that scope's data. |
491
- | `--alias <name>` | Keyring entry name (defaults to the context id). |
585
+ | `--alias <name>` | Keyring entry name (defaults to the context id; with `--tenant test` the entry is stored as `<alias>.test`). |
586
+ | `--tenant test\|live` (default `live`) | Which tenant the context lives in. |
492
587
  | `--activate` / `--no-activate` | Force (or skip) making the freshly-stored key this machine's active identity. |
493
588
 
494
589
  Re-joining an alias whose secret is still held locally is idempotent: it
495
- re-grants (a no-op upsert) and re-activates without churning the key. Only
590
+ re-grants (a no-op upsert) and re-activates without churning the key (a `--tenant test` join does not displace an active live identity unless you pass `--activate`). Only
496
591
  when the local secret is missing does it rotate to obtain a fresh one.
497
592
 
498
593
  ## `vectros key` — the scoped API key (`ssk_*`) lifecycle
@@ -516,7 +611,8 @@ vectros key rotate --principal usr_<id> --context my-app # revok
516
611
  is optional and stripped if present) — narrower than `access`'s grammar: there
517
612
  is no `me` alias on any `key` subcommand, and a key never authenticates as a
518
613
  `key_*` principal. Look your own id up first with `vectros identity list
519
- --type user` (or `vectros whoami`), or grant/issue via `vectros join`, which
614
+ --type user` (or `vectros whoami`; both show your live tenant, and for the test tenant
615
+ use `vectros identity list-all --tenant test`), or grant/issue via `vectros join`, which
520
616
  does resolve `--as me`.
521
617
 
522
618
  A minted secret has exactly two destinations, and the flags for choosing one
@@ -545,7 +641,7 @@ accept `--json`, `revoke` does not.
545
641
  A machine can hold several Vectros identities (one per context or principal).
546
642
  The keyring stores them as named entries plus one **active** pointer, so you can
547
643
  switch between them without re-issuing keys. `vectros join <context>` adds an
548
- entry and activates it; `vectros key issue`/`key rotate` add one when passed
644
+ entry and activates it (a `--tenant test` join keeps an active live identity unless you pass `--activate`); `vectros key issue`/`key rotate` add one when passed
549
645
  `--to-keyring` (which stores the new secret instead of printing it); `vectros switch
550
646
  <alias>` flips the active one.
551
647