@vectros-ai/cli 0.14.0 → 0.16.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
@@ -49,14 +49,24 @@ A name must be 2–50 chars: a lowercase letter, then lowercase letters / digits
49
49
 
50
50
  1. **Structural** — the file parses against the blueprint schema (readable
51
51
  `path: message` errors, e.g. `schemas[0].fields[1]: Required`).
52
- 2. **Scope gate** — the requested `allowedActions` are data-plane only. This is
53
- the trust boundary: control-plane scopes (`keys`, `profiles`,
54
- `users`, `admin`, …) and the `*` wildcard are **hard-rejected, no override**.
55
- A forked or hand-written blueprint gets the *identical* gate a bundled one
56
- does — it can never escalate.
52
+ 2. **Scope + capability gates** — the requested `allowedActions` are data-plane
53
+ only. This is the trust boundary: `keys`/`users`/`admin`/`app-contexts`/
54
+ `namespaces` and the `*` wildcard are **hard-rejected, no override**.
55
+ `profiles` is a narrower exception — `r`/`u`/`d` are admitted, and `c` only
56
+ when qualified `profiles:c:self` (create a profile for yourself; a bare
57
+ `profiles:c`, or one naming another principal, still hard-rejects). Any
58
+ requested `capabilities` are checked the same way, against their own,
59
+ separate allowlist — see "Capabilities" below. A forked or hand-written
60
+ blueprint gets the *identical* gates a bundled one does — it can never
61
+ escalate.
57
62
  3. **Lint** — semantic checks: duplicate `fieldId`, a `lookupField` naming a
58
63
  field that doesn't exist, a `seed` for a typeName with no schema (errors);
