@vectros-ai/cli 0.16.0 → 0.20.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
@@ -69,6 +69,16 @@ A name must be 2–50 chars: a lowercase letter, then lowercase letters / digits
69
69
  `dataScope: {}` when a shared/tenant-wide resource is what you intend
70
70
  (warnings — they print but don't fail).
71
71
 
72
+ A schema may also declare `expectedScopeDims` — the ownership dimensions
73
+ its records are typically scoped by, e.g. `expectedScopeDims: [org,
74
+ client]`. Purely advisory (never sent to the platform, never runtime-
75
+ enforced): it lets the lint warn when a role/`accessProfile` clause grants
76
+ `r`/`u`/`d` on that typeName (via a type-qualified action like
77
+ `records:r:case`) but its `dataScope` names only SOME of the declared
78
+ dims — reads never require full-dimension coverage the way creates do, so
79
+ a clause naming one of two declared dims silently leaves the other
80
+ unconstrained. Omit the field entirely and this lint never fires.
81
+
72
82
  `validate` accepts a file path **or** a bundled blueprint name.
73
83
 
74
84
  ### 3. `plan` — preview before you mint
@@ -87,6 +97,30 @@ A name must be 2–50 chars: a lowercase letter, then lowercase letters / digits
87
97
  + seed 1 record(s)
88
98
  ```
89
99
 
100
+ **A blueprint whose `accessProfile` composes roles (`roleIds:`) previews every
101
+ composed role's clauses individually**, not flattened, because two clauses
102
+ sharing an action set can still carry different `dataScope`:
103
+
104
+ ```
105
+ Plan for blueprint 'my-thing' (v1.0.0) → context 'mcp':
106
+ + schema 'thing' (2 fields, HYBRID) lookup: [externalId]
107
+ + context 'mcp' (MCP — my-thing)
108
+ + principal 'my-thing-sp' (SERVICE)
109
+ + role 'case-handler' (1 clause, actions [records:cru])
110
+ clause 1: actions [records:cru], dataScope: scope:org=[${{ self.scope.org }}]
111
+ + role 'hr-admin' (1 clause, actions [records:r])
112
+ clause 1: actions [records:r], dataScope: (none — unconstrained)
113
+ assumable: scope:org=[org_engineering, org_sales]
114
+ + profile via role(s) [case-handler, hr-admin] (composed — each role's own clauses, concatenated in order)
115
+ + key my-thing-<machine> (mint 1 scoped ssk_*)
116
+ + seed 1 record(s)
117
+ ```
118
+
119
+ A role's `assumable` line only appears when the blueprint's top-level
120
+ `roleAssumable:` map names that role — it's shown because it's
121
+ security-relevant (what a holder of the role may become via `POST
122
+ /v1/auth/token/assume`), not just that the role was declared.
123
+
90
124
  `bootstrap` shows the same plan and asks for confirmation before minting
91
125
  (`--yes` skips the prompt for CI/agents). ⚠️ **`--no-merge` is NOT a dry run** — it still
92
126
  mints/reconciles for real; it only changes where the resulting credential goes, printing the MCP
@@ -172,8 +206,19 @@ accessProfile:
172
206
  # capabilities: [member-lifecycle]
173
207
  # A SEPARATE gated dimension from allowedActions — named platform effects a
174
208
  # 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.
209
+ # your tenant). 'member-lifecycle' and 'delegate-principal-stamp' are
210
+ # admitted today; every other name is hard-rejected, same posture as the
211
+ # scope gate. See "Capabilities" below.
212
+ #
213
+ # Instead of allowedActions (+ optional dataScope/capabilities), accessProfile
214
+ # may declare `roleIds: [<roleId>, ...]` — one or more roles declared below,
215
+ # in `roles:`, composed additively (each role's own clauses, concatenated in
216
+ # the order listed; a single roleId is equivalent to the platform's
217
+ # deprecated singular roleId reference). Mutually exclusive with allowedActions; a roleIds-
218
+ # composed profile has no inline clause of its own, so dataScope/capabilities
219
+ # are rejected alongside it — author those on the referenced role instead:
220
+ # accessProfile:
221
+ # roleIds: [case-handler, hr-admin]
177
222
 
178
223
  roles:
179
224
  # Optional. Reusable, identity-agnostic access rules — DISTINCT from
@@ -247,14 +292,20 @@ namespaces:
247
292
  membershipTargetField: userId
248
293
  membershipLevelField: level # optional; membershipLevels required alongside it
249
294
  membershipLevels: [admin, viewer]
295
+ # tenantWide: true # optional, default false. Requests registration visible to
296
+ # # EVERY context in the account, not just this blueprint's own —
297
+ # # see below.
250
298
  # 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.
299
+ # Owned by this blueprint's own contextId (above) by default. `tenantWide: true` opts
300
+ # out of that, but declaring it is not enough on its own: the applying credential must
301
+ # separately hold OWNER-only authority, and `bootstrap`/`apply` refuse to even request
302
+ # it without the explicit `--allow-tenant-wide-namespaces` flag — a blueprint cannot make
303
+ # this happen by itself. Registration is create-once, NOT idempotent server-side: a
304
+ # re-apply matching every declared field converges silently, but one that disagrees
305
+ # with what's already registered fails the apply rather than silently overwriting it —
306
+ # for a tenant-wide namespace this includes a collision with a DIFFERENT blueprint's
307
+ # (or a manual) registration of the same name, which always fails rather than being
308
+ # silently adopted.
258
309
 
259
310
  identities:
260
311
  team: # local name — referenced elsewhere as ${{ identities.team }}
@@ -267,10 +318,15 @@ identities:
267
318
  # digit (demoOrg, not demo-org) — the same grammar '${{ identities.<name> }}'
268
319
  # itself can match. A name outside it is rejected as a parse error at
269
320
  # 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).
321
+ # identity is ensure-exist'd (idempotent by externalId), whether or not
322
+ # anything below references it, in its OWN creds-bearing pass — under the
323
+ # bootstrap credential, AFTER the app-context, service principal, and any
324
+ # namespaces: above are registered, but BEFORE schemas/accessProfile/roles
325
+ # (not in BLUEPRINT_FIELD_PHASES for that reason). This ordering matters
326
+ # for a non-'user' kind: an 'org'/'client'/registered-namespace identity is
327
+ # created IN that namespace, so it can only succeed once the namespace
328
+ # exists — declare it in namespaces: above (or ensure it's already
329
+ # registered) before referencing it here with a non-'user' kind.
274
330
  # Reference the resolved principal id anywhere one is valid — a schema's
275
331
  # userId/scopes, an accessProfile/role dataScope or identityOverrides,
276
332
  # seed-record ownership — e.g.:
@@ -411,10 +467,10 @@ boundary, not the file.
411
467
  An `accessProfile` or role clause may also declare `capabilities: [...]` —
412
468
  named platform effects (like creating or removing an identity in your tenant)
413
469
  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`,
470
+ separate from `allowedActions`, and has its own allowlist: **`member-lifecycle`
471
+ and `delegate-principal-stamp` are admitted today.** Every other capability
472
+ name — including ones the platform's own registry recognizes — is refused,
473
+ the same fail-closed, deliberately-narrow posture as the scope gate above. `validate`,
418
474
  `plan`, `blueprint-test` and `bootstrap` all report a rejected capability the
419
475
  same way they report a rejected scope: nothing mints, and the CLI exits
420
476
  non-zero.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,338 @@
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.20.0 — 2026-08-27
7
+
8
+ ### Added
9
+
10
+ - **The capability gate now admits `delegate-principal-stamp`**, alongside the
11
+ existing `member-lifecycle`. Grantable on an `accessProfile` or role clause's
12
+ `capabilities: [...]`, same as before — a blueprint can now let an admin
13
+ role create a membership-shaped record on behalf of a known third-party
14
+ user, once the clause authorizing the write also explicitly names `userId`
15
+ in its own `dataScope` (the capability itself may be granted on a
16
+ different clause of the same role, e.g. via a separately-composed
17
+ `roleIds` entry — only the write's own verb + `userId` admission must be
18
+ co-located). Bounded by the SAME app context and record type the clause's
19
+ `dataScope` already confines the write to — unlike `delegate-mint`/
20
+ `forensic-read`/`context-directory-read`, this is not tenant-wide, and
21
+ (like `member-lifecycle`) it is self-signup-eligible.
22
+ - **`vectros identity list-all`** — lists every user in the tenant, for the one
23
+ recovery case `identity list` can't reach: a user created but never granted
24
+ access anywhere (the gap between creating a principal and binding it to a
25
+ context). `identity list` is scoped to the caller's own context by design —
26
+ a user with no access grant at all is invisible there no matter how many
27
+ times a create is retried, since a retry with the same `--external-id` just
28
+ collides with the same invisible row. `list-all` authenticates with your own
29
+ login session (an account owner) instead of a context-scoped credential, so
30
+ it can see every user regardless of context. Supports `--external-id`,
31
+ `--tenant`, `--limit`, and `--json`, matching `identity list`'s conventions.
32
+ - **`vectros blueprint validate`/`plan`: new advisory lint —
33
+ `partial-datascope-dimension-coverage`** (needs `@vectros-ai/blueprints` ≥
34
+ 0.17.0). Warns when a role/`accessProfile` clause grants `r`/`u`/`d` on a
35
+ typeName (e.g. `records:r:case`) whose schema declares
36
+ `expectedScopeDims`, but the clause's `dataScope` names only SOME of those
37
+ dims — the missing dimension(s) are silently unconstrained for that grant,
38
+ since reads (unlike creates) never require full-dimension `dataScope`
39
+ coverage. Opt-in: a schema with no `expectedScopeDims` declared never
40
+ trips it, and it never fires on an untyped (bare `records:r`) grant.
41
+ Never runtime-enforced — pure authoring-time visibility into a choice
42
+ that's easy to make by omission rather than intent.
43
+ - **`bootstrap`/`blueprint-test` now catch a FRESH (first-apply) seed index
44
+ failure, not just a re-apply's.** A seed create's response always carries
45
+ `PENDING_INDEX` on a first apply (indexing runs async, after the
46
+ response), so the existing `seedIndexFailures` summary previously only
47
+ ever saw a terminal `FAILED` on a later idempotent re-apply — a
48
+ newly-bootstrapped demo whose seeds failed to index looked silently empty
49
+ in search/RAG until the operator happened to re-run bootstrap. Every seed
50
+ still `PENDING_INDEX` after seeding is now re-checked with a bounded,
51
+ concurrent, best-effort poll (default: 3 attempts, 500ms apart, ≤ ~1.5s
52
+ added to the happy path) and folded into the same run-summary warning if
53
+ it settles to `FAILED` within the bound. Deliberately NOT a guarantee — a
54
+ seed still `PENDING_INDEX` when the bound is exhausted stays silent, so a
55
+ genuinely slow index (reconcile/quarantine can stall for minutes) never
56
+ turns a one-shot provisioning command into an unbounded wait.
57
+ - **`vectros issuers` gains `register`/`update`/`delete`** — previously
58
+ read-only (`list`/`get`). Closes the gap where adding a second IdP, or
59
+ editing one issuer's `selfSignupPolicies` after initial setup, required
60
+ hand-authoring a blueprint `issuers:` block and re-running a full apply.
61
+ `register` is idempotent by issuer id (a repeat returns the existing
62
+ registration unchanged, not an error); `update` only ever sends the safe,
63
+ mutable fields (`subClaim`/`emailClaim`/`userinfoUri`/`status`/
64
+ `selfSignupPolicies`) — trust-anchor fields stay immutable via this route,
65
+ matching the platform; `delete` uses the same re-type-the-id confirmation
66
+ as `context destroy` (`--force` for non-interactive use), and is refused
67
+ server-side while any user is bound through the issuer. All three accept
68
+ `--tenant test|live` (like their `list`/`get` siblings) so a context
69
+ living in the `test` tenant is reachable, not just `live`.
70
+ - **New `vectros namespace` command group: `register`/`list`/`get`.**
71
+ Registering a brand-new scope namespace (e.g. a `practice`/`team` entity
72
+ dimension) was blueprint-apply-only before this; there was also no way to
73
+ discover which namespaces already exist without already knowing their
74
+ names. `register --entity-backed` is what makes a namespace a real
75
+ identity-entity dimension that `identity create --type <name>` can create
76
+ entities in. No `update`/`delete` verbs — both are gated to a genuine root
77
+ API key on the platform side with no provisioning-capability door, so
78
+ neither could ever succeed for any credential the CLI holds (documented in
79
+ the command's own module header).
80
+ - **`role create` gains `--assumable <json>`** — a role-level `vectros
81
+ assume` entitlement grant (JSON map of `scope:<namespace>` → allowed
82
+ values), previously blueprint-`roleAssumable:`-only. Independent of
83
+ `--data-scope`: holding broad data-scope reach in a namespace does not by
84
+ itself grant assuming any value in it.
85
+ - **New `vectros assume --context <c> --scope <ns:value>...`** — re-mints
86
+ the caller's own verb token with one or more `identity.<namespace>` values
87
+ switched (`POST /v1/auth/token/assume`), for a caller whose role
88
+ explicitly grants assuming those values. The CLI's first standalone
89
+ command reaching this endpoint; entitlement is checked live against the
90
+ caller's current roles, never a copy frozen into a token at mint time. No
91
+ `--to-keyring` — the response is an ephemeral bearer token with the same
92
+ remaining lifetime as the verb token this command itself mints to make the
93
+ call, not a durable secret.
94
+
95
+ ### Changed
96
+
97
+ - **`runLoader`'s `provisioned` is now REQUIRED — the self-provisioning
98
+ fallback is gone.** Internal hardening only, no CLI-facing behavior
99
+ change: every real caller (`vectros bootstrap`, `blueprint-test`) already
100
+ always ran `provisionContext` first and passed the result in, so the
101
+ fallback branch was dead in production. Removing it makes the
102
+ `provisionContext` → (apply-time identity resolution) → `runLoader`
103
+ ordering structurally required instead of convention-enforced — a future
104
+ caller can no longer reproduce the ordering bug the original fix
105
+ closed by skipping the split. `allowTenantWideNamespaces` and
106
+ `confirmExistingPrincipal` moved with it: both now live only on
107
+ `provisionContext`'s own input, where their actual checks have run since
108
+ that original fix — `runLoader`'s own copies of both parameters were already dead
109
+ plumbing before this change.
110
+ - **Apply-time identity resolution now REQUIRES `provisionContext`'s result.**
111
+ Internal hardening only, no CLI-facing behavior change. Closes the other
112
+ half of the ordering gap above: `runLoader` already required
113
+ `provisionContext`'s output, but the step between them — resolving a
114
+ blueprint's declared `identities:` — took a bare client with nothing tying
115
+ it to `provisionContext` having run first, so a caller could still
116
+ reproduce the original ordering bug (a non-`user` identity in a namespace
117
+ the same blueprint also registers racing that registration) with no
118
+ compiler signal. It now derives which client to use internally instead of
119
+ each caller doing so independently.
120
+
121
+ ### Fixed
122
+
123
+ - **`bootstrap`'s post-merge hint is now blueprint-specific, and `--client
124
+ code` always shows the `claude mcp add` command.** The restart hint used to
125
+ read "list my tasks" / "add a task to …" for EVERY blueprint (a leftover
126
+ from when only `task-management` existed) — it now names the actual
127
+ blueprint applied. The `claude mcp add` line (the idiomatic Claude Code
128
+ install path) used to print only on `--no-merge` / a declined / a failed
129
+ merge — a successful auto-merge silently hid it; it's now shown there too.
130
+ - Scrubbed internal issue/MR references, review-process citations, and
131
+ backend class names out of `packages/cli`'s comments — visible in the
132
+ published npm package's sourcemaps to anyone who reads mapped source.
133
+ Comment-only; no behavior change.
134
+ - **Keyring secret-store hardening.** A 0-byte salt file removed by
135
+ something else mid-poll (a concurrent cleanup, a manual delete of the
136
+ crash remnant) used to throw a raw `ENOENT` out of `setSecret`; it now
137
+ restarts salt creation instead, since nothing was ever encrypted under an
138
+ empty salt in the first place. Also fixed an order-dependent assumption
139
+ in two `keyring prune` tests (they now pin the processing order they
140
+ depend on, so a future ordering change fails with a diagnosable message
141
+ instead of a confusing downstream one).
142
+
143
+ ### Changed
144
+
145
+ - Bundled `@vectros-ai/sdk` refreshed to the current release's staging build.
146
+
147
+ ## 0.19.0 — 2026-08-27
148
+
149
+ ### Added
150
+
151
+ - **`vectros bootstrap`/`blueprint apply` mint a blueprint's own AccessProfile
152
+ from `accessProfile.roleIds`** (needs `@vectros-ai/blueprints` ≥ 0.16.0) —
153
+ the platform's additive role composition, wired end to end. Roles are
154
+ created BEFORE the AccessProfile mint (reordered from the prior sequence,
155
+ where nothing depended on the order), so a `roleIds` reference always
156
+ resolves on first apply. `vectros blueprint plan` previews the composed
157
+ form (`profile via role(s) [...]`), and the scope gate / footgun lints /
158
+ `blueprint-test` harness assertions all read the composed action set —
159
+ every referenced role's own clauses, concatenated in order — for a
160
+ `roleIds` profile instead of the (now-absent) inline `allowedActions`.
161
+ - **`vectros access grant --role` and `vectros join --role` are now
162
+ repeatable — 2+ roles compose additively**, sending the platform's
163
+ `roleIds` (plural) instead of the deprecated singular `roleId`. `access
164
+ get`/`access list` render the composed role list; `access explain` now
165
+ fetches and concatenates every referenced role's clauses instead of only
166
+ the first (a profile composing 2+ roles previously rendered as "no
167
+ clauses" — the singular `roleId` the old code followed is absent once a
168
+ profile composes more than one). A single `--role` still round-trips
169
+ through the deprecated singular field on an older server response.
170
+ - **An already-registered issuer now actually reconciles on a blueprint
171
+ re-apply** (needs `@vectros-ai/sdk` ≥ 0.41.0 for the
172
+ platform's `PUT /v1/auth/issuers/{issuerId}`) — `subClaim`/
173
+ `emailClaim`/`userinfoUri`/`selfSignupPolicies` now apply on re-apply, the
174
+ same create-then-reconcile shape the AccessProfile mint already used. The
175
+ trust-anchor/routing-pin fields (`issuer`/`jwksUri`/`audience`/`contextId`)
176
+ remain immutable via that route (rotating one still needs delete +
177
+ re-register), and `status` is reconcilable on the platform endpoint but has
178
+ no field in `BlueprintIssuerSchema` to author — `vectros blueprint plan`'s
179
+ legend and the apply-outcome summary both say so explicitly rather than
180
+ overclaiming full reconciliation.
181
+ **Behavior change:** editing a trust-anchor field (`issuer`/`jwksUri`/
182
+ `audience`/`contextId`) on an already-registered issuer and re-running
183
+ `bootstrap` now FAILS the whole run with the platform's 400, instead of
184
+ the prior silent no-op (the edit was simply never applied). This is the
185
+ correct direction — the loader stopped lying about it — but it is a
186
+ disclosed behavior change: a blueprint whose issuer trust anchor
187
+ drifted from its live registration will now block on re-apply where it
188
+ previously succeeded quietly.
189
+ - **A blueprint can now declare `issuers[].userinfoUri`** (needs
190
+ `@vectros-ai/sdk` ≥ 0.41.0, `@vectros-ai/blueprints` ≥ 0.16.0) — the IdP's
191
+ OIDC userinfo endpoint, used as a fallback email-resolution source when a
192
+ presented access token doesn't carry the configured `emailClaim` directly.
193
+ Lets a blueprint declare this server-side fallback instead of requiring a
194
+ manual `PUT /v1/auth/issuers/{issuerId}` outside blueprint authoring.
195
+ `vectros issuers get` now renders it alongside the other issuer fields.
196
+ - **`vectros identity invite --context <c> --role <roleId> --email <e>`** —
197
+ onboards a person into a context (`POST /v1/users/invite`) without a
198
+ hand-crafted `curl` call. `--role` resolves directly to
199
+ `accessProfile.roleId` (a Role's id is already its own user-facing
200
+ identifier — no separate name lookup). `--accept-url` is required unless
201
+ `--no-send-email` is given, in which case the response's raw
202
+ `inviteToken`/`acceptLink` let you deliver the invitation yourself.
203
+ Authorizes exactly like `access grant`/`role create` — an ordinary
204
+ context-pinned token minted from your `vectros login` session, not a root
205
+ key. No separate setup needed.
206
+
207
+ ### Fixed
208
+
209
+ - **`createRole` (the loader's role mint) now sends `upsert: true`** — without
210
+ it, an already-existing role's `scopes` were silently left unchanged on a
211
+ blueprint re-apply, so editing a role's `allowedActions`/`dataScope` and
212
+ re-running `bootstrap` never actually took effect, contradicting
213
+ `LoaderResult.roleActions`' own documented "reconciled means updated to
214
+ match" contract (the same false-convergence class this release fixes for
215
+ issuers). Pre-existing, caught in review while building `roleIds`
216
+ composition — now more consequential than before, since a `roleIds`-
217
+ composed AccessProfile ties the blueprint's own automatic service
218
+ credential to a role's live (previously possibly-stale) scope.
219
+ - **The bootstrap loader's scope gate now admits an unqualified `profiles:c`
220
+ when the same clause also declares `capabilities: [member-lifecycle]`** —
221
+ the platform's own documented pattern for a self-signup/admin role that
222
+ invites without raw tenant-wide `users:*` could not previously be
223
+ authored through `vectros blueprint apply`/`validate` at all: the gate's
224
+ action check and its capability check ran fully independently, so a bare
225
+ `profiles:c` always rejected regardless of what capabilities accompanied
226
+ it. The exception is narrow and additive — bare form only (a literal
227
+ qualifier like `profiles:c:usr_<id>` is still rejected unconditionally,
228
+ since `member-lifecycle`'s own elevation requires the fully unqualified
229
+ grant and does nothing to bound a literal-principal risk), same clause
230
+ only (a sibling clause's capability grants no benefit), every
231
+ pre-existing call site unaffected (the new parameter is optional).
232
+ - **`vectros blueprint validate`/`bootstrap`/`apply` no longer reject `${{ member.scope.<namespace> }}`
233
+ (and its `:<level>` form) as an unrecognized placeholder.** This is a real, shipped, server-side
234
+ mechanism — resolved per request from `org_membership`-shaped records, with no dependency on a
235
+ separate, unimplemented token-select step that `${{ self.scope.<ns> }}` needs — the CLI's local
236
+ lint simply hadn't been updated to recognize it, so an otherwise-correct blueprint authored
237
+ against it failed `validate` outright with a false `datascope-unknown-placeholder` error, even
238
+ though the platform itself would have accepted it. Found building a real org-membership-based
239
+ blueprint: a role's `${{ self.scope.org }}` dataScope silently granted nothing (an unresolved
240
+ self-placeholder matches nothing, not everything) because nothing populates that identity value
241
+ by default — switching to `${{ member.scope.org }}` is the actual fix for an org_membership-based
242
+ role, and the lint was wrongly in the way of it.
243
+ - **`vectros access explain` now reports `${{ member.scope.<ns>[:<level>] }}` as undecidable** instead
244
+ of silently mismodeling the clause as an opaque literal string — the clause normalizer had no MEMBER
245
+ case at all before this. Same treatment `${{ under.self.* }}` already gets, via `undecidableDims`
246
+ rather than a false match or a silent denial.
247
+
248
+ ### Changed
249
+
250
+ - **`@vectros-ai/sdk` repinned to the latest available `0.41.0` build** —
251
+ picks up the additive role composition's `roleIds` on
252
+ `AccessProfileRequest`/`AccessProfileResponse` and the issuer-update
253
+ surface (`IssuerUpdateRequest`, `client.auth.updateIssuer`). No other wire
254
+ shape changed since the prior pin (verified against the published
255
+ tarballs, not just source ancestry).
256
+
257
+ ## 0.18.0
258
+
259
+ ### Added
260
+
261
+ - **`vectros blueprint plan` now renders the fully-expanded, effective
262
+ per-clause `dataScope` for every declared role** (needs
263
+ `@vectros-ai/blueprints` ≥ 0.15.0) — not just the role's flattened action
264
+ set. This is the review-legibility mitigation the `fragments:`/
265
+ `dataScopeRef` sugar requires: a clause referencing a fragment has already
266
+ been expanded to a literal `dataScope` by the time the CLI sees it, so the
267
+ plan shows exactly what will be granted, never the DSL indirection that
268
+ produced it. Two clauses sharing an action set can legitimately carry
269
+ different `dataScope`, so this is shown per clause, not flattened across
270
+ the role the way the action set is.
271
+ - **A blueprint's `roleAssumable` map is now wired through
272
+ apply** — `vectros bootstrap`/`blueprint apply` passes
273
+ `roleAssumable.<roleId>` straight through to that role's `createRole`
274
+ call as `assumable`, unchanged. `vectros blueprint plan` discloses the
275
+ grant per role (a security-relevant fact the operator approving the plan
276
+ needs to see, same reasoning as the existing capability disclosure), and
277
+ `vectros role get`'s human view now shows a role's `assumable` grant
278
+ (`--json` already did — it prints the SDK response verbatim). Needs
279
+ `@vectros-ai/blueprints` ≥ 0.15.0 and `@vectros-ai/sdk` ≥ 0.41.0 (the
280
+ first generated SDK build carrying `Role.assumable`).
281
+
282
+ ## 0.17.0
283
+ ### Added
284
+
285
+ - **`vectros issuers list --context <ctx>` and `vectros issuers get <issuerId>
286
+ --context <ctx>`** — read a tenant's registered token-exchange issuers
287
+ without hand-minting a bootstrap token first. `GET`/`LIST /v1/auth/issuers`
288
+ require provisioning capability AND are context-confined for any non-root
289
+ caller, so `--context` is a required flag, not a default: a provisioning
290
+ token minted with no context pin resolves to the platform's `default`
291
+ context and won't see an issuer registered under any other one. Both verbs
292
+ now mint a single st_* carrying provisioning capability PINNED to
293
+ `--context` — a shape the CLI's auth seam couldn't produce before this.
294
+ - **A blueprint's `namespaces[]` entry can declare `tenantWide: true`** (needs
295
+ `@vectros-ai/blueprints` ≥ 0.14.0), requesting the platform's tenant-wide
296
+ registration form instead of the blueprint's own context. `bootstrap`/`apply`
297
+ now REFUSE such a blueprint before anything provisions — naming the
298
+ namespace(s) — unless invoked with the new `--allow-tenant-wide-namespaces`
299
+ flag: a static, scriptable opt-in, deliberately not an interactive runtime
300
+ prompt. This is the human-deliberateness check only; whether the request
301
+ actually succeeds is still decided server-side by an OWNER-gated platform
302
+ capability the flag has no bearing on — a non-OWNER credential gets the
303
+ existing clear failure regardless of the flag. An ordinary blueprint (no
304
+ `tenantWide` namespace) is completely unaffected.
305
+ - **`vectros blueprint plan`'s namespace preview names a `tenantWide` namespace
306
+ explicitly** (`TENANT-WIDE — visible to every context`, in place of the usual
307
+ `owned by '<contextId>'`), and its legend gains a carve-out: unlike an
308
+ ordinary namespace registration (which converges silently on a matching
309
+ re-apply), a tenant-wide one never adopts an existing registration, even an
310
+ exact field-for-field match — a collision always fails.
311
+
312
+ ### Changed
313
+
314
+ - **A namespace-name collision on a `tenantWide` registration is always a hard
315
+ failure, never adopted.** The existing read-back-and-reconcile behavior for
316
+ an ordinary, context-owned namespace (a re-apply matching every declared
317
+ field converges silently) does NOT extend to a tenant-wide one: a tenant-wide
318
+ row is co-owned by no single blueprint, so a field-for-field match is not
319
+ evidence it's safe to treat as "this blueprint's" — it could just as easily
320
+ be a different blueprint's (or a manual) registration that happens to declare
321
+ the same shape. The platform's 400 propagates as-is on any collision.
322
+
323
+ ### Fixed
324
+
325
+ - **`bootstrap`/`apply` now register a blueprint's `namespaces[]` BEFORE
326
+ creating any identity it declares in `identities[]`.** Previously, apply-time
327
+ identity resolution ran before namespace registration, so a blueprint
328
+ declaring `identities:` in a namespace it ALSO registers via `namespaces:`
329
+ could never succeed — creating that identity requires the namespace to
330
+ already be registered, and the old ordering always raced the identity create
331
+ ahead of it. The app-context, service principal, and every declared
332
+ namespace are now fully provisioned first; identity resolution (and
333
+ everything downstream) runs after. This is a step-ordering change only — an
334
+ unaffected blueprint (no `identities:` referencing a namespace it also
335
+ registers) sees no behavior change, and no CLI flag or blueprint schema
336
+ changed.
337
+
6
338
  ## 0.16.0
7
339
 
8
340
  ### Added