@vectros-ai/cli 0.15.0 → 0.19.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 +179 -8
- package/CHANGELOG.md +339 -0
- package/README.md +253 -2
- package/THIRD-PARTY-NOTICES.md +4 -4
- package/dist/cli.js +2018 -570
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +2018 -570
- package/dist/cli.mjs.map +1 -1
- package/package.json +4 -4
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
|
|
53
|
-
the trust boundary:
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
|
|
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,
|
|
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.
|
|
@@ -77,6 +87,30 @@ A name must be 2–50 chars: a lowercase letter, then lowercase letters / digits
|
|
|
77
87
|
+ seed 1 record(s)
|
|
78
88
|
```
|
|
79
89
|
|
|
90
|
+
**A blueprint whose `accessProfile` composes roles (`roleIds:`) previews every
|
|
91
|
+
composed role's clauses individually**, not flattened, because two clauses
|
|
92
|
+
sharing an action set can still carry different `dataScope`:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
Plan for blueprint 'my-thing' (v1.0.0) → context 'mcp':
|
|
96
|
+
+ schema 'thing' (2 fields, HYBRID) lookup: [externalId]
|
|
97
|
+
+ context 'mcp' (MCP — my-thing)
|
|
98
|
+
+ principal 'my-thing-sp' (SERVICE)
|
|
99
|
+
+ role 'case-handler' (1 clause, actions [records:cru])
|
|
100
|
+
clause 1: actions [records:cru], dataScope: scope:org=[${{ self.scope.org }}]
|
|
101
|
+
+ role 'hr-admin' (1 clause, actions [records:r])
|
|
102
|
+
clause 1: actions [records:r], dataScope: (none — unconstrained)
|
|
103
|
+
assumable: scope:org=[org_engineering, org_sales]
|
|
104
|
+
+ profile via role(s) [case-handler, hr-admin] (composed — each role's own clauses, concatenated in order)
|
|
105
|
+
+ key my-thing-<machine> (mint 1 scoped ssk_*)
|
|
106
|
+
+ seed 1 record(s)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A role's `assumable` line only appears when the blueprint's top-level
|
|
110
|
+
`roleAssumable:` map names that role — it's shown because it's
|
|
111
|
+
security-relevant (what a holder of the role may become via `POST
|
|
112
|
+
/v1/auth/token/assume`), not just that the role was declared.
|
|
113
|
+
|
|
80
114
|
`bootstrap` shows the same plan and asks for confirmation before minting
|
|
81
115
|
(`--yes` skips the prompt for CI/agents). ⚠️ **`--no-merge` is NOT a dry run** — it still
|
|
82
116
|
mints/reconciles for real; it only changes where the resulting credential goes, printing the MCP
|
|
@@ -154,13 +188,51 @@ accessProfile:
|
|
|
154
188
|
# owners' records, not shared/seed data. Keys are userId + namespaced scopes
|
|
155
189
|
# (scope:org, scope:client, ...):
|
|
156
190
|
# dataScope: { "scope:org": [<org-uuid>, null] }
|
|
191
|
+
# Omitting dataScope ENTIRELY is different from an empty dataScope: {} —
|
|
192
|
+
# both mean "every owner", but only {} says so on purpose. `validate` warns
|
|
193
|
+
# on the omitted-key form so an author who meant "my own org" and forgot
|
|
194
|
+
# the key notices before minting; write dataScope: {} to say the reach is
|
|
195
|
+
# deliberate.
|
|
196
|
+
# capabilities: [member-lifecycle]
|
|
197
|
+
# A SEPARATE gated dimension from allowedActions — named platform effects a
|
|
198
|
+
# resource:ops verb can't express (e.g. creating/removing an identity in
|
|
199
|
+
# your tenant). Only 'member-lifecycle' is admitted today; every other name
|
|
200
|
+
# is hard-rejected, same posture as the scope gate. See "Capabilities" below.
|
|
201
|
+
#
|
|
202
|
+
# Instead of allowedActions (+ optional dataScope/capabilities), accessProfile
|
|
203
|
+
# may declare `roleIds: [<roleId>, ...]` — one or more roles declared below,
|
|
204
|
+
# in `roles:`, composed additively (each role's own clauses, concatenated in
|
|
205
|
+
# the order listed; a single roleId is equivalent to the platform's
|
|
206
|
+
# deprecated singular roleId reference). Mutually exclusive with allowedActions; a roleIds-
|
|
207
|
+
# composed profile has no inline clause of its own, so dataScope/capabilities
|
|
208
|
+
# are rejected alongside it — author those on the referenced role instead:
|
|
209
|
+
# accessProfile:
|
|
210
|
+
# roleIds: [case-handler, hr-admin]
|
|
211
|
+
|
|
212
|
+
roles:
|
|
213
|
+
# Optional. Reusable, identity-agnostic access rules — DISTINCT from
|
|
214
|
+
# accessProfile (which scopes only the blueprint's own service-principal
|
|
215
|
+
# key). A role is provisioned but bound to no one by `bootstrap`; bind it
|
|
216
|
+
# afterward with `vectros access grant --principal <p> --context <c> --role
|
|
217
|
+
# <roleId>`, or let a human self-enroll with `vectros join <context> --role
|
|
218
|
+
# <roleId>`. Each roleId maps to an ORDERED LIST of clauses — a record is
|
|
219
|
+
# accessible when ANY clause grants it, and within one clause every
|
|
220
|
+
# dataScope dimension must match (the same allowedActions/dataScope/
|
|
221
|
+
# capabilities shape as accessProfile, three-part, evaluated per-clause):
|
|
222
|
+
editor:
|
|
223
|
+
- allowedActions: [records:cru, records:d, search:r, schemas:r]
|
|
224
|
+
dataScope: {} # explicit marker for tenant-wide (unscoped) reach
|
|
225
|
+
member:
|
|
226
|
+
- allowedActions: [records:r, search:r]
|
|
227
|
+
dataScope: { userId: ['${{ self.userId }}'] } # self-scoped: only your own records
|
|
157
228
|
|
|
158
229
|
servicePrincipal:
|
|
159
230
|
externalId: my-thing-sp # stable; the principal the profile + key bind to
|
|
160
231
|
displayName: My Thing
|
|
161
232
|
|
|
162
233
|
seed:
|
|
163
|
-
-
|
|
234
|
+
- surface: record # 'record' | 'document' — the seed's own discriminator
|
|
235
|
+
typeName: thing
|
|
164
236
|
externalId: seed-welcome # stable → idempotent; re-apply converges
|
|
165
237
|
fields: { title: "Hello" }
|
|
166
238
|
# Seeds are illustrative sample data. They're applied by default; provision a
|
|
@@ -172,7 +244,7 @@ issuers:
|
|
|
172
244
|
issuer: "https://your-tenant.us.auth0.com/"
|
|
173
245
|
jwksUri: "https://your-tenant.us.auth0.com/.well-known/jwks.json"
|
|
174
246
|
audience: "https://api.your-app.example.com" # globally unique per (issuer, audience)
|
|
175
|
-
contextId:
|
|
247
|
+
contextId: mcp # MUST equal this blueprint's own contextId (above)
|
|
176
248
|
# Optional trusted third-party IdP issuers (BYO-IdP token exchange). Applied
|
|
177
249
|
# in the BOOTSTRAP-token phase, alongside app-context/service-principal
|
|
178
250
|
# creation — NOT the same phase as schemas/accessProfile/roles above (see
|
|
@@ -180,6 +252,77 @@ issuers:
|
|
|
180
252
|
# is create-once: re-applying an already-registered issuerId leaves it
|
|
181
253
|
# unchanged — there's no update, so an edited jwksUri/audience on a live
|
|
182
254
|
# issuer won't take effect via re-apply.
|
|
255
|
+
#
|
|
256
|
+
# Each entry's contextId must equal the blueprint's own contextId. An issuer
|
|
257
|
+
# is a trust anchor — whoever controls its jwksUri can mint identities your
|
|
258
|
+
# tenant accepts — so a blueprint may only attach one to the context it
|
|
259
|
+
# actually provisions. One IdP account serving several contexts needs one
|
|
260
|
+
# entry per context anyway, because (issuer, audience) must be unique; put
|
|
261
|
+
# each in that context's own blueprint.
|
|
262
|
+
|
|
263
|
+
namespaces:
|
|
264
|
+
- namespace: team # 2-32 chars, lowercase letter first; reserved names
|
|
265
|
+
# (user, record, document, entity, self, tenant,
|
|
266
|
+
# context, scope, versions, lookup) are rejected.
|
|
267
|
+
# org/client are NOT in that set -- reserved namespace
|
|
268
|
+
# names, not built-ins, registered like any other. They
|
|
269
|
+
# already exist tenant-wide at rank 1000/2000 (below) --
|
|
270
|
+
# pick a different specificityRank, and note a
|
|
271
|
+
# context-owned registration shadows the tenant-wide
|
|
272
|
+
# one for this context's own callers.
|
|
273
|
+
specificityRank: 500 # 0..1_000_000, unique among this blueprint's namespaces
|
|
274
|
+
entityBacked: true # optional
|
|
275
|
+
membershipRecordType: thing # MUST be a typeName this blueprint declares in
|
|
276
|
+
# schemas: above — membership can only resolve
|
|
277
|
+
# over a record type the blueprint itself ships,
|
|
278
|
+
# never one that merely already exists in the
|
|
279
|
+
# target context. membershipTargetField required
|
|
280
|
+
# alongside it.
|
|
281
|
+
membershipTargetField: userId
|
|
282
|
+
membershipLevelField: level # optional; membershipLevels required alongside it
|
|
283
|
+
membershipLevels: [admin, viewer]
|
|
284
|
+
# tenantWide: true # optional, default false. Requests registration visible to
|
|
285
|
+
# # EVERY context in the account, not just this blueprint's own —
|
|
286
|
+
# # see below.
|
|
287
|
+
# Registered in the SAME bootstrap-token phase as issuers, before schemas.
|
|
288
|
+
# Owned by this blueprint's own contextId (above) by default. `tenantWide: true` opts
|
|
289
|
+
# out of that, but declaring it is not enough on its own: the applying credential must
|
|
290
|
+
# separately hold OWNER-only authority, and `bootstrap`/`apply` refuse to even request
|
|
291
|
+
# it without the explicit `--allow-tenant-wide-namespaces` flag — a blueprint cannot make
|
|
292
|
+
# this happen by itself. Registration is create-once, NOT idempotent server-side: a
|
|
293
|
+
# re-apply matching every declared field converges silently, but one that disagrees
|
|
294
|
+
# with what's already registered fails the apply rather than silently overwriting it —
|
|
295
|
+
# for a tenant-wide namespace this includes a collision with a DIFFERENT blueprint's
|
|
296
|
+
# (or a manual) registration of the same name, which always fails rather than being
|
|
297
|
+
# silently adopted.
|
|
298
|
+
|
|
299
|
+
identities:
|
|
300
|
+
team: # local name — referenced elsewhere as ${{ identities.team }}
|
|
301
|
+
kind: org # 'user', or an entity namespace (org/client/one you registered)
|
|
302
|
+
externalId: acme-team # your stable id; resolution is idempotent by this value
|
|
303
|
+
displayName: Acme Team # optional (users: folded into payload.displayName instead —
|
|
304
|
+
# a user has no first-class `name` field)
|
|
305
|
+
metadata: { plan: pro } # optional JSON object, merged into the principal's payload
|
|
306
|
+
# A declared name must match letters/digits/underscore, not starting with a
|
|
307
|
+
# digit (demoOrg, not demo-org) — the same grammar '${{ identities.<name> }}'
|
|
308
|
+
# itself can match. A name outside it is rejected as a parse error at
|
|
309
|
+
# declare time (validate/plan), not silently accepted. Every declared
|
|
310
|
+
# identity is ensure-exist'd (idempotent by externalId), whether or not
|
|
311
|
+
# anything below references it, in its OWN creds-bearing pass — under the
|
|
312
|
+
# bootstrap credential, AFTER the app-context, service principal, and any
|
|
313
|
+
# namespaces: above are registered, but BEFORE schemas/accessProfile/roles
|
|
314
|
+
# (not in BLUEPRINT_FIELD_PHASES for that reason). This ordering matters
|
|
315
|
+
# for a non-'user' kind: an 'org'/'client'/registered-namespace identity is
|
|
316
|
+
# created IN that namespace, so it can only succeed once the namespace
|
|
317
|
+
# exists — declare it in namespaces: above (or ensure it's already
|
|
318
|
+
# registered) before referencing it here with a non-'user' kind.
|
|
319
|
+
# Reference the resolved principal id anywhere one is valid — a schema's
|
|
320
|
+
# userId/scopes, an accessProfile/role dataScope or identityOverrides,
|
|
321
|
+
# seed-record ownership — e.g.:
|
|
322
|
+
# identityOverrides: { "scope:org": "${{ identities.team }}" }
|
|
323
|
+
# Only the WHOLE id can be referenced (no '${{ identities.team.externalId }}'
|
|
324
|
+
# property access), and every reference must name an identity declared here
|
|
325
|
+
# — both checked OFFLINE at validate/plan, never deferred to a live apply.
|
|
183
326
|
```
|
|
184
327
|
|
|
185
328
|
YAML or JSON are both accepted (chosen by extension). The bundled library is
|
|
@@ -308,6 +451,19 @@ wildcard (`records:*`). Anything else — control-plane resources or the bare
|
|
|
308
451
|
*why* it's safe to apply a blueprint you didn't write: the binary is the trust
|
|
309
452
|
boundary, not the file.
|
|
310
453
|
|
|
454
|
+
### Capabilities (`granted_capabilities`) — a separate, equally-gated dimension
|
|
455
|
+
|
|
456
|
+
An `accessProfile` or role clause may also declare `capabilities: [...]` —
|
|
457
|
+
named platform effects (like creating or removing an identity in your tenant)
|
|
458
|
+
that a `resource:ops` scope token can't express. This rides its own field,
|
|
459
|
+
separate from `allowedActions`, and has its own allowlist: **only
|
|
460
|
+
`member-lifecycle` is admitted today.** Every other capability name —
|
|
461
|
+
including ones the platform's own registry recognizes — is refused, the same
|
|
462
|
+
fail-closed, deliberately-narrow posture as the scope gate above. `validate`,
|
|
463
|
+
`plan`, `blueprint-test` and `bootstrap` all report a rejected capability the
|
|
464
|
+
same way they report a rejected scope: nothing mints, and the CLI exits
|
|
465
|
+
non-zero.
|
|
466
|
+
|
|
311
467
|
### The idempotency contract
|
|
312
468
|
|
|
313
469
|
Re-applying a blueprint **converges, never duplicates**, because everything has
|
|
@@ -323,6 +479,21 @@ but fails on every re-apply with an "already exists" error instead of the
|
|
|
323
479
|
existing record. Request `:r` alongside `:c` on every resource your blueprint
|
|
324
480
|
creates (the bundled blueprints all do this — see the example above).
|
|
325
481
|
|
|
482
|
+
**One qualification to "converges, never duplicates": a NEW key mint bound to
|
|
483
|
+
a pre-existing service principal is not silent.** If `servicePrincipal.externalId`
|
|
484
|
+
resolves to an identity that already existed in the tenant before this run —
|
|
485
|
+
ordinarily true on a routine same-machine re-apply, since the earlier run's
|
|
486
|
+
local key record already names that same principal — `bootstrap` proceeds as
|
|
487
|
+
usual. But the FIRST time a new machine (no local key record for the blueprint's
|
|
488
|
+
`contextId`) applies a blueprint whose service principal already exists, it
|
|
489
|
+
refuses by default and asks for confirmation: interactively, a y/N prompt;
|
|
490
|
+
non-interactively (`--yes`, or no TTY), it requires `--confirm-existing-principal`
|
|
491
|
+
and otherwise exits with an error rather than minting silently. This is
|
|
492
|
+
deliberate — the platform cannot tell an intentional second-machine setup apart
|
|
493
|
+
from a blueprint's `externalId` colliding with an unrelated identity, so it asks
|
|
494
|
+
rather than guessing either way. `--rotate` is exempt only when the local
|
|
495
|
+
record already names the same principal being rotated.
|
|
496
|
+
|
|
326
497
|
## Distribution
|
|
327
498
|
|
|
328
499
|
- **Bundled** — reviewed, versioned blueprints shipped in the library
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,345 @@
|
|
|
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.19.0 — 2026-08-27
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`vectros bootstrap`/`blueprint apply` mint a blueprint's own AccessProfile
|
|
11
|
+
from `accessProfile.roleIds`** (needs `@vectros-ai/blueprints` ≥ 0.16.0) —
|
|
12
|
+
the platform's additive role composition, wired end to end. Roles are
|
|
13
|
+
created BEFORE the AccessProfile mint (reordered from the prior sequence,
|
|
14
|
+
where nothing depended on the order), so a `roleIds` reference always
|
|
15
|
+
resolves on first apply. `vectros blueprint plan` previews the composed
|
|
16
|
+
form (`profile via role(s) [...]`), and the scope gate / footgun lints /
|
|
17
|
+
`blueprint-test` harness assertions all read the composed action set —
|
|
18
|
+
every referenced role's own clauses, concatenated in order — for a
|
|
19
|
+
`roleIds` profile instead of the (now-absent) inline `allowedActions`.
|
|
20
|
+
- **`vectros access grant --role` and `vectros join --role` are now
|
|
21
|
+
repeatable — 2+ roles compose additively**, sending the platform's
|
|
22
|
+
`roleIds` (plural) instead of the deprecated singular `roleId`. `access
|
|
23
|
+
get`/`access list` render the composed role list; `access explain` now
|
|
24
|
+
fetches and concatenates every referenced role's clauses instead of only
|
|
25
|
+
the first (a profile composing 2+ roles previously rendered as "no
|
|
26
|
+
clauses" — the singular `roleId` the old code followed is absent once a
|
|
27
|
+
profile composes more than one). A single `--role` still round-trips
|
|
28
|
+
through the deprecated singular field on an older server response.
|
|
29
|
+
- **An already-registered issuer now actually reconciles on a blueprint
|
|
30
|
+
re-apply** (needs `@vectros-ai/sdk` ≥ 0.41.0 for the
|
|
31
|
+
platform's `PUT /v1/auth/issuers/{issuerId}`) — `subClaim`/
|
|
32
|
+
`emailClaim`/`userinfoUri`/`selfSignupPolicies` now apply on re-apply, the
|
|
33
|
+
same create-then-reconcile shape the AccessProfile mint already used. The
|
|
34
|
+
trust-anchor/routing-pin fields (`issuer`/`jwksUri`/`audience`/`contextId`)
|
|
35
|
+
remain immutable via that route (rotating one still needs delete +
|
|
36
|
+
re-register), and `status` is reconcilable on the platform endpoint but has
|
|
37
|
+
no field in `BlueprintIssuerSchema` to author — `vectros blueprint plan`'s
|
|
38
|
+
legend and the apply-outcome summary both say so explicitly rather than
|
|
39
|
+
overclaiming full reconciliation.
|
|
40
|
+
**Behavior change:** editing a trust-anchor field (`issuer`/`jwksUri`/
|
|
41
|
+
`audience`/`contextId`) on an already-registered issuer and re-running
|
|
42
|
+
`bootstrap` now FAILS the whole run with the platform's 400, instead of
|
|
43
|
+
the prior silent no-op (the edit was simply never applied). This is the
|
|
44
|
+
correct direction — the loader stopped lying about it — but it is a
|
|
45
|
+
disclosed behavior change: a blueprint whose issuer trust anchor
|
|
46
|
+
drifted from its live registration will now block on re-apply where it
|
|
47
|
+
previously succeeded quietly.
|
|
48
|
+
- **A blueprint can now declare `issuers[].userinfoUri`** (needs
|
|
49
|
+
`@vectros-ai/sdk` ≥ 0.41.0, `@vectros-ai/blueprints` ≥ 0.16.0) — the IdP's
|
|
50
|
+
OIDC userinfo endpoint, used as a fallback email-resolution source when a
|
|
51
|
+
presented access token doesn't carry the configured `emailClaim` directly.
|
|
52
|
+
Lets a blueprint declare this server-side fallback instead of requiring a
|
|
53
|
+
manual `PUT /v1/auth/issuers/{issuerId}` outside blueprint authoring.
|
|
54
|
+
`vectros issuers get` now renders it alongside the other issuer fields.
|
|
55
|
+
- **`vectros identity invite --context <c> --role <roleId> --email <e>`** —
|
|
56
|
+
onboards a person into a context (`POST /v1/users/invite`) without a
|
|
57
|
+
hand-crafted `curl` call. `--role` resolves directly to
|
|
58
|
+
`accessProfile.roleId` (a Role's id is already its own user-facing
|
|
59
|
+
identifier — no separate name lookup). `--accept-url` is required unless
|
|
60
|
+
`--no-send-email` is given, in which case the response's raw
|
|
61
|
+
`inviteToken`/`acceptLink` let you deliver the invitation yourself.
|
|
62
|
+
Authorizes exactly like `access grant`/`role create` — an ordinary
|
|
63
|
+
context-pinned token minted from your `vectros login` session, not a root
|
|
64
|
+
key. No separate setup needed.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- **`createRole` (the loader's role mint) now sends `upsert: true`** — without
|
|
69
|
+
it, an already-existing role's `scopes` were silently left unchanged on a
|
|
70
|
+
blueprint re-apply, so editing a role's `allowedActions`/`dataScope` and
|
|
71
|
+
re-running `bootstrap` never actually took effect, contradicting
|
|
72
|
+
`LoaderResult.roleActions`' own documented "reconciled means updated to
|
|
73
|
+
match" contract (the same false-convergence class this release fixes for
|
|
74
|
+
issuers). Pre-existing, caught in review while building `roleIds`
|
|
75
|
+
composition — now more consequential than before, since a `roleIds`-
|
|
76
|
+
composed AccessProfile ties the blueprint's own automatic service
|
|
77
|
+
credential to a role's live (previously possibly-stale) scope.
|
|
78
|
+
- **The bootstrap loader's scope gate now admits an unqualified `profiles:c`
|
|
79
|
+
when the same clause also declares `capabilities: [member-lifecycle]`** —
|
|
80
|
+
the platform's own documented pattern for a self-signup/admin role that
|
|
81
|
+
invites without raw tenant-wide `users:*` could not previously be
|
|
82
|
+
authored through `vectros blueprint apply`/`validate` at all: the gate's
|
|
83
|
+
action check and its capability check ran fully independently, so a bare
|
|
84
|
+
`profiles:c` always rejected regardless of what capabilities accompanied
|
|
85
|
+
it. The exception is narrow and additive — bare form only (a literal
|
|
86
|
+
qualifier like `profiles:c:usr_<id>` is still rejected unconditionally,
|
|
87
|
+
since `member-lifecycle`'s own elevation requires the fully unqualified
|
|
88
|
+
grant and does nothing to bound a literal-principal risk), same clause
|
|
89
|
+
only (a sibling clause's capability grants no benefit), every
|
|
90
|
+
pre-existing call site unaffected (the new parameter is optional).
|
|
91
|
+
- **`vectros blueprint validate`/`bootstrap`/`apply` no longer reject `${{ member.scope.<namespace> }}`
|
|
92
|
+
(and its `:<level>` form) as an unrecognized placeholder.** This is a real, shipped, server-side
|
|
93
|
+
mechanism — resolved per request from `org_membership`-shaped records, with no dependency on a
|
|
94
|
+
separate, unimplemented token-select step that `${{ self.scope.<ns> }}` needs — the CLI's local
|
|
95
|
+
lint simply hadn't been updated to recognize it, so an otherwise-correct blueprint authored
|
|
96
|
+
against it failed `validate` outright with a false `datascope-unknown-placeholder` error, even
|
|
97
|
+
though the platform itself would have accepted it. Found building a real org-membership-based
|
|
98
|
+
blueprint: a role's `${{ self.scope.org }}` dataScope silently granted nothing (an unresolved
|
|
99
|
+
self-placeholder matches nothing, not everything) because nothing populates that identity value
|
|
100
|
+
by default — switching to `${{ member.scope.org }}` is the actual fix for an org_membership-based
|
|
101
|
+
role, and the lint was wrongly in the way of it.
|
|
102
|
+
- **`vectros access explain` now reports `${{ member.scope.<ns>[:<level>] }}` as undecidable** instead
|
|
103
|
+
of silently mismodeling the clause as an opaque literal string — the clause normalizer had no MEMBER
|
|
104
|
+
case at all before this. Same treatment `${{ under.self.* }}` already gets, via `undecidableDims`
|
|
105
|
+
rather than a false match or a silent denial.
|
|
106
|
+
|
|
107
|
+
### Changed
|
|
108
|
+
|
|
109
|
+
- **`@vectros-ai/sdk` repinned to the latest available `0.41.0` build** —
|
|
110
|
+
picks up the additive role composition's `roleIds` on
|
|
111
|
+
`AccessProfileRequest`/`AccessProfileResponse` and the issuer-update
|
|
112
|
+
surface (`IssuerUpdateRequest`, `client.auth.updateIssuer`). No other wire
|
|
113
|
+
shape changed since the prior pin (verified against the published
|
|
114
|
+
tarballs, not just source ancestry).
|
|
115
|
+
|
|
116
|
+
## 0.18.0
|
|
117
|
+
|
|
118
|
+
### Added
|
|
119
|
+
|
|
120
|
+
- **`vectros blueprint plan` now renders the fully-expanded, effective
|
|
121
|
+
per-clause `dataScope` for every declared role** (needs
|
|
122
|
+
`@vectros-ai/blueprints` ≥ 0.15.0) — not just the role's flattened action
|
|
123
|
+
set. This is the review-legibility mitigation the `fragments:`/
|
|
124
|
+
`dataScopeRef` sugar requires: a clause referencing a fragment has already
|
|
125
|
+
been expanded to a literal `dataScope` by the time the CLI sees it, so the
|
|
126
|
+
plan shows exactly what will be granted, never the DSL indirection that
|
|
127
|
+
produced it. Two clauses sharing an action set can legitimately carry
|
|
128
|
+
different `dataScope`, so this is shown per clause, not flattened across
|
|
129
|
+
the role the way the action set is.
|
|
130
|
+
- **A blueprint's `roleAssumable` map is now wired through
|
|
131
|
+
apply** — `vectros bootstrap`/`blueprint apply` passes
|
|
132
|
+
`roleAssumable.<roleId>` straight through to that role's `createRole`
|
|
133
|
+
call as `assumable`, unchanged. `vectros blueprint plan` discloses the
|
|
134
|
+
grant per role (a security-relevant fact the operator approving the plan
|
|
135
|
+
needs to see, same reasoning as the existing capability disclosure), and
|
|
136
|
+
`vectros role get`'s human view now shows a role's `assumable` grant
|
|
137
|
+
(`--json` already did — it prints the SDK response verbatim). Needs
|
|
138
|
+
`@vectros-ai/blueprints` ≥ 0.15.0 and `@vectros-ai/sdk` ≥ 0.41.0 (the
|
|
139
|
+
first generated SDK build carrying `Role.assumable`).
|
|
140
|
+
|
|
141
|
+
## 0.17.0
|
|
142
|
+
### Added
|
|
143
|
+
|
|
144
|
+
- **`vectros issuers list --context <ctx>` and `vectros issuers get <issuerId>
|
|
145
|
+
--context <ctx>`** — read a tenant's registered token-exchange issuers
|
|
146
|
+
without hand-minting a bootstrap token first. `GET`/`LIST /v1/auth/issuers`
|
|
147
|
+
require provisioning capability AND are context-confined for any non-root
|
|
148
|
+
caller, so `--context` is a required flag, not a default: a provisioning
|
|
149
|
+
token minted with no context pin resolves to the platform's `default`
|
|
150
|
+
context and won't see an issuer registered under any other one. Both verbs
|
|
151
|
+
now mint a single st_* carrying provisioning capability PINNED to
|
|
152
|
+
`--context` — a shape the CLI's auth seam couldn't produce before this.
|
|
153
|
+
- **A blueprint's `namespaces[]` entry can declare `tenantWide: true`** (needs
|
|
154
|
+
`@vectros-ai/blueprints` ≥ 0.14.0), requesting the platform's tenant-wide
|
|
155
|
+
registration form instead of the blueprint's own context. `bootstrap`/`apply`
|
|
156
|
+
now REFUSE such a blueprint before anything provisions — naming the
|
|
157
|
+
namespace(s) — unless invoked with the new `--allow-tenant-wide-namespaces`
|
|
158
|
+
flag: a static, scriptable opt-in, deliberately not an interactive runtime
|
|
159
|
+
prompt. This is the human-deliberateness check only; whether the request
|
|
160
|
+
actually succeeds is still decided server-side by an OWNER-gated platform
|
|
161
|
+
capability the flag has no bearing on — a non-OWNER credential gets the
|
|
162
|
+
existing clear failure regardless of the flag. An ordinary blueprint (no
|
|
163
|
+
`tenantWide` namespace) is completely unaffected.
|
|
164
|
+
- **`vectros blueprint plan`'s namespace preview names a `tenantWide` namespace
|
|
165
|
+
explicitly** (`TENANT-WIDE — visible to every context`, in place of the usual
|
|
166
|
+
`owned by '<contextId>'`), and its legend gains a carve-out: unlike an
|
|
167
|
+
ordinary namespace registration (which converges silently on a matching
|
|
168
|
+
re-apply), a tenant-wide one never adopts an existing registration, even an
|
|
169
|
+
exact field-for-field match — a collision always fails.
|
|
170
|
+
|
|
171
|
+
### Changed
|
|
172
|
+
|
|
173
|
+
- **A namespace-name collision on a `tenantWide` registration is always a hard
|
|
174
|
+
failure, never adopted.** The existing read-back-and-reconcile behavior for
|
|
175
|
+
an ordinary, context-owned namespace (a re-apply matching every declared
|
|
176
|
+
field converges silently) does NOT extend to a tenant-wide one: a tenant-wide
|
|
177
|
+
row is co-owned by no single blueprint, so a field-for-field match is not
|
|
178
|
+
evidence it's safe to treat as "this blueprint's" — it could just as easily
|
|
179
|
+
be a different blueprint's (or a manual) registration that happens to declare
|
|
180
|
+
the same shape. The platform's 400 propagates as-is on any collision.
|
|
181
|
+
|
|
182
|
+
### Fixed
|
|
183
|
+
|
|
184
|
+
- **`bootstrap`/`apply` now register a blueprint's `namespaces[]` BEFORE
|
|
185
|
+
creating any identity it declares in `identities[]`.** Previously, apply-time
|
|
186
|
+
identity resolution ran before namespace registration, so a blueprint
|
|
187
|
+
declaring `identities:` in a namespace it ALSO registers via `namespaces:`
|
|
188
|
+
could never succeed — creating that identity requires the namespace to
|
|
189
|
+
already be registered, and the old ordering always raced the identity create
|
|
190
|
+
ahead of it. The app-context, service principal, and every declared
|
|
191
|
+
namespace are now fully provisioned first; identity resolution (and
|
|
192
|
+
everything downstream) runs after. This is a step-ordering change only — an
|
|
193
|
+
unaffected blueprint (no `identities:` referencing a namespace it also
|
|
194
|
+
registers) sees no behavior change, and no CLI flag or blueprint schema
|
|
195
|
+
changed.
|
|
196
|
+
|
|
197
|
+
## 0.16.0
|
|
198
|
+
|
|
199
|
+
### Added
|
|
200
|
+
|
|
201
|
+
- **A blueprint's `accessProfile` and role clauses can now declare
|
|
202
|
+
`capabilities: [...]`** — the platform's `granted_capabilities` field,
|
|
203
|
+
a grant dimension separate from `allowedActions` for named effects that
|
|
204
|
+
reach across a partition boundary (e.g. creating/removing an identity in
|
|
205
|
+
your tenant) which no `resource:ops` verb can express. Gated the same way
|
|
206
|
+
`allowedActions` is: only `member-lifecycle` is admitted, fail-closed,
|
|
207
|
+
every other name (including ones the platform's own registry recognizes)
|
|
208
|
+
is refused before anything mints. `vectros blueprint validate`/`plan` and
|
|
209
|
+
`bootstrap` all report a capability grant/rejection the same way they
|
|
210
|
+
already report a scope grant/rejection.
|
|
211
|
+
|
|
212
|
+
- **`bootstrap` now flags when a mint binds to a principal that already existed in your
|
|
213
|
+
tenant before this run**, and the outcome summary always reports whether the service
|
|
214
|
+
principal was newly created or reconciled. This surfaces the one collision shape the
|
|
215
|
+
platform itself can't tell apart from an intentional one (e.g. re-applying a blueprint
|
|
216
|
+
from a second machine): a blueprint's declared `servicePrincipal.externalId` matching an
|
|
217
|
+
existing, unrelated identity in your tenant. Interactively, you're prompted to confirm
|
|
218
|
+
before the key mints; non-interactively (`--yes`, or no TTY) the run refuses by default
|
|
219
|
+
and requires the new `--confirm-existing-principal` flag to proceed, so an unexpected
|
|
220
|
+
collision fails a CI run loudly instead of silently minting a credential bound to the
|
|
221
|
+
wrong identity. Never triggered by `--rotate` or an ordinary same-machine re-apply — both
|
|
222
|
+
are exempted as already-informed operator intent.
|
|
223
|
+
|
|
224
|
+
- **The bootstrap scope gate now admits `profiles:c:self`.** The platform's
|
|
225
|
+
`profiles:c`/`u`/`d` accept an optional qualifier confining WHICH principal
|
|
226
|
+
the grant may act on — a literal `usr_<id>`, or the bare `self` sentinel,
|
|
227
|
+
which the platform resolves from the caller's own bound identity, never
|
|
228
|
+
from anything a blueprint declares. A role clause (or the bootstrap
|
|
229
|
+
`accessProfile`) may now grant `profiles:c:self` to let a credential create
|
|
230
|
+
an access profile for itself. A bare, unqualified `profiles:c`, and a
|
|
231
|
+
literal `profiles:c:usr_<id>` naming another principal, still hard-reject —
|
|
232
|
+
only the platform-resolved `self` form is admitted.
|
|
233
|
+
|
|
234
|
+
- **`vectros blueprint validate`/`plan` now warns when a role or the
|
|
235
|
+
`accessProfile` clause has NO `dataScope` key at all on a read/update/delete
|
|
236
|
+
action against `records`/`documents`/`entities`.** The existing null-sentinel
|
|
237
|
+
warning already caught a *narrower* dataScope gap; this catches the more
|
|
238
|
+
dangerous case of no constraint whatsoever, which grants reach across every
|
|
239
|
+
owner in the tenant — not "my own org", whatever the author may have
|
|
240
|
+
intended. Advisory only (`exit 0`): a deliberately shared/tenant-wide
|
|
241
|
+
resource (declared with `dataScope: {}`) is unaffected.
|
|
242
|
+
|
|
243
|
+
- **Top-level `namespaces[]`** registers a blueprint's own namespaces via
|
|
244
|
+
`POST /v1/namespaces`, in the same bootstrap-token phase as `issuers`.
|
|
245
|
+
Every registration is unconditionally owned by the blueprint's own
|
|
246
|
+
context — there is no tenant-wide option, since the platform confines
|
|
247
|
+
namespace registration to the caller's own context unconditionally, even
|
|
248
|
+
for a `contextId: null` request. Registration is not idempotent
|
|
249
|
+
server-side; a re-apply matching the existing declaration converges
|
|
250
|
+
silently, one that disagrees throws rather than being swallowed.
|
|
251
|
+
|
|
252
|
+
### Fixed
|
|
253
|
+
|
|
254
|
+
- **`key issue`/`list`/`get`/`revoke`/`rotate` can now target the test tenant
|
|
255
|
+
via `--tenant test`**, matching `bootstrap`/`join`/`context`. Previously
|
|
256
|
+
none of the five accepted it, so a key minted into the test tenant (e.g. by
|
|
257
|
+
`bootstrap --tenant test`) could be discovered via `key list --all-contexts`
|
|
258
|
+
(which spans both tenants) but never individually fetched, revoked, rotated,
|
|
259
|
+
or re-issued — there was no way to reach it at all. `--tenant` is refused in
|
|
260
|
+
combination with `--all-contexts`, which already spans both tenants on its
|
|
261
|
+
own.
|
|
262
|
+
|
|
263
|
+
- **`bootstrap` (and `blueprint-test`) now work for a blueprint whose `issuers:`
|
|
264
|
+
target a non-`default` app context** — previously every such blueprint failed
|
|
265
|
+
registration with an authorization error, even though the same blueprint's
|
|
266
|
+
schemas/roles/access profile provisioned fine. The bootstrap credential now
|
|
267
|
+
re-mints itself, pinned to the blueprint's own context, specifically to
|
|
268
|
+
register issuers — closing the gap left once the platform started requiring
|
|
269
|
+
an issuer registration to be confined to the context it targets. No blueprint
|
|
270
|
+
authoring change needed; existing `issuers:` entries behave the same, they
|
|
271
|
+
just no longer fail for any context other than `default`.
|
|
272
|
+
|
|
273
|
+
- **`blueprint-test` now also registers and reads back each declared `namespaces[]` entry**,
|
|
274
|
+
the same assert-readback proof `issuers`/schemas/roles already get. Its teardown is honest
|
|
275
|
+
about what it can and can't undo: a namespace registration is never deleted — namespace
|
|
276
|
+
deletion requires a root API key, which the harness never holds — and is reported as a
|
|
277
|
+
permanent skip rather than a silently-attempted step. Issuer teardown may now report a failed
|
|
278
|
+
step rather than a silent success, too: the platform refuses (`409`) to delete an issuer that
|
|
279
|
+
has ever completed a real token exchange during the run.
|
|
280
|
+
|
|
281
|
+
- **The blueprint format reference showed an `issuers:` entry that no longer validates.**
|
|
282
|
+
`AUTHORING.md`'s "the format, field by field" example declared one `contextId` for the
|
|
283
|
+
blueprint and a different one inside `issuers[0]`. As of `@vectros-ai/blueprints` 0.13.0
|
|
284
|
+
an issuer must target the blueprint's own app context, so copying that example produced a
|
|
285
|
+
blueprint `vectros blueprint validate` rejects. The example now matches, and the reference
|
|
286
|
+
states the constraint where the field is introduced rather than leaving you to discover it
|
|
287
|
+
from a validation error.
|
|
288
|
+
|
|
289
|
+
- **The bootstrap scope gate now admits `profiles:r`/`profiles:u`/`profiles:d`** —
|
|
290
|
+
read is disclosure-only and cannot widen anything; update/delete are bounded by
|
|
291
|
+
a platform-enforced check that a write to an AccessProfile can never grant more
|
|
292
|
+
scope than the requesting credential already holds, so they can only
|
|
293
|
+
redistribute or revoke access, never manufacture new reach. A bare, unqualified
|
|
294
|
+
`profiles:c` (creating a profile for an arbitrary, caller-chosen principal
|
|
295
|
+
name) and every `users:*` verb stay rejected — see the `profiles:c:self`
|
|
296
|
+
entry above for the one qualified exception. This unblocks any blueprint
|
|
297
|
+
role that reads or manages its own app-context's membership — previously
|
|
298
|
+
the gate refused the whole role outright the moment it named any
|
|
299
|
+
`profiles:*` scope.
|
|
300
|
+
|
|
301
|
+
- **`bootstrap --tenant test`, `join --tenant test`, and `key issue|rotate
|
|
302
|
+
--to-keyring` no longer silently activate over a live credential.** Storing
|
|
303
|
+
a freshly-minted (or re-joined) test-tenant key used to make it the
|
|
304
|
+
machine's active identity unconditionally, exactly like a live-tenant run —
|
|
305
|
+
flipping what `vectros keyring show --format raw` (and anything that
|
|
306
|
+
resolves through it with no explicit `--alias`, including the MCP server)
|
|
307
|
+
authenticates as, with no warning. `key issue|rotate --to-keyring` has no
|
|
308
|
+
`--tenant` flag of its own; it now classifies the tenant from the minted
|
|
309
|
+
secret's own prefix. All three commands now activate unless doing so would
|
|
310
|
+
displace a currently-active LIVE credential, in which case the key is
|
|
311
|
+
stored (or the re-join still counts as a successful join) without switching
|
|
312
|
+
to it, and the terminal says so explicitly — naming whether that was the
|
|
313
|
+
safety default or an explicit `--no-activate`, never the wrong reason.
|
|
314
|
+
`bootstrap`/`join` also take `--activate` to force activation over a live
|
|
315
|
+
credential explicitly; `key issue|rotate --to-keyring` has no such flag
|
|
316
|
+
today — run `vectros switch <alias>` afterward if you want that key active.
|
|
317
|
+
|
|
318
|
+
- **`keyring list`'s human-readable table now shows a `tenant` (test/live)
|
|
319
|
+
column and drops `env` (staging/production).** `env` is an internal-team
|
|
320
|
+
concern an external developer's CLI never surfaces; `tenant` is the
|
|
321
|
+
distinction every user actually holds two credentials side by side over, and
|
|
322
|
+
previously the only way to see it was to run `doctor` instead or hope the
|
|
323
|
+
alias happened to carry a `.test` suffix. `--json` output is UNCHANGED —
|
|
324
|
+
still metadata-only, no per-entry secret read, no `tenant` field — because
|
|
325
|
+
resolving every entry's secret from the OS credential store (what showing
|
|
326
|
+
tenant requires) is real per-invocation cost a script relying on `--json`'s
|
|
327
|
+
established speed should not silently start paying.
|
|
328
|
+
|
|
329
|
+
### Changed
|
|
330
|
+
|
|
331
|
+
- **Internal:** the exact `@vectros-ai/blueprints` dependency pin was updated so a clean
|
|
332
|
+
`npm ci` resolves. No behaviour change.
|
|
333
|
+
|
|
334
|
+
### Security
|
|
335
|
+
|
|
336
|
+
- **`@napi-rs/keyring` is now pinned to an exact version** (was `^1.3.0`). The
|
|
337
|
+
Linux/Windows fail-closed guarantee behind `keyring remove`/`prune` rests on
|
|
338
|
+
an upstream behavioural contract — a locked credential store must make
|
|
339
|
+
`findCredentials()` throw, not return an empty list — that this package does
|
|
340
|
+
not otherwise verify at install time. A future bump of this dependency should
|
|
341
|
+
be treated as a security review of that contract against the new release, not
|
|
342
|
+
a routine version update (see the comment at its enforcement site,
|
|
343
|
+
`os-keychain.ts`, for exactly what would break).
|
|
344
|
+
|
|
6
345
|
## 0.15.0
|
|
7
346
|
|
|
8
347
|
### Added
|