@vectros-ai/cli 0.16.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 CHANGED
@@ -87,6 +87,30 @@ A name must be 2–50 chars: a lowercase letter, then lowercase letters / digits
87
87
  + seed 1 record(s)
88
88
  ```
89
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
+
90
114
  `bootstrap` shows the same plan and asks for confirmation before minting
91
115
  (`--yes` skips the prompt for CI/agents). ⚠️ **`--no-merge` is NOT a dry run** — it still
92
116
  mints/reconciles for real; it only changes where the resulting credential goes, printing the MCP
@@ -174,6 +198,16 @@ accessProfile:
174
198
  # resource:ops verb can't express (e.g. creating/removing an identity in
175
199
  # your tenant). Only 'member-lifecycle' is admitted today; every other name
176
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]
177
211
 
178
212
  roles:
179
213
  # Optional. Reusable, identity-agnostic access rules — DISTINCT from
@@ -247,14 +281,20 @@ namespaces:
247
281
  membershipTargetField: userId
248
282
  membershipLevelField: level # optional; membershipLevels required alongside it
249
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.
250
287
  # 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.
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.
258
298
 
259
299
  identities:
260
300
  team: # local name — referenced elsewhere as ${{ identities.team }}
@@ -267,10 +307,15 @@ identities:
267
307
  # digit (demoOrg, not demo-org) — the same grammar '${{ identities.<name> }}'
268
308
  # itself can match. A name outside it is rejected as a parse error at
269
309
  # 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).
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.
274
319
  # Reference the resolved principal id anywhere one is valid — a schema's
275
320
  # userId/scopes, an accessProfile/role dataScope or identityOverrides,
276
321
  # seed-record ownership — e.g.:
package/CHANGELOG.md CHANGED
@@ -3,6 +3,197 @@
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
+
6
197
  ## 0.16.0
7
198
 
8
199
  ### Added
package/README.md CHANGED
@@ -9,11 +9,13 @@ The Vectros provisioning CLI — the `vectros` binary:
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
11
  - **`vectros context`** — create, list, and destroy app contexts.
12
+ - **`vectros issuers`** — inspect registered third-party IdP issuers (token-exchange trust anchors) for a context.
12
13
  - **`vectros identity`** — manage tenant-wide principals (users, orgs, clients, and namespaces you register).
13
14
  - **`vectros role`** — define reusable, context-scoped access rules.
14
15
  - **`vectros access`** — bind principals to contexts, and inspect what a binding grants.
15
16
  - **`vectros key`** — issue, list, revoke, and rotate scoped API keys (`ssk_*`).
16
17
  - **`vectros keyring`** — hold several identities on one machine, pick which is active, and let other tools resolve keys from one place.
18
+ - **`vectros join`** — enroll a principal in a context and keep the resulting credential in the keyring, in one step.
17
19
 
18
20
  ```bash
19
21
  npx -y @vectros-ai/cli bootstrap # provision a credential
@@ -144,6 +146,26 @@ vectros context destroy my-app # irreversible — asks you to r
144
146
  All four accept `--json` for machine-readable output, plus the common auth
145
147
  flags (`--base-url`, `--token`).
146
148
 
