@vectros-ai/cli 0.19.0 → 0.21.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 +150 -9
- package/CHANGELOG.md +339 -0
- package/README.md +155 -24
- package/dist/cli.js +8058 -4835
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +8058 -4835
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
package/AUTHORING.md
CHANGED
|
@@ -54,7 +54,11 @@ A name must be 2–50 chars: a lowercase letter, then lowercase letters / digits
|
|
|
54
54
|
`namespaces` and the `*` wildcard are **hard-rejected, no override**.
|
|
55
55
|
`profiles` is a narrower exception — `r`/`u`/`d` are admitted, and `c` only
|
|
56
56
|
when qualified `profiles:c:self` (create a profile for yourself; a bare
|
|
57
|
-
`profiles:c`, or one naming another principal, still hard-rejects).
|
|
57
|
+
`profiles:c`, or one naming another principal, still hard-rejects).
|
|
58
|
+
`scripts` is the other — `scripts:x:<script-name>` (execute one named
|
|
59
|
+
script) is admitted, and nothing else on that resource is: a bare
|
|
60
|
+
`scripts:x`, a wildcard `scripts:x:*`, and `scripts:c` (authoring) all
|
|
61
|
+
hard-reject. Any
|
|
58
62
|
requested `capabilities` are checked the same way, against their own,
|
|
59
63
|
separate allowlist — see "Capabilities" below. A forked or hand-written
|
|
60
64
|
blueprint gets the *identical* gates a bundled one does — it can never
|
|
@@ -69,6 +73,16 @@ A name must be 2–50 chars: a lowercase letter, then lowercase letters / digits
|
|
|
69
73
|
`dataScope: {}` when a shared/tenant-wide resource is what you intend
|
|
70
74
|
(warnings — they print but don't fail).
|
|
71
75
|
|
|
76
|
+
A schema may also declare `expectedScopeDims` — the ownership dimensions
|
|
77
|
+
its records are typically scoped by, e.g. `expectedScopeDims: [org,
|
|
78
|
+
client]`. Purely advisory (never sent to the platform, never runtime-
|
|
79
|
+
enforced): it lets the lint warn when a role/`accessProfile` clause grants
|
|
80
|
+
`r`/`u`/`d` on that typeName (via a type-qualified action like
|
|
81
|
+
`records:r:case`) but its `dataScope` names only SOME of the declared
|
|
82
|
+
dims — reads never require full-dimension coverage the way creates do, so
|
|
83
|
+
a clause naming one of two declared dims silently leaves the other
|
|
84
|
+
unconstrained. Omit the field entirely and this lint never fires.
|
|
85
|
+
|
|
72
86
|
`validate` accepts a file path **or** a bundled blueprint name.
|
|
73
87
|
|
|
74
88
|
### 3. `plan` — preview before you mint
|
|
@@ -196,8 +210,9 @@ accessProfile:
|
|
|
196
210
|
# capabilities: [member-lifecycle]
|
|
197
211
|
# A SEPARATE gated dimension from allowedActions — named platform effects a
|
|
198
212
|
# resource:ops verb can't express (e.g. creating/removing an identity in
|
|
199
|
-
# your tenant).
|
|
200
|
-
# is hard-rejected, same posture as the
|
|
213
|
+
# your tenant). 'member-lifecycle' and 'delegate-principal-stamp' are
|
|
214
|
+
# admitted today; every other name is hard-rejected, same posture as the
|
|
215
|
+
# scope gate. See "Capabilities" below.
|
|
201
216
|
#
|
|
202
217
|
# Instead of allowedActions (+ optional dataScope/capabilities), accessProfile
|
|
203
218
|
# may declare `roleIds: [<roleId>, ...]` — one or more roles declared below,
|
|
@@ -287,7 +302,7 @@ namespaces:
|
|
|
287
302
|
# Registered in the SAME bootstrap-token phase as issuers, before schemas.
|
|
288
303
|
# Owned by this blueprint's own contextId (above) by default. `tenantWide: true` opts
|
|
289
304
|
# out of that, but declaring it is not enough on its own: the applying credential must
|
|
290
|
-
# separately hold OWNER-only authority, and `bootstrap
|
|
305
|
+
# separately hold OWNER-only authority, and `vectros bootstrap` refuses to even request
|
|
291
306
|
# it without the explicit `--allow-tenant-wide-namespaces` flag — a blueprint cannot make
|
|
292
307
|
# this happen by itself. Registration is create-once, NOT idempotent server-side: a
|
|
293
308
|
# re-apply matching every declared field converges silently, but one that disagrees
|
|
@@ -438,6 +453,90 @@ so if you seed a referencing record, seed its target first. To query "which X
|
|
|
438
453
|
reference Y", add the reference field to `lookupFields` as an equality lookup
|
|
439
454
|
(there is no reverse-reference index on this surface).
|
|
440
455
|
|
|
456
|
+
### Scripts and triggers — code a record write runs
|
|
457
|
+
|
|
458
|
+
A blueprint can ship the code an event runs, and the rule that runs it. Two
|
|
459
|
+
top-level blocks, applied in-context and in this order (`scripts` first —
|
|
460
|
+
a rule's `scriptRef` is checked when the rule is *declared*, not when it fires,
|
|
461
|
+
so a rule naming a script with no versions is refused outright):
|
|
462
|
+
|
|
463
|
+
```yaml
|
|
464
|
+
schemas:
|
|
465
|
+
- typeName: intake
|
|
466
|
+
displayName: Intake
|
|
467
|
+
# A schema fires triggers only when it opts in. Default is false.
|
|
468
|
+
capabilities: { triggersEnabled: true }
|
|
469
|
+
fields:
|
|
470
|
+
- { fieldId: status, fieldType: string, inline: true } # `inline` = projectable
|
|
471
|
+
|
|
472
|
+
scripts:
|
|
473
|
+
notify-assignee:
|
|
474
|
+
source: |
|
|
475
|
+
export default function (input) {
|
|
476
|
+
return { notified: input.recordId };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
triggers:
|
|
480
|
+
on-intake-update:
|
|
481
|
+
firingSource: { schemaName: intake, event: UPDATE }
|
|
482
|
+
# A script this blueprint ships is always `latest` — a pin is rejected at authoring time.
|
|
483
|
+
scriptRef: { name: notify-assignee, version: latest }
|
|
484
|
+
fields: [status]
|
|
485
|
+
allowedActions: [records:r:intake, records:u:intake]
|
|
486
|
+
dataScope: { "scope:org": ["${{ input.scope.org }}"] }
|
|
487
|
+
manifest: [records.get, records.update]
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Four things to get right, because each is a refusal rather than a warning:
|
|
491
|
+
|
|
492
|
+
- **`fields` is required.** It names the record fields projected into the
|
|
493
|
+
script's `input.record` (and, on an `UPDATE`, `input.previous`). `fields: []`
|
|
494
|
+
is the explicit "project nothing" declaration — required rather than defaulted
|
|
495
|
+
so that projecting nothing is something you state. An entry may name only a
|
|
496
|
+
field the schema keeps **inline** (`inline: true`, `filterable: true`, or a
|
|
497
|
+
lookup field) and never one marked `sensitive`. `validate` checks this against
|
|
498
|
+
the schema in the same blueprint, so you see it before an apply starts.
|
|
499
|
+
- **A rule that declares any `fields` must be able to read what it fires on** —
|
|
500
|
+
its grant needs a `records:r` clause covering the firing schema's type, and
|
|
501
|
+
that clause may not carry a *literal* `dataScope` constraint. A
|
|
502
|
+
`${{ input.* }}` placeholder is fine, because it resolves to the firing
|
|
503
|
+
record's own value.
|
|
504
|
+
- **The schema must opt in** with `capabilities.triggersEnabled: true`, or the
|
|
505
|
+
rule is refused at `plan` time rather than created and silently never fired.
|
|
506
|
+
- **The grant is live authority.** A trigger's `allowedActions`/`dataScope`, or
|
|
507
|
+
the roles its `roleIds` composes, are what the script actually executes under
|
|
508
|
+
— author them as narrowly as any other credential's. `${{ self.* }}`,
|
|
509
|
+
`${{ under.self.* }}` and `${{ member.* }}` are rejected inside a trigger's
|
|
510
|
+
grant: they resolve against the trigger principal's identity, not yours.
|
|
511
|
+
|
|
512
|
+
**Shipping a script does not make it runnable.** A `scripts:` entry provisions
|
|
513
|
+
the source; the two consumers reach it differently. A **trigger** names it in
|
|
514
|
+
`scriptRef` and runs it under the trigger's own grant — nothing further needed.
|
|
515
|
+
**Your app calling `POST /v1/scripts/execute` needs an explicit grant**, and the
|
|
516
|
+
role that calls it must carry `scripts:x:<script-name>`, qualified to that one
|
|
517
|
+
name:
|
|
518
|
+
|
|
519
|
+
```yaml
|
|
520
|
+
roles:
|
|
521
|
+
case-handler:
|
|
522
|
+
- allowedActions: [scripts:x:create-case]
|
|
523
|
+
dataScope:
|
|
524
|
+
scope:org: ['${{ member.scope.org }}']
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Omit it and the call is a 403 that cannot tell you the grant is what's missing.
|
|
528
|
+
See *The scope gate* for why the bare and wildcard forms are refused.
|
|
529
|
+
|
|
530
|
+
**Triggers are the one block an apply reconciles by absence.** Remove an entry
|
|
531
|
+
and re-apply, and its rule and access profile are deleted, so a trigger you
|
|
532
|
+
removed cannot keep acting. Its service principal is kept, so re-adding the same
|
|
533
|
+
trigger reattaches to one identity. Removing a `scripts:` entry deletes nothing
|
|
534
|
+
— see *The idempotency contract*, below, for why.
|
|
535
|
+
|
|
536
|
+
Full field grammar, including the byte caps on `source` and
|
|
537
|
+
`declaredInputContract` and the `manifest` verb vocabulary:
|
|
538
|
+
[`@vectros-ai/blueprints`](https://www.npmjs.com/package/@vectros-ai/blueprints).
|
|
539
|
+
|
|
441
540
|
### The scope gate — what you can and can't request
|
|
442
541
|
|
|
443
542
|
The gate allows only **data-plane** resources:
|
|
@@ -446,20 +545,44 @@ The gate allows only **data-plane** resources:
|
|
|
446
545
|
registered — ownership-gated the same way as `records`; it grants no
|
|
447
546
|
authority over the namespace *registry* itself, which stays root-key-only).
|
|
448
547
|
Operations are `c|r|u|d`, combinable (`records:cru`), with a per-resource
|
|
449
|
-
wildcard (`records:*`). Anything else — control-plane resources
|
|
548
|
+
wildcard (`records:*`). Anything else — control-plane resources and the bare
|
|
450
549
|
`*` — is rejected, and there is **intentionally no override flag**. This is
|
|
451
550
|
*why* it's safe to apply a blueprint you didn't write: the binary is the trust
|
|
452
551
|
boundary, not the file.
|
|
453
552
|
|
|
553
|
+
**One exception on `scripts`, because a blueprint that ships a script has to be
|
|
554
|
+
able to grant permission to run it.** `scripts:x:<script-name>` is admitted.
|
|
555
|
+
Everything else on that resource is not:
|
|
556
|
+
|
|
557
|
+
| Grant | Verdict |
|
|
558
|
+
|---|---|
|
|
559
|
+
| `scripts:x:create-case` | **admitted** — execute that one script |
|
|
560
|
+
| `scripts:x` (bare) | rejected — it would mean "run every script in this context, including ones added later that your reviewer never saw" |
|
|
561
|
+
| `scripts:x:*` | rejected — **there is no wildcard form.** The qualifier is compared to the script name exactly, so a `*` matches nothing: the grant would store cleanly and then refuse every execution, with no error able to say why |
|
|
562
|
+
| `scripts:c` (authoring) | rejected — creating a script is a design-time admin surface, and a blueprint ships its source rather than granting the ability to write one |
|
|
563
|
+
| `triggers:*` | rejected — unchanged |
|
|
564
|
+
|
|
565
|
+
Execution consults no data scope, so this widens **what can be composed**, never
|
|
566
|
+
**what data is reachable**: every write the script makes is still checked, one
|
|
567
|
+
by one, against the caller's own permissions. A credential holding only
|
|
568
|
+
`scripts:x:<name>` runs the script and is refused on its first write.
|
|
569
|
+
|
|
570
|
+
If your blueprint declares a `scripts:` block, `validate` also checks each
|
|
571
|
+
`scripts:x:<name>` grant against the names you actually ship, and refuses a
|
|
572
|
+
grant that names something else. That check exists here because it cannot exist
|
|
573
|
+
anywhere else: the platform stores a misspelled grant without complaint, and
|
|
574
|
+
the resulting 403 at execution time deliberately cannot tell you the name was
|
|
575
|
+
wrong.
|
|
576
|
+
|
|
454
577
|
### Capabilities (`granted_capabilities`) — a separate, equally-gated dimension
|
|
455
578
|
|
|
456
579
|
An `accessProfile` or role clause may also declare `capabilities: [...]` —
|
|
457
580
|
named platform effects (like creating or removing an identity in your tenant)
|
|
458
581
|
that a `resource:ops` scope token can't express. This rides its own field,
|
|
459
|
-
separate from `allowedActions`, and has its own allowlist:
|
|
460
|
-
`
|
|
461
|
-
including ones the platform's own registry recognizes — is refused,
|
|
462
|
-
fail-closed, deliberately-narrow posture as the scope gate above. `validate`,
|
|
582
|
+
separate from `allowedActions`, and has its own allowlist: **`member-lifecycle`
|
|
583
|
+
and `delegate-principal-stamp` are admitted today.** Every other capability
|
|
584
|
+
name — including ones the platform's own registry recognizes — is refused,
|
|
585
|
+
the same fail-closed, deliberately-narrow posture as the scope gate above. `validate`,
|
|
463
586
|
`plan`, `blueprint-test` and `bootstrap` all report a rejected capability the
|
|
464
587
|
same way they report a rejected scope: nothing mints, and the CLI exits
|
|
465
588
|
non-zero.
|
|
@@ -494,6 +617,24 @@ from a blueprint's `externalId` colliding with an unrelated identity, so it asks
|
|
|
494
617
|
rather than guessing either way. `--rotate` is exempt only when the local
|
|
495
618
|
record already names the same principal being rotated.
|
|
496
619
|
|
|
620
|
+
**A second qualification, and a different KIND of one: a `scripts:` entry is not
|
|
621
|
+
idempotent server-side.** Every push of a script name stores a genuinely new,
|
|
622
|
+
immutable version — there is no update, and no create-or-return. Convergence
|
|
623
|
+
here is the loader's doing, not the platform's: it reads the name's current
|
|
624
|
+
version and compares the stored `source` and `declaredInputContract` against
|
|
625
|
+
what the blueprint declares, and pushes only on a real difference. So a re-apply
|
|
626
|
+
of an unedited blueprint writes nothing, and editing `source` stores exactly one
|
|
627
|
+
new version, which every `version: latest` rule then picks up. The apply summary
|
|
628
|
+
reports which of the two happened per script.
|
|
629
|
+
|
|
630
|
+
Two consequences worth planning for. A concurrent apply of the same blueprint
|
|
631
|
+
can collide on one script name; the loader re-reads and retries once, so two
|
|
632
|
+
runners applying identical content converge rather than storing it twice, but a
|
|
633
|
+
sustained collision fails the apply and asks you to re-run. And **removing a
|
|
634
|
+
`scripts:` entry does not delete anything** — every stored version stays, because
|
|
635
|
+
a script carries no record of which blueprint pushed it. Remove one deliberately
|
|
636
|
+
with `vectros scripts delete`.
|
|
637
|
+
|
|
497
638
|
## Distribution
|
|
498
639
|
|
|
499
640
|
- **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.21.0 — 2026-09-07
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`vectros bootstrap --blueprint` provisions a blueprint's `scripts` block**, before the triggers
|
|
11
|
+
that reference them. A trigger's `scriptRef` is checked when the rule is DECLARED, not when it
|
|
12
|
+
fires, so the ordering is what makes a blueprint shipping both work at all — the other way round,
|
|
13
|
+
the trigger is refused for naming a script that does not exist yet, after the apply has already
|
|
14
|
+
minted that trigger's service principal and access profile.
|
|
15
|
+
|
|
16
|
+
**A re-apply pushes nothing when the source has not changed.** Pushing a script is not
|
|
17
|
+
idempotent — every push stores a new, server-numbered version — so an apply reads the name's
|
|
18
|
+
current version first and stores a new one only when `source` or `declaredInputContract` actually
|
|
19
|
+
differs. Without that, every apply of an unedited blueprint would mint a version and silently
|
|
20
|
+
re-point every `version: latest` rule at code identical to what it was already running. Editing
|
|
21
|
+
the source pushes exactly one new version, which `latest` rules then pick up. A concurrent push
|
|
22
|
+
from another apply is re-read rather than blindly retried, so two runners applying the same
|
|
23
|
+
blueprint converge instead of storing the same source twice.
|
|
24
|
+
|
|
25
|
+
**Removing a `scripts` entry does NOT delete the script** — the deliberate asymmetry with
|
|
26
|
+
triggers, which an apply does reconcile by absence. A script carries no record of which blueprint
|
|
27
|
+
pushed it, so an apply cannot tell one it created from one pushed by hand; the delete would be
|
|
28
|
+
unrecoverable, since the stored source is the only copy, and would break any trigger still
|
|
29
|
+
referencing it. An unreferenced script version is inert — it fires nothing and grants nothing — so
|
|
30
|
+
it is left in place. `blueprint plan` states this every time, including when the blueprint declares
|
|
31
|
+
no scripts at all, which is exactly the plan you see after removing the last one.
|
|
32
|
+
|
|
33
|
+
- **`vectros scripts <push|list|get|delete>`** — manage script objects outside a full apply, which
|
|
34
|
+
previously had no route at all short of raw HTTP.
|
|
35
|
+
|
|
36
|
+
- `scripts push <name> --source-file <path> --context <ctx>` stores a new version and reports its
|
|
37
|
+
number. It always pushes, matching the platform: `--if-changed` opts into the convergent
|
|
38
|
+
behaviour instead, which is what a CI job re-running the same push wants. Name grammar and both
|
|
39
|
+
byte caps are checked locally, so a bad push exits 2 naming the field instead of sending 300 KB
|
|
40
|
+
to earn a rejection. `--input-contract` / `--input-contract-file` supply the optional contract.
|
|
41
|
+
- `scripts list --context <ctx> [--name <name>]` — every version of one name, or all scripts. The
|
|
42
|
+
human view shows a byte count rather than the source, so listing a much-edited script does not
|
|
43
|
+
dump hundreds of kilobytes into a terminal; `--json` carries the source for a machine consumer.
|
|
44
|
+
- `scripts get <id> --context <ctx>` shows one version including its full source.
|
|
45
|
+
- `scripts delete <id> --context <ctx>` removes ONE version, leaving other versions of the name
|
|
46
|
+
alone — and warns that nothing checked whether a trigger still referenced it. That warning is
|
|
47
|
+
the point: a rule pinned to the deleted version fails on every firing, while a rule on `latest`
|
|
48
|
+
silently falls back to the previous version and keeps running older code.
|
|
49
|
+
|
|
50
|
+
There is no `scripts update`: a stored version is immutable and the update route always rejects.
|
|
51
|
+
`--context` is required on all four, since a trigger resolves its `scriptRef` inside its own
|
|
52
|
+
context and a defaulted one would push where nobody asked.
|
|
53
|
+
|
|
54
|
+
- **`blueprint-test` tears down the script versions a run pushed**, by id, after the trigger teardown
|
|
55
|
+
so a rule never briefly outlives the script it names. A version the run did not push — the source
|
|
56
|
+
already matched what the blueprint declares — is reported as skipped and left alone.
|
|
57
|
+
|
|
58
|
+
- **`blueprint-test` tears down trigger rules it created.** The harness deletes what a run
|
|
59
|
+
provisions, and triggers were the one resource it could not reach — so testing a blueprint with a
|
|
60
|
+
`triggers:` block left the rule behind, and because a schema will not delete while a rule fires
|
|
61
|
+
off it, the leak kept the schema alive and the next run failed against it. Deletion is
|
|
62
|
+
created-only and requires TWO independent factors, mirroring what `bootstrap` already does when it
|
|
63
|
+
reconciles a trigger away: the rule must be absent from the pre-run snapshot AND carry this
|
|
64
|
+
blueprint's own `provisionedBy` marker. A rule that pre-existed, one another blueprint provisioned,
|
|
65
|
+
one created directly through the API, or any rule at all when the snapshot could not be read, is
|
|
66
|
+
reported and left in place. Each rule's access profile goes with it — that is where its declared
|
|
67
|
+
authority lives — while its service principal is kept, so re-adding the same trigger reattaches to
|
|
68
|
+
one identity.
|
|
69
|
+
|
|
70
|
+
- **Blueprint trigger `fields` and schema field `inline` are provisioned.** A trigger rule declares
|
|
71
|
+
which record fields it projects into its script's `input.record` (and, on an `UPDATE` firing,
|
|
72
|
+
`input.previous`); `vectros bootstrap --blueprint` forwards that declaration, and forwards a
|
|
73
|
+
schema field's `inline` flag, which is what makes a field projectable at all. `blueprint plan`
|
|
74
|
+
now names the projected fields per trigger — this is the record data that leaves storage on every
|
|
75
|
+
firing, so an operator approving a plan should see it — and spells out a trigger that projects
|
|
76
|
+
nothing rather than omitting the line. Requires `@vectros-ai/blueprints` ≥ 0.18.0.
|
|
77
|
+
|
|
78
|
+
- **`assume` shows the identity you actually got.** `POST /v1/auth/token/assume` now returns a
|
|
79
|
+
`resolvedScope` alongside the re-minted token — the same plaintext data baked into the token's own
|
|
80
|
+
compressed `scope` claim, resolved server-side — so the human view reports the resolved `identity`
|
|
81
|
+
and `allowedActions` instead of leaving you to decode the token. Switching `identity` is the whole
|
|
82
|
+
point of assuming, so this is the direct answer to the question the command was run to ask. The
|
|
83
|
+
rows are omitted entirely against a backend that returns no `resolvedScope`, rather than rendered
|
|
84
|
+
as empty: on this command an em dash would read as "you assumed nothing". `--json` already carried
|
|
85
|
+
the field verbatim.
|
|
86
|
+
|
|
87
|
+
- **`role create --assignable-role <roleId>`** (repeatable) — restrict which named roles the new
|
|
88
|
+
role's clause may compose into a delegated AccessProfile. Omit for no restriction; the platform
|
|
89
|
+
rejects an empty list, so the flag has no "compose nothing" spelling and omitting it leaves the
|
|
90
|
+
clause byte-identical to one authored before this release. Validated locally against the platform's
|
|
91
|
+
roleId grammar, 20-entry cap and no-duplicates rule, so a bad list exits 2 naming the offending
|
|
92
|
+
entry instead of round-tripping to a `400`.
|
|
93
|
+
|
|
94
|
+
- **`access explain` reports a clause's `assignableRoles`**, in both the human and `--json` views,
|
|
95
|
+
for clauses that carry one. Reported alongside the access matrix rather than folded into it: the
|
|
96
|
+
restriction bounds role composition, not data reach, so narrowing the matrix on it would be wrong.
|
|
97
|
+
A blueprint can declare the same restriction via `assignableRoles` on a role or `accessProfile`
|
|
98
|
+
clause (`@vectros-ai/blueprints` ≥ 0.18.0), which `vectros bootstrap --blueprint` forwards.
|
|
99
|
+
|
|
100
|
+
- **`issuers register` / `issuers update` accept `--captured-claim <name>`** (repeatable) — additional
|
|
101
|
+
OIDC claim names, beyond the email claim, to capture from that issuer's tokens on every successful
|
|
102
|
+
token exchange and store as your tenant's golden identity-provider-asserted copy for the signed-in
|
|
103
|
+
user. `issuers update` also gains `--clear-captured-claims`, which sends an explicit empty list
|
|
104
|
+
(capture nothing beyond email) — distinct from omitting the flag, which leaves the stored value
|
|
105
|
+
unchanged. Mutually exclusive with `--captured-claim`. The list is bounds-checked locally (20
|
|
106
|
+
entries, 64 characters each, no duplicates), so a bad list exits 2 naming the offending entry
|
|
107
|
+
rather than round-tripping to a `400`. `issuers get` renders the captured names, and `issuers
|
|
108
|
+
list` gains a `captured claims` column.
|
|
109
|
+
A blueprint can declare the same thing via `issuers[].capturedClaims` (`@vectros-ai/blueprints`
|
|
110
|
+
≥ 0.18.0), which `vectros bootstrap --blueprint` now forwards and reconciles when re-applied.
|
|
111
|
+
|
|
112
|
+
- **Blueprint `identityProjectionClaims` is applied.** A blueprint declaring the new top-level block
|
|
113
|
+
(`@vectros-ai/blueprints` ≥ 0.18.0) has it set on the app context during the bootstrap phase,
|
|
114
|
+
alongside the context create — that is the only phase whose credential carries the platform
|
|
115
|
+
provisioning capability the field requires. Declaring it selects which captured identity-provider
|
|
116
|
+
claim names are projected, read-only, onto access profiles in that context. A blueprint that does
|
|
117
|
+
not declare it sends nothing, leaving any existing declaration untouched. **It applies only when this
|
|
118
|
+
blueprint CREATES the context**: the platform sets the declaration on a true create, so a context
|
|
119
|
+
created separately beforehand never receives it, and re-applying against an existing context does
|
|
120
|
+
not change it in either direction — edit an existing context's
|
|
121
|
+
declaration through the platform API.
|
|
122
|
+
|
|
123
|
+
- **Blueprint `triggers:` provisioning.** `vectros bootstrap --blueprint` now provisions each declared trigger:
|
|
124
|
+
a dedicated service principal, a context-scoped AccessProfile carrying the trigger's declared
|
|
125
|
+
grant, and the trigger rule itself. A trigger's real declared grant is attached only after the
|
|
126
|
+
trigger rule is confirmed created — a failed apply never leaves a principal holding meaningful
|
|
127
|
+
authority with nothing referencing it. `blueprint plan` renders each trigger's firing schema/
|
|
128
|
+
event, script reference, and manifest (or `(none)` when absent) alongside the principal/profile
|
|
129
|
+
steps it provisions. Requires `@vectros-ai/blueprints` ≥ 0.18.0. A schema's
|
|
130
|
+
`capabilities.triggersEnabled` is now applied along with `auditHistory` — it is the opt-in a schema
|
|
131
|
+
must declare before a trigger may fire on its records, and a blueprint declaring a trigger against
|
|
132
|
+
a schema without it is refused at `plan` time rather than creating a rule that never fires.
|
|
133
|
+
- **Removing a trigger from a blueprint now de-provisions it.** Triggers are the one resource
|
|
134
|
+
`vectros bootstrap --blueprint` reconciles by *absence* as well as presence: a trigger this blueprint previously
|
|
135
|
+
provisioned and no longer declares has its rule and its access profile deleted, so a trigger you
|
|
136
|
+
removed cannot keep acting under a grant that outlived it. Its service principal is kept, so
|
|
137
|
+
re-adding the same trigger later reattaches to the same identity. Triggers created outside this
|
|
138
|
+
blueprint — by hand, or by a different blueprint sharing the app context — are never touched.
|
|
139
|
+
Ownership is established from **two** independent pieces of evidence, both of which must agree: the
|
|
140
|
+
trigger rule's own `provisionedBy` marker (which `vectros bootstrap --blueprint` stamps, and which the platform refuses to
|
|
141
|
+
re-point once set) and the service principal the rule names. Neither alone is sufficient, because
|
|
142
|
+
each is writable with a different scope; requiring both means no single credential can make one
|
|
143
|
+
blueprint's apply delete another principal's grant. A trigger whose two factors disagree, or whose
|
|
144
|
+
principal cannot be read, is left in place and reported rather than deleted. Each de-provisioned
|
|
145
|
+
trigger is named individually in the apply output, and `blueprint plan` states the behaviour in
|
|
146
|
+
its legend. No other resource is ever deleted by an apply.
|
|
147
|
+
|
|
148
|
+
### Changed
|
|
149
|
+
|
|
150
|
+
- **A refusal caused by suspension is reported as one.** `POST
|
|
151
|
+
/v1/admin/keys/scoped` now answers with a `409` when the access profile or the user it names is
|
|
152
|
+
suspended, and the CLI renders that distinctly: no credential — root included — mints against a
|
|
153
|
+
suspended profile, so the remedy the `403` text points at (re-issue a broader credential) is
|
|
154
|
+
actively wrong here. Reactivate the profile, then retry. The developer scoped-token route answers
|
|
155
|
+
every refusal with one uniform `404`, and a suspended member is one of them; since every
|
|
156
|
+
credential-bearing verb mints through that route, the CLI names suspension among the causes rather
|
|
157
|
+
than reporting only that the context was not found.
|
|
158
|
+
|
|
159
|
+
- **The scope gate names `scripts` and `triggers` as control-plane.** Both were already refused for
|
|
160
|
+
a blueprint-issued role — they are not data-plane resources — and the refusal now says so, rather
|
|
161
|
+
than reading as an unrecognized resource that the allowlist might be widened for. Script authoring
|
|
162
|
+
and trigger declaration are deliberately admin-shaped surfaces. No scope that passed the gate
|
|
163
|
+
before is refused.
|
|
164
|
+
|
|
165
|
+
- **Two blueprints may not declare a trigger of the same name in one app context.** The second apply
|
|
166
|
+
is rejected with a `400`. A trigger fires real code under a real grant, so one blueprint quietly
|
|
167
|
+
re-pointing another's rule is not something an apply does. Rename one of the triggers, or move it
|
|
168
|
+
to its own app context.
|
|
169
|
+
|
|
170
|
+
### Fixed
|
|
171
|
+
|
|
172
|
+
- **`role` and `access` accept `--tenant`, so a blueprint applied to the test tenant can be verified.**
|
|
173
|
+
Every sibling verb (`context`, `issuers`, `scripts`, `key`, `identity`) already took the flag; these
|
|
174
|
+
two did not, and they are the ones that read back roles and access bindings. Omitting it fell back
|
|
175
|
+
to the live tenant and failed with *"Context not found"* — a message naming two possible causes,
|
|
176
|
+
neither of which was the real one. A blueprint apply's most security-relevant output was the one
|
|
177
|
+
part that could not be read back from the tenant it was written to.
|
|
178
|
+
- **A failed token mint no longer aborts the process.** `role` and `access` now set the exit code and
|
|
179
|
+
return rather than exiting mid-flight, which on Windows could trip a native assertion in Node's HTTP
|
|
180
|
+
stack and replace the command's real exit code with `127` — the code a shell uses for "command not
|
|
181
|
+
found". Anything branching on exit status read a refused request as a missing binary. If you script
|
|
182
|
+
against another verb and see `127` from a request that clearly reached the server, that is this bug
|
|
183
|
+
and not a missing binary; re-run and read the printed error, which is accurate.
|
|
184
|
+
|
|
185
|
+
- **A blueprint can now grant `scripts:x:<script-name>`, so a blueprint that ships a script can also
|
|
186
|
+
grant permission to run it.** The scope gate classified `scripts` as control-plane by resource, so
|
|
187
|
+
the grant was refused outright and a blueprint could ship a script's source with no way to
|
|
188
|
+
authorize its execution. That reasoning is correct for `scripts:c` — authoring is a design-time,
|
|
189
|
+
admin-shaped surface, and it is unchanged — but it does not carry to execution, which consults no
|
|
190
|
+
data scope at all and whose writes are still checked one by one against the caller's own
|
|
191
|
+
permissions. The grant widens what can be composed, never what data is reachable.
|
|
192
|
+
|
|
193
|
+
Qualified to a script name only. A bare `scripts:x` is still rejected: execution gates on the verb
|
|
194
|
+
and the name, so an unqualified grant would mean "may run every script in this context, including
|
|
195
|
+
ones added later". `scripts:*` and `scripts:x:*` are rejected too — **there is no wildcard form.**
|
|
196
|
+
The qualifier is compared to the script name exactly, so a `*` matches nothing: such a grant would
|
|
197
|
+
store cleanly and then refuse every execution, with no error able to explain why.
|
|
198
|
+
- **`blueprint validate`/`plan` now catch a misspelled script grant.** If your blueprint declares a
|
|
199
|
+
`scripts:` block, each `scripts:x:<name>` grant is checked against the names you actually ship, and
|
|
200
|
+
the refusal names the role and lists what is shipped. Nothing downstream can catch this: the
|
|
201
|
+
platform stores a wrong name without complaint and the execution-time 403 deliberately cannot say
|
|
202
|
+
the name is the problem.
|
|
203
|
+
|
|
204
|
+
## 0.20.0 — 2026-08-27
|
|
205
|
+
|
|
206
|
+
### Added
|
|
207
|
+
|
|
208
|
+
- **The capability gate now admits `delegate-principal-stamp`**, alongside the
|
|
209
|
+
existing `member-lifecycle`. Grantable on an `accessProfile` or role clause's
|
|
210
|
+
`capabilities: [...]`, same as before — a blueprint can now let an admin
|
|
211
|
+
role create a membership-shaped record on behalf of a known third-party
|
|
212
|
+
user, once the clause authorizing the write also explicitly names `userId`
|
|
213
|
+
in its own `dataScope` (the capability itself may be granted on a
|
|
214
|
+
different clause of the same role, e.g. via a separately-composed
|
|
215
|
+
`roleIds` entry — only the write's own verb + `userId` admission must be
|
|
216
|
+
co-located). Bounded by the SAME app context and record type the clause's
|
|
217
|
+
`dataScope` already confines the write to — unlike `delegate-mint`/
|
|
218
|
+
`forensic-read`/`context-directory-read`, this is not tenant-wide, and
|
|
219
|
+
(like `member-lifecycle`) it is self-signup-eligible.
|
|
220
|
+
- **`vectros identity list-all`** — lists every user in the tenant, for the one
|
|
221
|
+
recovery case `identity list` can't reach: a user created but never granted
|
|
222
|
+
access anywhere (the gap between creating a principal and binding it to a
|
|
223
|
+
context). `identity list` is scoped to the caller's own context by design —
|
|
224
|
+
a user with no access grant at all is invisible there no matter how many
|
|
225
|
+
times a create is retried, since a retry with the same `--external-id` just
|
|
226
|
+
collides with the same invisible row. `list-all` authenticates with your own
|
|
227
|
+
login session (an account owner) instead of a context-scoped credential, so
|
|
228
|
+
it can see every user regardless of context. Supports `--external-id`,
|
|
229
|
+
`--tenant`, `--limit`, and `--json`, matching `identity list`'s conventions.
|
|
230
|
+
- **`vectros blueprint validate`/`plan`: new advisory lint —
|
|
231
|
+
`partial-datascope-dimension-coverage`** (needs `@vectros-ai/blueprints` ≥
|
|
232
|
+
0.17.0). Warns when a role/`accessProfile` clause grants `r`/`u`/`d` on a
|
|
233
|
+
typeName (e.g. `records:r:case`) whose schema declares
|
|
234
|
+
`expectedScopeDims`, but the clause's `dataScope` names only SOME of those
|
|
235
|
+
dims — the missing dimension(s) are silently unconstrained for that grant,
|
|
236
|
+
since reads (unlike creates) never require full-dimension `dataScope`
|
|
237
|
+
coverage. Opt-in: a schema with no `expectedScopeDims` declared never
|
|
238
|
+
trips it, and it never fires on an untyped (bare `records:r`) grant.
|
|
239
|
+
Never runtime-enforced — pure authoring-time visibility into a choice
|
|
240
|
+
that's easy to make by omission rather than intent.
|
|
241
|
+
- **`bootstrap`/`blueprint-test` now catch a FRESH (first-apply) seed index
|
|
242
|
+
failure, not just a re-apply's.** A seed create's response always carries
|
|
243
|
+
`PENDING_INDEX` on a first apply (indexing runs async, after the
|
|
244
|
+
response), so the existing `seedIndexFailures` summary previously only
|
|
245
|
+
ever saw a terminal `FAILED` on a later idempotent re-apply — a
|
|
246
|
+
newly-bootstrapped demo whose seeds failed to index looked silently empty
|
|
247
|
+
in search/RAG until the operator happened to re-run bootstrap. Every seed
|
|
248
|
+
still `PENDING_INDEX` after seeding is now re-checked with a bounded,
|
|
249
|
+
concurrent, best-effort poll (default: 3 attempts, 500ms apart, ≤ ~1.5s
|
|
250
|
+
added to the happy path) and folded into the same run-summary warning if
|
|
251
|
+
it settles to `FAILED` within the bound. Deliberately NOT a guarantee — a
|
|
252
|
+
seed still `PENDING_INDEX` when the bound is exhausted stays silent, so a
|
|
253
|
+
genuinely slow index (reconcile/quarantine can stall for minutes) never
|
|
254
|
+
turns a one-shot provisioning command into an unbounded wait.
|
|
255
|
+
- **`vectros issuers` gains `register`/`update`/`delete`** — previously
|
|
256
|
+
read-only (`list`/`get`). Closes the gap where adding a second IdP, or
|
|
257
|
+
editing one issuer's `selfSignupPolicies` after initial setup, required
|
|
258
|
+
hand-authoring a blueprint `issuers:` block and re-running a full apply.
|
|
259
|
+
`register` is idempotent by issuer id (a repeat returns the existing
|
|
260
|
+
registration unchanged, not an error); `update` only ever sends the safe,
|
|
261
|
+
mutable fields (`subClaim`/`emailClaim`/`userinfoUri`/`status`/
|
|
262
|
+
`selfSignupPolicies`) — trust-anchor fields stay immutable via this route,
|
|
263
|
+
matching the platform; `delete` uses the same re-type-the-id confirmation
|
|
264
|
+
as `context destroy` (`--force` for non-interactive use), and is refused
|
|
265
|
+
server-side while any user is bound through the issuer. All three accept
|
|
266
|
+
`--tenant test|live` (like their `list`/`get` siblings) so a context
|
|
267
|
+
living in the `test` tenant is reachable, not just `live`.
|
|
268
|
+
- **New `vectros namespace` command group: `register`/`list`/`get`.**
|
|
269
|
+
Registering a brand-new scope namespace (e.g. a `practice`/`team` entity
|
|
270
|
+
dimension) was blueprint-apply-only before this; there was also no way to
|
|
271
|
+
discover which namespaces already exist without already knowing their
|
|
272
|
+
names. `register --entity-backed` is what makes a namespace a real
|
|
273
|
+
identity-entity dimension that `identity create --type <name>` can create
|
|
274
|
+
entities in. No `update`/`delete` verbs — both are gated to a genuine root
|
|
275
|
+
API key on the platform side with no provisioning-capability door, so
|
|
276
|
+
neither could ever succeed for any credential the CLI holds (documented in
|
|
277
|
+
the command's own module header).
|
|
278
|
+
- **`role create` gains `--assumable <json>`** — a role-level `vectros
|
|
279
|
+
assume` entitlement grant (JSON map of `scope:<namespace>` → allowed
|
|
280
|
+
values), previously blueprint-`roleAssumable:`-only. Independent of
|
|
281
|
+
`--data-scope`: holding broad data-scope reach in a namespace does not by
|
|
282
|
+
itself grant assuming any value in it.
|
|
283
|
+
- **New `vectros assume --context <c> --scope <ns:value>...`** — re-mints
|
|
284
|
+
the caller's own verb token with one or more `identity.<namespace>` values
|
|
285
|
+
switched (`POST /v1/auth/token/assume`), for a caller whose role
|
|
286
|
+
explicitly grants assuming those values. The CLI's first standalone
|
|
287
|
+
command reaching this endpoint; entitlement is checked live against the
|
|
288
|
+
caller's current roles, never a copy frozen into a token at mint time. No
|
|
289
|
+
`--to-keyring` — the response is an ephemeral bearer token with the same
|
|
290
|
+
remaining lifetime as the verb token this command itself mints to make the
|
|
291
|
+
call, not a durable secret.
|
|
292
|
+
|
|
293
|
+
### Changed
|
|
294
|
+
|
|
295
|
+
- **`runLoader`'s `provisioned` is now REQUIRED — the self-provisioning
|
|
296
|
+
fallback is gone.** Internal hardening only, no CLI-facing behavior
|
|
297
|
+
change: every real caller (`vectros bootstrap`, `blueprint-test`) already
|
|
298
|
+
always ran `provisionContext` first and passed the result in, so the
|
|
299
|
+
fallback branch was dead in production. Removing it makes the
|
|
300
|
+
`provisionContext` → (apply-time identity resolution) → `runLoader`
|
|
301
|
+
ordering structurally required instead of convention-enforced — a future
|
|
302
|
+
caller can no longer reproduce the ordering bug the original fix
|
|
303
|
+
closed by skipping the split. `allowTenantWideNamespaces` and
|
|
304
|
+
`confirmExistingPrincipal` moved with it: both now live only on
|
|
305
|
+
`provisionContext`'s own input, where their actual checks have run since
|
|
306
|
+
that original fix — `runLoader`'s own copies of both parameters were already dead
|
|
307
|
+
plumbing before this change.
|
|
308
|
+
- **Apply-time identity resolution now REQUIRES `provisionContext`'s result.**
|
|
309
|
+
Internal hardening only, no CLI-facing behavior change. Closes the other
|
|
310
|
+
half of the ordering gap above: `runLoader` already required
|
|
311
|
+
`provisionContext`'s output, but the step between them — resolving a
|
|
312
|
+
blueprint's declared `identities:` — took a bare client with nothing tying
|
|
313
|
+
it to `provisionContext` having run first, so a caller could still
|
|
314
|
+
reproduce the original ordering bug (a non-`user` identity in a namespace
|
|
315
|
+
the same blueprint also registers racing that registration) with no
|
|
316
|
+
compiler signal. It now derives which client to use internally instead of
|
|
317
|
+
each caller doing so independently.
|
|
318
|
+
|
|
319
|
+
### Fixed
|
|
320
|
+
|
|
321
|
+
- **`bootstrap`'s post-merge hint is now blueprint-specific, and `--client
|
|
322
|
+
code` always shows the `claude mcp add` command.** The restart hint used to
|
|
323
|
+
read "list my tasks" / "add a task to …" for EVERY blueprint (a leftover
|
|
324
|
+
from when only `task-management` existed) — it now names the actual
|
|
325
|
+
blueprint applied. The `claude mcp add` line (the idiomatic Claude Code
|
|
326
|
+
install path) used to print only on `--no-merge` / a declined / a failed
|
|
327
|
+
merge — a successful auto-merge silently hid it; it's now shown there too.
|
|
328
|
+
- Scrubbed internal issue/MR references, review-process citations, and
|
|
329
|
+
backend class names out of `packages/cli`'s comments — visible in the
|
|
330
|
+
published npm package's sourcemaps to anyone who reads mapped source.
|
|
331
|
+
Comment-only; no behavior change.
|
|
332
|
+
- **Keyring secret-store hardening.** A 0-byte salt file removed by
|
|
333
|
+
something else mid-poll (a concurrent cleanup, a manual delete of the
|
|
334
|
+
crash remnant) used to throw a raw `ENOENT` out of `setSecret`; it now
|
|
335
|
+
restarts salt creation instead, since nothing was ever encrypted under an
|
|
336
|
+
empty salt in the first place. Also fixed an order-dependent assumption
|
|
337
|
+
in two `keyring prune` tests (they now pin the processing order they
|
|
338
|
+
depend on, so a future ordering change fails with a diagnosable message
|
|
339
|
+
instead of a confusing downstream one).
|
|
340
|
+
|
|
341
|
+
### Changed
|
|
342
|
+
|
|
343
|
+
- Bundled `@vectros-ai/sdk` refreshed to the current release's staging build.
|
|
344
|
+
|
|
6
345
|
## 0.19.0 — 2026-08-27
|
|
7
346
|
|
|
8
347
|
### Added
|