59
- `enumValues` on an incompatible type, and the dataScope null-sentinel nudge
64
+ `enumValues` on an incompatible type, the dataScope null-sentinel nudge,
65
+ and a role/`accessProfile` clause that omits `dataScope` **entirely** on
66
+ an owner-scoped read/write action — a stronger nudge than the null-
67
+ sentinel one, since it means unconstrained reach across every owner, not
68
+ just a narrower gap in an existing constraint. Say so explicitly with
69
+ `dataScope: {}` when a shared/tenant-wide resource is what you intend
60
70
  (warnings — they print but don't fail).
61
71
 
62
72
  `validate` accepts a file path **or** a bundled blueprint name.
@@ -154,18 +164,120 @@ accessProfile:
154
164
  # owners' records, not shared/seed data. Keys are userId + namespaced scopes
155
165
  # (scope:org, scope:client, ...):
156
166
  # dataScope: { "scope:org": [<org-uuid>, null] }
167
+ # Omitting dataScope ENTIRELY is different from an empty dataScope: {} —
168
+ # both mean "every owner", but only {} says so on purpose. `validate` warns
169
+ # on the omitted-key form so an author who meant "my own org" and forgot
170
+ # the key notices before minting; write dataScope: {} to say the reach is
171
+ # deliberate.
172
+ # capabilities: [member-lifecycle]
173
+ # A SEPARATE gated dimension from allowedActions — named platform effects a
174
+ # resource:ops verb can't express (e.g. creating/removing an identity in
175
+ # your tenant). Only 'member-lifecycle' is admitted today; every other name
176
+ # is hard-rejected, same posture as the scope gate. See "Capabilities" below.
177
+
178
+ roles:
179
+ # Optional. Reusable, identity-agnostic access rules — DISTINCT from
180
+ # accessProfile (which scopes only the blueprint's own service-principal
181
+ # key). A role is provisioned but bound to no one by `bootstrap`; bind it
182
+ # afterward with `vectros access grant --principal <p> --context <c> --role
183
+ # <roleId>`, or let a human self-enroll with `vectros join <context> --role
184
+ # <roleId>`. Each roleId maps to an ORDERED LIST of clauses — a record is
185
+ # accessible when ANY clause grants it, and within one clause every
186
+ # dataScope dimension must match (the same allowedActions/dataScope/
187
+ # capabilities shape as accessProfile, three-part, evaluated per-clause):
188
+ editor:
189
+ - allowedActions: [records:cru, records:d, search:r, schemas:r]
190
+ dataScope: {} # explicit marker for tenant-wide (unscoped) reach
191
+ member:
192
+ - allowedActions: [records:r, search:r]
193
+ dataScope: { userId: ['${{ self.userId }}'] } # self-scoped: only your own records
157
194
 
158
195
  servicePrincipal:
159
196
  externalId: my-thing-sp # stable; the principal the profile + key bind to
160
197
  displayName: My Thing
161
198
 
162
199
  seed:
163
- - typeName: thing
200
+ - surface: record # 'record' | 'document' — the seed's own discriminator
201
+ typeName: thing
164
202
  externalId: seed-welcome # stable → idempotent; re-apply converges
165
203
  fields: { title: "Hello" }
166
204
  # Seeds are illustrative sample data. They're applied by default; provision a
167
205
  # production context with `bootstrap --no-seed` to skip them (schemas + key
168
206
  # still apply).
207
+
208
+ issuers:
209
+ - issuerId: auth0-prod # your slug; 3-31 chars, lowercase letter first
210
+ issuer: "https://your-tenant.us.auth0.com/"
211
+ jwksUri: "https://your-tenant.us.auth0.com/.well-known/jwks.json"
212
+ audience: "https://api.your-app.example.com" # globally unique per (issuer, audience)
213
+ contextId: mcp # MUST equal this blueprint's own contextId (above)
214
+ # Optional trusted third-party IdP issuers (BYO-IdP token exchange). Applied
215
+ # in the BOOTSTRAP-token phase, alongside app-context/service-principal
216
+ # creation — NOT the same phase as schemas/accessProfile/roles above (see
217
+ # BLUEPRINT_FIELD_PHASES, exported by @vectros-ai/blueprints). Registration
218
+ # is create-once: re-applying an already-registered issuerId leaves it
219
+ # unchanged — there's no update, so an edited jwksUri/audience on a live
220
+ # issuer won't take effect via re-apply.
221
+ #
222
+ # Each entry's contextId must equal the blueprint's own contextId. An issuer
223
+ # is a trust anchor — whoever controls its jwksUri can mint identities your
224
+ # tenant accepts — so a blueprint may only attach one to the context it
225
+ # actually provisions. One IdP account serving several contexts needs one
226
+ # entry per context anyway, because (issuer, audience) must be unique; put
227
+ # each in that context's own blueprint.
228
+
229
+ namespaces:
230
+ - namespace: team # 2-32 chars, lowercase letter first; reserved names
231
+ # (user, record, document, entity, self, tenant,
232
+ # context, scope, versions, lookup) are rejected.
233
+ # org/client are NOT in that set -- reserved namespace
234
+ # names, not built-ins, registered like any other. They
235
+ # already exist tenant-wide at rank 1000/2000 (below) --
236
+ # pick a different specificityRank, and note a
237
+ # context-owned registration shadows the tenant-wide
238
+ # one for this context's own callers.
239
+ specificityRank: 500 # 0..1_000_000, unique among this blueprint's namespaces
240
+ entityBacked: true # optional
241
+ membershipRecordType: thing # MUST be a typeName this blueprint declares in
242
+ # schemas: above — membership can only resolve
243
+ # over a record type the blueprint itself ships,
244
+ # never one that merely already exists in the
245
+ # target context. membershipTargetField required
246
+ # alongside it.
247
+ membershipTargetField: userId
248
+ membershipLevelField: level # optional; membershipLevels required alongside it
249
+ membershipLevels: [admin, viewer]
250
+ # Registered in the SAME bootstrap-token phase as issuers, before schemas.
251
+ # Always owned by this blueprint's own contextId (above) — there is no
252
+ # tenant-wide option, since the platform confines namespace registration to
253
+ # the credential's own context unconditionally, even for a tenant-wide
254
+ # request. Registration is create-once, NOT idempotent server-side: a
255
+ # re-apply matching every declared field converges silently, but one that
256
+ # disagrees with what's already registered fails the apply rather than
257
+ # silently overwriting it.
258
+
259
+ identities:
260
+ team: # local name — referenced elsewhere as ${{ identities.team }}
261
+ kind: org # 'user', or an entity namespace (org/client/one you registered)
262
+ externalId: acme-team # your stable id; resolution is idempotent by this value
263
+ displayName: Acme Team # optional (users: folded into payload.displayName instead —
264
+ # a user has no first-class `name` field)
265
+ metadata: { plan: pro } # optional JSON object, merged into the principal's payload
266
+ # A declared name must match letters/digits/underscore, not starting with a
267
+ # digit (demoOrg, not demo-org) — the same grammar '${{ identities.<name> }}'
268
+ # itself can match. A name outside it is rejected as a parse error at
269
+ # declare time (validate/plan), not silently accepted. Every declared
270
+ # identity is ensure-exist'd (idempotent by
271
+ # externalId), whether or not anything below references it, in its OWN
272
+ # creds-bearing pass — tenant-wide, under the bootstrap credential, BEFORE
273
+ # either loader phase (not in BLUEPRINT_FIELD_PHASES for that reason).
274
+ # Reference the resolved principal id anywhere one is valid — a schema's
275
+ # userId/scopes, an accessProfile/role dataScope or identityOverrides,
276
+ # seed-record ownership — e.g.:
277
+ # identityOverrides: { "scope:org": "${{ identities.team }}" }
278
+ # Only the WHOLE id can be referenced (no '${{ identities.team.externalId }}'
279
+ # property access), and every reference must name an identity declared here
280
+ # — both checked OFFLINE at validate/plan, never deferred to a live apply.
169
281
  ```
170
282
 
171
283
  YAML or JSON are both accepted (chosen by extension). The bundled library is
@@ -284,13 +396,29 @@ reference Y", add the reference field to `lookupFields` as an equality lookup
284
396
  ### The scope gate — what you can and can't request
285
397
 
286
398
  The gate allows only **data-plane** resources:
287
- `records`, `schemas`, `search`, `documents`, `folders`, `inference`.
399
+ `records`, `schemas`, `search`, `documents`, `folders`, `inference`, `entities`
400
+ (per-namespace identity-entity CRUD — `org`/`client`/any namespace you
401
+ registered — ownership-gated the same way as `records`; it grants no
402
+ authority over the namespace *registry* itself, which stays root-key-only).
288
403
  Operations are `c|r|u|d`, combinable (`records:cru`), with a per-resource
289
404
  wildcard (`records:*`). Anything else — control-plane resources or the bare
290
405
  `*` — is rejected, and there is **intentionally no override flag**. This is
291
406
  *why* it's safe to apply a blueprint you didn't write: the binary is the trust
292
407
  boundary, not the file.
293
408
 
409
+ ### Capabilities (`granted_capabilities`) — a separate, equally-gated dimension
410
+
411
+ An `accessProfile` or role clause may also declare `capabilities: [...]` —
412
+ named platform effects (like creating or removing an identity in your tenant)
413
+ that a `resource:ops` scope token can't express. This rides its own field,
414
+ separate from `allowedActions`, and has its own allowlist: **only
415
+ `member-lifecycle` is admitted today.** Every other capability name —
416
+ including ones the platform's own registry recognizes — is refused, the same
417
+ fail-closed, deliberately-narrow posture as the scope gate above. `validate`,
418
+ `plan`, `blueprint-test` and `bootstrap` all report a rejected capability the
419
+ same way they report a rejected scope: nothing mints, and the CLI exits
420
+ non-zero.
421
+
294
422
  ### The idempotency contract
295
423
 
296
424
  Re-applying a blueprint **converges, never duplicates**, because everything has
@@ -306,6 +434,21 @@ but fails on every re-apply with an "already exists" error instead of the
306
434
  existing record. Request `:r` alongside `:c` on every resource your blueprint
307
435
  creates (the bundled blueprints all do this — see the example above).
308
436
 
437
+ **One qualification to "converges, never duplicates": a NEW key mint bound to
438
+ a pre-existing service principal is not silent.** If `servicePrincipal.externalId`
439
+ resolves to an identity that already existed in the tenant before this run —
440
+ ordinarily true on a routine same-machine re-apply, since the earlier run's
441
+ local key record already names that same principal — `bootstrap` proceeds as
442
+ usual. But the FIRST time a new machine (no local key record for the blueprint's
443
+ `contextId`) applies a blueprint whose service principal already exists, it
444
+ refuses by default and asks for confirmation: interactively, a y/N prompt;
445
+ non-interactively (`--yes`, or no TTY), it requires `--confirm-existing-principal`
446
+ and otherwise exits with an error rather than minting silently. This is
447
+ deliberate — the platform cannot tell an intentional second-machine setup apart
448
+ from a blueprint's `externalId` colliding with an unrelated identity, so it asks
449
+ rather than guessing either way. `--rotate` is exempt only when the local
450
+ record already names the same principal being rotated.
451
+
309
452
  ## Distribution
310
453
 
311
454
  - **Bundled** — reviewed, versioned blueprints shipped in the library
package/CHANGELOG.md CHANGED
@@ -3,6 +3,216 @@
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.16.0
7
+
8
+ ### Added
9
+
10
+ - **A blueprint's `accessProfile` and role clauses can now declare
11
+ `capabilities: [...]`** — the platform's `granted_capabilities` field,
12
+ a grant dimension separate from `allowedActions` for named effects that
13
+ reach across a partition boundary (e.g. creating/removing an identity in
14
+ your tenant) which no `resource:ops` verb can express. Gated the same way
15
+ `allowedActions` is: only `member-lifecycle` is admitted, fail-closed,
16
+ every other name (including ones the platform's own registry recognizes)
17
+ is refused before anything mints. `vectros blueprint validate`/`plan` and
18
+ `bootstrap` all report a capability grant/rejection the same way they
19
+ already report a scope grant/rejection.
20
+
21
+ - **`bootstrap` now flags when a mint binds to a principal that already existed in your
22
+ tenant before this run**, and the outcome summary always reports whether the service
23
+ principal was newly created or reconciled. This surfaces the one collision shape the
24
+ platform itself can't tell apart from an intentional one (e.g. re-applying a blueprint
25
+ from a second machine): a blueprint's declared `servicePrincipal.externalId` matching an
26
+ existing, unrelated identity in your tenant. Interactively, you're prompted to confirm
27
+ before the key mints; non-interactively (`--yes`, or no TTY) the run refuses by default
28
+ and requires the new `--confirm-existing-principal` flag to proceed, so an unexpected
29
+ collision fails a CI run loudly instead of silently minting a credential bound to the
30
+ wrong identity. Never triggered by `--rotate` or an ordinary same-machine re-apply — both
31
+ are exempted as already-informed operator intent.
32
+
33
+ - **The bootstrap scope gate now admits `profiles:c:self`.** The platform's
34
+ `profiles:c`/`u`/`d` accept an optional qualifier confining WHICH principal
35
+ the grant may act on — a literal `usr_<id>`, or the bare `self` sentinel,
36
+ which the platform resolves from the caller's own bound identity, never
37
+ from anything a blueprint declares. A role clause (or the bootstrap
38
+ `accessProfile`) may now grant `profiles:c:self` to let a credential create
39
+ an access profile for itself. A bare, unqualified `profiles:c`, and a
40
+ literal `profiles:c:usr_<id>` naming another principal, still hard-reject —
41
+ only the platform-resolved `self` form is admitted.
42
+
43
+ - **`vectros blueprint validate`/`plan` now warns when a role or the
44
+ `accessProfile` clause has NO `dataScope` key at all on a read/update/delete
45
+ action against `records`/`documents`/`entities`.** The existing null-sentinel
46
+ warning already caught a *narrower* dataScope gap; this catches the more
47
+ dangerous case of no constraint whatsoever, which grants reach across every
48
+ owner in the tenant — not "my own org", whatever the author may have
49
+ intended. Advisory only (`exit 0`): a deliberately shared/tenant-wide
50
+ resource (declared with `dataScope: {}`) is unaffected.
51
+
52
+ - **Top-level `namespaces[]`** registers a blueprint's own namespaces via
53
+ `POST /v1/namespaces`, in the same bootstrap-token phase as `issuers`.
54
+ Every registration is unconditionally owned by the blueprint's own
55
+ context — there is no tenant-wide option, since the platform confines
56
+ namespace registration to the caller's own context unconditionally, even
57
+ for a `contextId: null` request. Registration is not idempotent
58
+ server-side; a re-apply matching the existing declaration converges
59
+ silently, one that disagrees throws rather than being swallowed.
60
+
61
+ ### Fixed
62
+
63
+ - **`key issue`/`list`/`get`/`revoke`/`rotate` can now target the test tenant
64
+ via `--tenant test`**, matching `bootstrap`/`join`/`context`. Previously
65
+ none of the five accepted it, so a key minted into the test tenant (e.g. by
66
+ `bootstrap --tenant test`) could be discovered via `key list --all-contexts`
67
+ (which spans both tenants) but never individually fetched, revoked, rotated,
68
+ or re-issued — there was no way to reach it at all. `--tenant` is refused in
69
+ combination with `--all-contexts`, which already spans both tenants on its
70
+ own.
71
+
72
+ - **`bootstrap` (and `blueprint-test`) now work for a blueprint whose `issuers:`
73
+ target a non-`default` app context** — previously every such blueprint failed
74
+ registration with an authorization error, even though the same blueprint's
75
+ schemas/roles/access profile provisioned fine. The bootstrap credential now
76
+ re-mints itself, pinned to the blueprint's own context, specifically to
77
+ register issuers — closing the gap left once the platform started requiring
78
+ an issuer registration to be confined to the context it targets. No blueprint
79
+ authoring change needed; existing `issuers:` entries behave the same, they
80
+ just no longer fail for any context other than `default`.
81
+
82
+ - **`blueprint-test` now also registers and reads back each declared `namespaces[]` entry**,
83
+ the same assert-readback proof `issuers`/schemas/roles already get. Its teardown is honest
84
+ about what it can and can't undo: a namespace registration is never deleted — namespace
85
+ deletion requires a root API key, which the harness never holds — and is reported as a
86
+ permanent skip rather than a silently-attempted step. Issuer teardown may now report a failed
87
+ step rather than a silent success, too: the platform refuses (`409`) to delete an issuer that
88
+ has ever completed a real token exchange during the run.
89
+
90
+ - **The blueprint format reference showed an `issuers:` entry that no longer validates.**
91
+ `AUTHORING.md`'s "the format, field by field" example declared one `contextId` for the
92
+ blueprint and a different one inside `issuers[0]`. As of `@vectros-ai/blueprints` 0.13.0
93
+ an issuer must target the blueprint's own app context, so copying that example produced a
94
+ blueprint `vectros blueprint validate` rejects. The example now matches, and the reference
95
+ states the constraint where the field is introduced rather than leaving you to discover it
96
+ from a validation error.
97
+
98
+ - **The bootstrap scope gate now admits `profiles:r`/`profiles:u`/`profiles:d`** —
99
+ read is disclosure-only and cannot widen anything; update/delete are bounded by
100
+ a platform-enforced check that a write to an AccessProfile can never grant more
101
+ scope than the requesting credential already holds, so they can only
102
+ redistribute or revoke access, never manufacture new reach. A bare, unqualified
103
+ `profiles:c` (creating a profile for an arbitrary, caller-chosen principal
104
+ name) and every `users:*` verb stay rejected — see the `profiles:c:self`
105
+ entry above for the one qualified exception. This unblocks any blueprint
106
+ role that reads or manages its own app-context's membership — previously
107
+ the gate refused the whole role outright the moment it named any
108
+ `profiles:*` scope.
109
+
110
+ - **`bootstrap --tenant test`, `join --tenant test`, and `key issue|rotate
111
+ --to-keyring` no longer silently activate over a live credential.** Storing
112
+ a freshly-minted (or re-joined) test-tenant key used to make it the
113
+ machine's active identity unconditionally, exactly like a live-tenant run —
114
+ flipping what `vectros keyring show --format raw` (and anything that
115
+ resolves through it with no explicit `--alias`, including the MCP server)
116
+ authenticates as, with no warning. `key issue|rotate --to-keyring` has no
117
+ `--tenant` flag of its own; it now classifies the tenant from the minted
118
+ secret's own prefix. All three commands now activate unless doing so would
119
+ displace a currently-active LIVE credential, in which case the key is
120
+ stored (or the re-join still counts as a successful join) without switching
121
+ to it, and the terminal says so explicitly — naming whether that was the
122
+ safety default or an explicit `--no-activate`, never the wrong reason.
123
+ `bootstrap`/`join` also take `--activate` to force activation over a live
124
+ credential explicitly; `key issue|rotate --to-keyring` has no such flag
125
+ today — run `vectros switch <alias>` afterward if you want that key active.
126
+
127
+ - **`keyring list`'s human-readable table now shows a `tenant` (test/live)
128
+ column and drops `env` (staging/production).** `env` is an internal-team
129
+ concern an external developer's CLI never surfaces; `tenant` is the
130
+ distinction every user actually holds two credentials side by side over, and
131
+ previously the only way to see it was to run `doctor` instead or hope the
132
+ alias happened to carry a `.test` suffix. `--json` output is UNCHANGED —
133
+ still metadata-only, no per-entry secret read, no `tenant` field — because
134
+ resolving every entry's secret from the OS credential store (what showing
135
+ tenant requires) is real per-invocation cost a script relying on `--json`'s
136
+ established speed should not silently start paying.
137
+
138
+ ### Changed
139
+
140
+ - **Internal:** the exact `@vectros-ai/blueprints` dependency pin was updated so a clean
141
+ `npm ci` resolves. No behaviour change.
142
+
143
+ ### Security
144
+
145
+ - **`@napi-rs/keyring` is now pinned to an exact version** (was `^1.3.0`). The
146
+ Linux/Windows fail-closed guarantee behind `keyring remove`/`prune` rests on
147
+ an upstream behavioural contract — a locked credential store must make
148
+ `findCredentials()` throw, not return an empty list — that this package does
149
+ not otherwise verify at install time. A future bump of this dependency should
150
+ be treated as a security review of that contract against the new release, not
151
+ a routine version update (see the comment at its enforcement site,
152
+ `os-keychain.ts`, for exactly what would break).
153
+
154
+ ## 0.15.0
155
+
156
+ ### Added
157
+
158
+ - **`bootstrap` can register trusted third-party IdP issuers via a blueprint's
159
+ new `issuers:` block** (requires `@vectros-ai/blueprints` 0.12.0+). Each entry
160
+ (`issuerId`, `issuer`, `jwksUri`, `audience`, `contextId`, optional `subClaim`/
161
+ `emailClaim`) is applied in the bootstrap-token phase — alongside app-context
162
+ and service-principal creation — using the same owner-only authority that
163
+ creates the app context itself, never an ordinary partner-grantable scope.
164
+ `vectros blueprint plan` names this phase explicitly in the preview.
165
+
166
+ - **`vectros blueprint-test` now verifies a declared `issuers:` entry actually
167
+ registered and is readable back**, the same live-credential proof it already
168
+ gives schemas and roles. One limitation, surfaced explicitly in the run's
169
+ report rather than silently: the platform has no way to remove a registered
170
+ issuer yet, so (unlike every other resource `blueprint-test` provisions)
171
+ issuer registrations are not torn down after the test run.
172
+
173
+ ### Fixed
174
+
175
+ - **`key rotate` no longer silently mints a second key when `--name` defaults
176
+ to `'default'` and the real key you meant to rotate is named something
177
+ else** (e.g. a `bootstrap`-minted key, which is never named `'default'`).
178
+ It now refuses when the default name matches no active key but another
179
+ active key already exists for the same principal/context, naming the
180
+ other key(s) so you can re-run with the right `--name` — or `--force-new`
181
+ to mint a new one alongside them anyway. Previously this minted an
182
+ unrelated key with no error or warning, leaving the original — possibly
183
+ the reason you were rotating — still active.
184
+
185
+ - **`bootstrap --blueprint <file>` now accepts `--set name=value` / `--values
186
+ <file>`** for a file blueprint's declared `inputs:`, matching what `vectros
187
+ blueprint validate`/`plan` already supported. Previously there was no way
188
+ to apply a blueprint with a required, no-default input via `bootstrap` at
189
+ all — the flags simply didn't exist on that command. Supplying either flag
190
+ against a bundled or `--blank` blueprint (which have no `inputs:` to set)
191
+ is now a clear usage error rather than a silent no-op.
192
+
193
+ - **The bootstrap scope gate no longer treats `entities` (identity-entity
194
+ CRUD — `org`/`client`/any namespace you registered) as control-plane.**
195
+ It's ordinary tenant data, ownership-gated the same way as `records` —
196
+ creating or reading an entity grants no additional access by itself. This
197
+ had blocked provisioning any blueprint role that manages its own
198
+ entities (e.g. a user creating their own org). The namespace *registry*
199
+ itself (registering a new namespace type) is unaffected and stays
200
+ root-key-only, correctly rejected by the gate as before.
201
+
202
+ - **The displayed/stored environment (`whoami`, `keyring list`/`doctor`, `blueprint-test`'s
203
+ banner) now reflects the base URL a command actually resolved and sent requests to, not
204
+ a hidden flag default.** The two diverge whenever `VECTROS_API_BASE_URL`/`--base-url`
205
+ alone selects the environment (the common shape when targeting staging without also
206
+ passing `--env staging`) — previously this echoed the wrong environment label next to a
207
+ key or session that was actually minted/used against a different one.
208
+
209
+ ### Changed
210
+
211
+ - **Dependency maintenance** — repinned the bundled `@vectros-ai/sdk` to `0.39.0` (the
212
+ trusted BYO-IdP issuer registry and RFC 8693 token exchange, `exists-by-email`, and the
213
+ scoped-token TTL cap narrowed to 1 hour). The CLI bundles the SDK into its published
214
+ binary, so this is a rebuild against its current version.
215
+
6
216
  ## 0.14.0
7
217
 
8
218
  ### Security
package/README.md CHANGED
@@ -8,6 +8,11 @@ The Vectros provisioning CLI — the `vectros` binary:
8
8
  - **`vectros bootstrap`** — provision a least-privilege MCP credential (+ optional blueprint).
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
+ - **`vectros context`** — create, list, and destroy app contexts.
12
+ - **`vectros identity`** — manage tenant-wide principals (users, orgs, clients, and namespaces you register).
13
+ - **`vectros role`** — define reusable, context-scoped access rules.
14
+ - **`vectros access`** — bind principals to contexts, and inspect what a binding grants.
15
+ - **`vectros key`** — issue, list, revoke, and rotate scoped API keys (`ssk_*`).
11
16
  - **`vectros keyring`** — hold several identities on one machine, pick which is active, and let other tools resolve keys from one place.
12
17
 
13
18
  ```bash
@@ -58,6 +63,11 @@ npx -y @vectros-ai/cli bootstrap --help
58
63
  ```
59
64
 
60
65
  Re-running is idempotent; `--rotate` revokes + re-mints just this machine's key.
66
+ On a machine with no local record of a prior mint (e.g. an ephemeral CI
67
+ container re-running `--yes` for the first time) against a blueprint whose
68
+ service principal already exists in the tenant, `--yes` alone isn't enough —
69
+ add `--confirm-existing-principal` to make that expected case explicit and
70
+ non-interactive; see AUTHORING.md's "idempotency contract" for why.
61
71
 
62
72
  A bundled blueprint ships with **sample seed records** so the credential works
63
73
  the moment it's minted. For a **production** context that's synthetic clutter:
@@ -110,6 +120,163 @@ Exit `0` = applied and every assertion passed; `1` = an assertion failed or appl
110
120
  errored; `2` = no credential, or the blueprint requested control-plane scope.
111
121
  The app-context is intentionally **not** deleted (it's idempotent and reused).
112
122
 
123
+ ## `vectros context` — app contexts
124
+
125
+ A context is the namespace all data (records, documents, schemas, roles, access
126
+ profiles) lives in — the unit `bootstrap`/`blueprint-test` provision into.
127
+ `vectros context` manages contexts directly, outside of a blueprint.
128
+
129
+ ```bash
130
+ vectros context create my-app --name "My App" # idempotent — re-running converges
131
+ vectros context list # every context in the tenant
132
+ vectros context list --mine # only contexts you hold an access profile in
133
+ vectros context get my-app
134
+ vectros context destroy my-app # irreversible — asks you to re-type the id
135
+ ```
136
+
137
+ | Subcommand | Flags | Notes |
138
+ |---|---|---|
139
+ | `create <contextId>` | `--name <name>` (defaults to the id), `--tenant test\|live` | Idempotent by `contextId`; re-running converges. |
140
+ | `list` | `--mine`, `--tenant test\|live` | Default lists every context in the tenant; `--mine` narrows to contexts you hold an active access profile in. |
141
+ | `get <contextId>` | `--tenant test\|live` | |
142
+ | `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. |
143
+
144
+ All four accept `--json` for machine-readable output, plus the common auth
145
+ flags (`--base-url`, `--token`).
146
+
147
+ ## `vectros identity` — tenant-wide principals
148
+
149
+ Manage the principals a context's access profiles and roles reference: users
150
+ (the one fixed principal surface, `--type user`) and namespaced identity
151
+ entities (`org`, `client`, or any namespace you registered, via `--type
152
+ <namespace>`). Identities are **tenant-wide and orthogonal to context** — a
153
+ blueprint or role *references* one by id, it never creates one.
154
+
155
+ ```bash
156
+ vectros identity create --type user --external-id alice@example.com --email alice@example.com
157
+ vectros identity create --type org --external-id acme-inc --name "Acme Inc"
158
+ vectros identity list --type user
159
+ vectros identity get --type org --id <vectrosId>
160
+ vectros identity delete --type org --id <vectrosId>
161
+ ```
162
+
163
+ | Subcommand | Flags | Notes |
164
+ |---|---|---|
165
+ | `create` | `--type <user\|namespace>` (required), `--external-id <id>` (required — your stable idempotency key) | Idempotent, upsert-by-`externalId`. |
166
+ | | `--type user` only: `--email <email>`, `--service` (mint a SERVICE principal; default HUMAN) | `--name`/`--scope` are rejected for `--type user`. |
167
+ | | entity namespaces only: `--name <name>`, `--scope <ns:value>` (repeatable, up to 2 parent ownership edges) | `--email`/`--service` are rejected for an entity namespace. |
168
+ | | both: `--metadata <json>` | Free-form JSON object. |
169
+ | `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. |
170
+ | `get` | `--type <user\|namespace>` (required), `--id <vectrosId>` (required) | `--id` is the Vectros-assigned id from `create`/`list`, not the `--external-id`. |
171
+ | `delete` | `--type <user\|namespace>` (required), `--id <vectrosId>` (required) | |
172
+
173
+ `create`/`list`/`get` accept `--json`; `delete` does not.
174
+
175
+ ## `vectros role` — reusable, context-scoped access rules
176
+
177
+ A Role is a reusable, identity-agnostic scope rule bound to a principal later
178
+ via `vectros access grant --role <roleId>` — the same rule can be shared
179
+ across many principals. `role create` builds a **single-clause** role from a
180
+ flat `--actions` list; author a multi-clause role (several rules unioned
181
+ together) in a blueprint's `roles:` block instead.
182
+
183
+ ```bash
184
+ vectros role create --context my-app --role-id editor --name Editor \
185
+ --actions records:cru,search:r
186
+ vectros role create --context my-app --role-id own-org --name "Own org" \
187
+ --actions records:r --data-scope '{"scope:org":["<orgId>", null]}'
188
+ vectros role list --context my-app
189
+ vectros role get --context my-app --role-id editor
190
+ vectros role delete --context my-app --role-id editor
191
+ ```
192
+
193
+ | Subcommand | Flags | Notes |
194
+ |---|---|---|
195
+ | `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). |
196
+ | `list` | `--context <c>` (required) | |
197
+ | `get` | `--context <c>`, `--role-id <id>` (both required) | |
198
+ | `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. |
199
+
200
+ `create`/`list`/`get` accept `--json`; `delete` does not.
201
+
202
+ ## `vectros access` — bind principals to contexts
203
+
204
+ The binding layer between an identity and a context: `access grant` joins a
205
+ principal to a context via a Role reference (`--role`) **or** inline actions
206
+ (`--actions`) — never both. A later `vectros key issue` mints a credential
207
+ that authenticates *as* the bound principal and can never exceed this
208
+ binding.
209
+
210
+ ```bash
211
+ vectros access grant --principal me --context my-app --role editor
212
+ vectros access grant --principal usr_<id> --context my-app --actions records:r,search:r \
213
+ --scope org:<orgId>
214
+ vectros access list --context my-app # who's in this context
215
+ vectros access list --principal me # which contexts I'm bound to
216
+ vectros access get --principal me --context my-app
217
+ vectros access explain --principal me --context my-app
218
+ vectros access revoke --principal usr_<id> --context my-app
219
+ ```
220
+
221
+ `--principal` accepts `usr_<userId>`, `key_<keyId>`, or the alias `me` (your
222
+ own signed-in user, resolved from your login) — every `access` subcommand
223
+ shares this grammar.
224
+
225
+ | Subcommand | Flags | Notes |
226
+ |---|---|---|
227
+ | `grant` | `--principal <p>`, `--context <c>` (required); exactly one of `--role <roleId>` / `--actions <csv>`; `--scope <ns:value>` (repeatable), `--identity-overrides <json>` | Idempotent **upsert** — re-granting an existing binding updates it to the newly-declared shape (role/actions/overrides), it does not no-op. `--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. |
228
+ | `revoke` | `--principal <p>`, `--context <c>` (required) | |
229
+ | `list` | Exactly one of `--context <c>` / `--principal <p>` | `--context` lists the context's members; `--principal` lists every context that principal is bound to. |
230
+ | `get` | `--principal <p>`, `--context <c>` (required) | |
231
+ | `explain` | `--principal <p>`, `--context <c>` (required) | Resolves the binding's clauses (following its role, if bound via one) 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). |
232
+
233
+ `grant`/`list`/`get`/`explain` accept `--json`; `revoke` does not.
234
+
235
+ ## `vectros key` — the scoped API key (`ssk_*`) lifecycle
236
+
237
+ Mint, inspect, revoke, and rotate the scoped keys (`ssk_*`) that authenticate
238
+ as a principal already bound to a context (`vectros access grant` first — a
239
+ key can never exceed its profile). The raw secret is disclosed **exactly
240
+ once**, on `issue`/`rotate`; if you lose it, `key rotate` mints a fresh one.
241
+
242
+ ```bash
243
+ vectros key issue --principal usr_<id> --context my-app # print the secret (default)
244
+ vectros key issue --principal usr_<id> --context my-app --to-keyring # store it instead
245
+ vectros key list # keys in the default context
246
+ vectros key list --all-contexts # every key, every context (owner-only)
247
+ vectros key get <keyId>
248
+ vectros key revoke <keyId>
249
+ vectros key rotate --principal usr_<id> --context my-app # revoke + re-mint the same tuple
250
+ ```
251
+
252
+ `--principal` here is `usr_<userId>` **or a bare user id** (the leading `usr_`
253
+ is optional and stripped if present) — narrower than `access`'s grammar: there
254
+ is no `me` alias on any `key` subcommand, and a key never authenticates as a
255
+ `key_*` principal. Look your own id up first with `vectros identity list
256
+ --type user` (or `vectros whoami`), or grant/issue via `vectros join`, which
257
+ does resolve `--as me`.
258
+
259
+ A minted secret has exactly two destinations, and the flags for choosing one
260
+ are shared by `issue` and `rotate`:
261
+
262
+ | Flag | Meaning |
263
+ |---|---|
264
+ | `--format human\|raw\|env\|json` (default `human`) | Print the secret — `raw` = bare secret only, `env` = a `VECTROS_API_KEY=...` line, `json` = the full key object. |
265
+ | `--to-keyring` | Store the secret in this machine's keyring instead of printing it (the desktop/MCP path). Mutually exclusive with an explicit `--format raw\|env\|json`. |
266
+ | `--alias <name>` | Keyring entry name for `--to-keyring` (defaults to the context id). |
267
+ | `--no-activate` | With `--to-keyring`, store the entry without making it the active identity. |
268
+
269
+ | Subcommand | Flags | Notes |
270
+ |---|---|---|
271
+ | `issue` | `--principal <p>`, `--context <c>` (required); `--name <n>` (default `default`), `--label <l>`, `--tenant test\|live`, destination flags above | Idempotent on `(tenant, context, principal, name)` — re-issuing an existing tuple returns the key **without** the secret (never re-disclosed); the CLI detects that and points you at `rotate`. |
272
+ | `list` | `--principal <p>`, `--context <c>`, `--tenant test\|live` **or** `--all-contexts` (owner-only; spans both tenants, not combinable with `--tenant`) | **The default lists only ONE context's keys** (the token's context — `default`, or `--context`), not your whole account — easy to miss since the active-key limit is tenant-wide across all contexts. `--all-contexts` is the true account-wide view. |
273
+ | `get <keyId>` | `--context <c>`, `--tenant test\|live` | `--context` pins the verb token to the key's context — omit it for a key bound to a non-default context (e.g. one `bootstrap` minted) and the lookup 404s. |
274
+ | `revoke <keyId>` | `--context <c>`, `--tenant test\|live` | Soft-delete; ~5 minutes to take effect (authorizer cache). Same `--context` pinning caveat as `get`. |
275
+ | `rotate` | `--principal <p>`, `--context <c>` (required); `--name <n>`, `--force-new`, `--tenant test\|live`, destination flags above | Revokes the matching active key (by the exact `(context, principal, name)` tuple), then mints a fresh one — there is no dedicated rotate endpoint. **Default-name guard:** if no key is currently active under `--name` (default `default`) but the principal/context has *other* active keys, `rotate` refuses rather than silently minting an unrelated second key — pass the right `--name` or `--force-new` to proceed anyway. |
276
+
277
+ `issue`/`rotate` have no `--json` (use `--format json` instead); `list`/`get`
278
+ accept `--json`, `revoke` does not.
279
+
113
280
  ## `vectros keyring` — one home for your keys
