@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 +151 -8
- package/CHANGELOG.md +210 -0
- package/README.md +196 -3
- package/dist/cli.js +2306 -545
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +2306 -545
- 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.
|
|
@@ -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
|
-
-
|
|
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`).
|
|
192
|
-
|
|
193
|
-
exits non-zero. There is
|
|
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
|
|