149
+ ## `vectros issuers` — registered third-party IdP issuers
150
+
151
+ Read the third-party identity-provider registrations a context trusts for
152
+ [RFC 8693 token exchange](https://www.rfc-editor.org/rfc/rfc8693) — the
153
+ `issuer`/`jwksUri`/`audience` a blueprint's `issuers[]` entries register, plus
154
+ any self-signup policy attached to one. Read-only: registering an issuer is
155
+ done through a blueprint (`bootstrap`/`blueprint-test`), not this command.
156
+
157
+ ```bash
158
+ vectros issuers list --context my-app
159
+ vectros issuers get primary --context my-app
160
+ ```
161
+
162
+ | Subcommand | Flags | Notes |
163
+ |---|---|---|
164
+ | `list` | `--context <contextId>` (required), `--tenant test\|live` | Every issuer registered in that context. `--context` has no default — an issuer read is confined to the context you name; there's no tenant-wide "every issuer in every context" view. |
165
+ | `get <issuerId>` | `--context <contextId>` (required), `--tenant test\|live` | One issuer's full registration, including its self-signup policies. |
166
+
167
+ Both accept `--json`, plus the common auth flags (`--base-url`, `--token`).
168
+
147
169
  ## `vectros identity` — tenant-wide principals
148
170
 
149
171
  Manage the principals a context's access profiles and roles reference: users
@@ -158,6 +180,8 @@ vectros identity create --type org --external-id acme-inc --name "Acme Inc"
158
180
  vectros identity list --type user
159
181
  vectros identity get --type org --id <vectrosId>
160
182
  vectros identity delete --type org --id <vectrosId>
183
+ vectros identity invite --context my-app --role editor --email bob@example.com \
184
+ --accept-url https://my-app.example.com/accept
161
185
  ```
162
186
 
163
187
  | Subcommand | Flags | Notes |
@@ -169,8 +193,9 @@ vectros identity delete --type org --id <vectrosId>
169
193
  | `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
194
  | `get` | `--type <user\|namespace>` (required), `--id <vectrosId>` (required) | `--id` is the Vectros-assigned id from `create`/`list`, not the `--external-id`. |
171
195
  | `delete` | `--type <user\|namespace>` (required), `--id <vectrosId>` (required) | |
196
+ | `invite` | `--context <c>`, `--role <roleId>`, `--email <e>` (all required); `--first-name <n>`, `--accept-url <url>`, `--no-send-email` | Onboards a person into a context by email (`POST /v1/users/invite`) — no root key required, your `vectros login` session already carries enough scope. `--role` is an existing Role's own id (no separate lookup). `--accept-url` is required unless `--no-send-email` is given, in which case the response carries the raw `inviteToken`/`acceptLink` for you to deliver yourself. |
172
197
 
173
- `create`/`list`/`get` accept `--json`; `delete` does not.
198
+ `create`/`list`/`get`/`invite` accept `--json`; `delete` does not.
174
199
 
175
200
  ## `vectros role` — reusable, context-scoped access rules
176
201
 
@@ -194,7 +219,7 @@ vectros role delete --context my-app --role-id editor
194
219
  |---|---|---|
195
220
  | `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
221
  | `list` | `--context <c>` (required) | |
197
- | `get` | `--context <c>`, `--role-id <id>` (both required) | |
222
+ | `get` | `--context <c>`, `--role-id <id>` (both required) | Also shows the role's `assumable` grant (which values, per namespace, a holder of this role may become via `POST /v1/auth/token/assume`), if any is set. `role create` has no `--assumable` flag today — author it via a blueprint's `roleAssumable:` block or a direct API call. |
198
223
  | `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
224
 
200
225
  `create`/`list`/`get` accept `--json`; `delete` does not.
@@ -224,14 +249,42 @@ shares this grammar.
224
249
 
225
250
  | Subcommand | Flags | Notes |
226
251
  |---|---|---|
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. |
252
+ | `grant` | `--principal <p>`, `--context <c>` (required); exactly one of `--role <roleId>` (repeatable — 2+ compose additively) / `--actions <csv>`; `--scope <ns:value>` (repeatable), `--identity-overrides <json>` | Idempotent **upsert** — re-granting an existing binding updates it to the newly-declared shape (role(s)/actions/overrides), it does not no-op. Repeating `--role` composes: `--role case-handler --role hr-admin` grants each role's own clauses, concatenated in the order given — never merged, so each clause keeps meaning exactly what its own author wrote. `--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
253
  | `revoke` | `--principal <p>`, `--context <c>` (required) | |
229
254
  | `list` | Exactly one of `--context <c>` / `--principal <p>` | `--context` lists the context's members; `--principal` lists every context that principal is bound to. |
230
255
  | `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). |
256
+ | `explain` | `--principal <p>`, `--context <c>` (required) | Resolves the binding's clauses (following every referenced role and concatenating their clauses, if bound via one or more) 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
257
 
233
258
  `grant`/`list`/`get`/`explain` accept `--json`; `revoke` does not.
234
259
 
260
+ ## `vectros join` — enroll and keep the credential, in one step
261
+
262
+ `join` is a **composition verb**: everything it does already exists as a
263
+ discrete command (`access grant`, `key issue`, `keyring add`) — `join` runs
264
+ them in order against one context and records the outcome as a keyring
265
+ entry, so `vectros switch <alias>` (and the MCP server / recall hook) can act
266
+ as that principal from then on.
267
+
268
+ ```bash
269
+ vectros join my-app --role editor # enroll "me" via a role
270
+ vectros join my-app --role case-handler --role hr-admin # compose 2+ roles additively
271
+ vectros join my-app --actions records:cru,search:r --scope org:<orgId>
272
+ vectros join my-app --as usr_<id> --role editor --alias teammate
273
+ ```
274
+
275
+ | Flag | Notes |
276
+ |---|---|
277
+ | `--role <roleId>` | Repeatable — 2+ compose additively, same semantics as `access grant --role`. XOR with `--actions`. |
278
+ | `--actions <csv>` | Inline single-clause grant. XOR with `--role`. |
279
+ | `--as me\|usr_<id>` (default `me`) | Which principal to enroll — `me` resolves to your own signed-in user. |
280
+ | `--scope <ns:value>` | Repeatable. Places the member under a scope for this context (an identity override on the profile) so their key can WRITE that scope's data. |
281
+ | `--alias <name>` | Keyring entry name (defaults to the context id). |
282
+ | `--activate` / `--no-activate` | Force (or skip) making the freshly-stored key this machine's active identity. |
283
+
284
+ Re-joining an alias whose secret is still held locally is idempotent: it
285
+ re-grants (a no-op upsert) and re-activates without churning the key. Only
286
+ when the local secret is missing does it rotate to obtain a fresh one.
287
+
235
288
  ## `vectros key` — the scoped API key (`ssk_*`) lifecycle
236
289
 
237
290
  Mint, inspect, revoke, and rotate the scoped keys (`ssk_*`) that authenticate
@@ -368,9 +421,15 @@ already holds — those verbs can only disclose or redistribute access, never
368
421
  manufacture new reach. `profiles:c` (creating a profile for an arbitrary,
369
422
  caller-chosen principal name) still hard-rejects — **except** `profiles:c:self`,
370
423
  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.
424
+ time, never from anything the blueprint declares, and **except a bare
425
+ `profiles:c` on a clause that also declares the `member-lifecycle` capability**
426
+ (see Capabilities, below) — that combination is admitted because
427
+ `member-lifecycle` already grants unbounded principal-creation reach, so
428
+ gating bare `profiles:c` behind it adds no further technical bound but keeps
429
+ the same explicit, fail-closed opt-in signal every other admission here
430
+ requires. A literal-qualified `profiles:c:usr_<id>` naming another principal
431
+ still hard-rejects regardless of `member-lifecycle`, same as every other
432
+ control-plane scope.
374
433
 
375
434
  ### Capabilities (`granted_capabilities`) — a separate, equally-gated dimension
376
435
 
@@ -1,11 +1,11 @@
1
1
  # Third-party notices
2
2
 
3
3
  `@vectros-ai/cli` compiles the following third-party packages directly into its
4
- published binary (`dist/cli.js`). Their licenses require that these notices travel
5
- with the code, so they are reproduced in full below.
4
+ published bundle. Their licenses require that these notices travel with the code,
5
+ so they are reproduced in full below.
6
6
 
7
- This file is GENERATED by `pipelines/third_party_notices.mjs` from the `noExternal`
8
- lists in `packages/*/tsup.config.ts`. Do not edit it by hand — run
7
+ This file is GENERATED by `pipelines/third_party_notices.mjs` from `packages/cli/tsup.config.ts`'s
8
+ `noExternal` list. Do not edit it by hand — run
9
9
  `node pipelines/third_party_notices.mjs --write`. CI fails if it is stale, because a
10
10
  notices file that silently drifts is worse than none: it asserts a compliance that
11
11
  nobody checked.