114
281
 
115
282
  A machine can hold several Vectros identities (one per context or principal).
@@ -188,9 +355,35 @@ Blueprints are **untrusted input** (agent- or community-authored) and the
188
355
  CLI runs under an elevated ephemeral bridge token. So the trust boundary is
189
356
  **this binary, not the blueprint**: a hardcoded **scope gate** bounds every
190
357
  mint to a data-plane allowlist (`records` / `schemas` / `search` / `documents`
191
- / `folders` / `inference`). Any control-plane scope (keys / profiles / users /
192
- billing / admin) or `*` wildcard **hard-rejects** — the CLI mints nothing and
193
- exits non-zero. There is intentionally **no override flag**.
358
+ / `folders` / `inference` / `entities`). Most control-plane scope (keys /
359
+ app-contexts / users / billing / admin / namespaces) or a `*` wildcard
360
+ **hard-rejects** — the CLI mints nothing and exits non-zero. There is
361
+ intentionally **no override flag**.
362
+
363
+ **One narrow, deliberate exception:** `profiles` (which configures who has
364
+ access to what — control-plane by nature) admits its `r`/`u`/`d` operations,
365
+ because the platform enforces an unconditional check that a write to an
366
+ AccessProfile can never grant more scope than the requesting credential
367
+ already holds — those verbs can only disclose or redistribute access, never
368
+ manufacture new reach. `profiles:c` (creating a profile for an arbitrary,
369
+ caller-chosen principal name) still hard-rejects — **except** `profiles:c:self`,
370
+ which the platform resolves from the credential's own identity at request
371
+ time, never from anything the blueprint declares; a bare `profiles:c` or a
372
+ literal `profiles:c:usr_<id>` naming another principal still hard-rejects,
373
+ same as every other control-plane scope.
374
+
375
+ ### Capabilities (`granted_capabilities`) — a separate, equally-gated dimension
376
+
377
+ A blueprint's `accessProfile` and role clauses may also declare
378
+ `capabilities: [...]` — named platform effects (like creating or removing an
379
+ identity in your tenant) that reach across a partition boundary in a way no
380
+ `resource:ops` verb can express. This rides a field entirely separate from
381
+ `allowedActions`, and it has its own allowlist: **only `member-lifecycle` is
382
+ admitted today.** Every other capability name — including ones the platform's
383
+ own registry recognizes — is refused, the same fail-closed, deliberately-widen
384
+ posture as the scope gate above. `vectros blueprint validate`/`plan` and
385
+ `bootstrap` all report a rejected capability the same way they report a
386
+ rejected scope: nothing mints, and the CLI exits non-zero.
194
387
 
195
388
  ## Rate limits
196
389