@vectros-ai/cli 0.20.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 +133 -3
- package/CHANGELOG.md +198 -0
- package/README.md +84 -17
- package/dist/cli.js +2978 -462
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +2978 -462
- 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
|
|
@@ -298,7 +302,7 @@ namespaces:
|
|
|
298
302
|
# Registered in the SAME bootstrap-token phase as issuers, before schemas.
|
|
299
303
|
# Owned by this blueprint's own contextId (above) by default. `tenantWide: true` opts
|
|
300
304
|
# out of that, but declaring it is not enough on its own: the applying credential must
|
|
301
|
-
# separately hold OWNER-only authority, and `bootstrap
|
|
305
|
+
# separately hold OWNER-only authority, and `vectros bootstrap` refuses to even request
|
|
302
306
|
# it without the explicit `--allow-tenant-wide-namespaces` flag — a blueprint cannot make
|
|
303
307
|
# this happen by itself. Registration is create-once, NOT idempotent server-side: a
|
|
304
308
|
# re-apply matching every declared field converges silently, but one that disagrees
|
|
@@ -449,6 +453,90 @@ so if you seed a referencing record, seed its target first. To query "which X
|
|
|
449
453
|
reference Y", add the reference field to `lookupFields` as an equality lookup
|
|
450
454
|
(there is no reverse-reference index on this surface).
|
|
451
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
|
+
|
|
452
540
|
### The scope gate — what you can and can't request
|
|
453
541
|
|
|
454
542
|
The gate allows only **data-plane** resources:
|
|
@@ -457,11 +545,35 @@ The gate allows only **data-plane** resources:
|
|
|
457
545
|
registered — ownership-gated the same way as `records`; it grants no
|
|
458
546
|
authority over the namespace *registry* itself, which stays root-key-only).
|
|
459
547
|
Operations are `c|r|u|d`, combinable (`records:cru`), with a per-resource
|
|
460
|
-
wildcard (`records:*`). Anything else — control-plane resources
|
|
548
|
+
wildcard (`records:*`). Anything else — control-plane resources and the bare
|
|
461
549
|
`*` — is rejected, and there is **intentionally no override flag**. This is
|
|
462
550
|
*why* it's safe to apply a blueprint you didn't write: the binary is the trust
|
|
463
551
|
boundary, not the file.
|
|
464
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
|
+
|
|
465
577
|
### Capabilities (`granted_capabilities`) — a separate, equally-gated dimension
|
|
466
578
|
|
|
467
579
|
An `accessProfile` or role clause may also declare `capabilities: [...]` —
|
|
@@ -505,6 +617,24 @@ from a blueprint's `externalId` colliding with an unrelated identity, so it asks
|
|
|
505
617
|
rather than guessing either way. `--rotate` is exempt only when the local
|
|
506
618
|
record already names the same principal being rotated.
|
|
507
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
|
+
|
|
508
638
|
## Distribution
|
|
509
639
|
|
|
510
640
|
- **Bundled** — reviewed, versioned blueprints shipped in the library
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,204 @@
|
|
|
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
|
+
|
|
6
204
|
## 0.20.0 — 2026-08-27
|
|
7
205
|
|
|
8
206
|
### Added
|
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ The Vectros provisioning CLI — the `vectros` binary:
|
|
|
13
13
|
- **`vectros namespace`** — register a new scope namespace, and list/get existing ones.
|
|
14
14
|
- **`vectros identity`** — manage tenant-wide principals (users, orgs, clients, and namespaces you register).
|
|
15
15
|
- **`vectros role`** — define reusable, context-scoped access rules.
|
|
16
|
+
- **`vectros scripts`** — push, list, inspect, and delete the versioned script source a trigger
|
|
17
|
+
fires, or your own app executes directly.
|
|
16
18
|
- **`vectros assume`** — switch identity namespace values on your own token, per a role's `assumable` grant.
|
|
17
19
|
- **`vectros access`** — bind principals to contexts, and inspect what a binding grants.
|
|
18
20
|
- **`vectros key`** — issue, list, revoke, and rotate scoped API keys (`ssk_*`).
|
|
@@ -162,7 +164,8 @@ requires hand-authoring a blueprint and re-running a full apply.
|
|
|
162
164
|
vectros issuers register --context my-app --issuer-id auth0-prod \
|
|
163
165
|
--issuer https://your-tenant.us.auth0.com/ \
|
|
164
166
|
--jwks-uri https://your-tenant.us.auth0.com/.well-known/jwks.json \
|
|
165
|
-
--audience https://api.your-app.example.com
|
|
167
|
+
--audience https://api.your-app.example.com \
|
|
168
|
+
--captured-claim name --captured-claim phone_number
|
|
166
169
|
vectros issuers list --context my-app
|
|
167
170
|
vectros issuers get primary --context my-app
|
|
168
171
|
vectros issuers update primary --context my-app --status suspended
|
|
@@ -171,11 +174,11 @@ vectros issuers delete primary --context my-app # re-types the id to confirm
|
|
|
171
174
|
|
|
172
175
|
| Subcommand | Flags | Notes |
|
|
173
176
|
|---|---|---|
|
|
174
|
-
| `register` | `--context`, `--issuer-id`, `--issuer`, `--jwks-uri`, `--audience` (all required); `--sub-claim`, `--email-claim`, `--userinfo-uri`, `--self-signup-policy <type:roleId>` (repeatable), `--tenant test\|live` | Idempotent by `--issuer-id` within the tenant — a repeat returns the existing registration unchanged, not an error. |
|
|
175
|
-
| `update <issuerId>` | `--context <contextId>` (required); `--sub-claim`, `--email-claim`, `--userinfo-uri`, `--status active\|suspended`, `--self-signup-policy <type:roleId>` (repeatable), `--clear-self-signup-policies`, `--tenant test\|live` | Only ever sends the SAFE, mutable fields — `issuer`/`jwks-uri`/`audience`/`contextId` are trust-anchor fields, immutable via this route on the platform side; rotating one is `delete` + `register`. |
|
|
177
|
+
| `register` | `--context`, `--issuer-id`, `--issuer`, `--jwks-uri`, `--audience` (all required); `--sub-claim`, `--email-claim`, `--userinfo-uri`, `--self-signup-policy <type:roleId>` (repeatable), `--captured-claim <name>` (repeatable), `--tenant test\|live` | Idempotent by `--issuer-id` within the tenant — a repeat returns the existing registration unchanged, not an error. `--captured-claim` names additional OIDC claims to capture beyond the email claim on every successful token exchange, stored as your tenant's golden identity-provider-asserted copy for that user; omit it to capture nothing beyond email. At most 20 names of at most 64 characters, no duplicates. |
|
|
178
|
+
| `update <issuerId>` | `--context <contextId>` (required); `--sub-claim`, `--email-claim`, `--userinfo-uri`, `--status active\|suspended`, `--self-signup-policy <type:roleId>` (repeatable), `--clear-self-signup-policies`, `--captured-claim <name>` (repeatable), `--clear-captured-claims`, `--tenant test\|live` | Omitting `--captured-claim` leaves the stored list UNCHANGED; `--clear-captured-claims` sends an explicit empty list (capture nothing beyond email). The two are mutually exclusive, and the same absent-vs-clear split applies to `--self-signup-policy`. Only ever sends the SAFE, mutable fields — `issuer`/`jwks-uri`/`audience`/`contextId` are trust-anchor fields, immutable via this route on the platform side; rotating one is `delete` + `register`. |
|
|
176
179
|
| `delete <issuerId>` | `--context <contextId>` (required), `--force`, `--tenant test\|live` | Re-types the issuer id to confirm (mirrors `context destroy`); `--force` skips the prompt and is required when stdin isn't a TTY. Refused server-side while any user was ever created or matched through this issuer. |
|
|
177
180
|
| `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. |
|
|
178
|
-
| `get <issuerId>` | `--context <contextId>` (required), `--tenant test\|live` | One issuer's full registration, including its self-signup policies. |
|
|
181
|
+
| `get <issuerId>` | `--context <contextId>` (required), `--tenant test\|live` | One issuer's full registration, including its self-signup policies and captured claims. |
|
|
179
182
|
|
|
180
183
|
All five accept `--json`, plus the common auth flags (`--base-url`, `--token`).
|
|
181
184
|
|
|
@@ -252,6 +255,8 @@ vectros role create --context my-app --role-id own-org --name "Own org" \
|
|
|
252
255
|
--actions records:r --data-scope '{"scope:org":["<orgId>", null]}'
|
|
253
256
|
vectros role create --context my-app --role-id hr-admin --name "HR Admin" \
|
|
254
257
|
--actions records:cru --assumable '{"scope:org":["org_engineering", "org_sales"]}'
|
|
258
|
+
vectros role create --context my-app --role-id delegator --name Delegator \
|
|
259
|
+
--actions profiles:c --assignable-role editor --assignable-role own-org
|
|
255
260
|
vectros role list --context my-app
|
|
256
261
|
vectros role get --context my-app --role-id editor
|
|
257
262
|
vectros role delete --context my-app --role-id editor
|
|
@@ -259,13 +264,65 @@ vectros role delete --context my-app --role-id editor
|
|
|
259
264
|
|
|
260
265
|
| Subcommand | Flags | Notes |
|
|
261
266
|
|---|---|---|
|
|
262
|
-
| `create` | `--context <c>`, `--role-id <id>`, `--name <n>`, `--actions <csv>` (all required); `--data-scope <json>`, `--assumable <json>`, `--description <text
|
|
263
|
-
| `list` | `--context <c>` (required) | |
|
|
264
|
-
| `get` | `--context <c>`, `--role-id <id>` (both required) | Also shows the role's `assumable` grant, if any is set. |
|
|
265
|
-
| `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. |
|
|
267
|
+
| `create` | `--context <c>`, `--role-id <id>`, `--name <n>`, `--actions <csv>` (all required); `--data-scope <json>`, `--assumable <json>`, `--assignable-role <roleId>` (repeatable), `--description <text>`, `--tenant test\|live` | Idempotent by `roleId`. `--assignable-role` restricts WHICH named roles this clause may compose into a delegated AccessProfile — orthogonal to `--data-scope`, which bounds data reach rather than role composition. Omit it for no restriction; the platform rejects an empty list, so there is no way to spell "compose nothing". Once a clause carries the restriction it can compose only the roles it names, so name every roleId it still needs before adding the flag. `access explain` reports the restriction for clauses that carry one. `--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). `--assumable` is a SEPARATE, role-level grant — a JSON map of `scope:<namespace>` → allowed values naming which values a holder of this role may switch into via `vectros assume`; holding broad `--data-scope` reach in a namespace does not by itself grant assuming any value in it. |
|
|
268
|
+
| `list` | `--context <c>` (required); `--tenant test\|live` | Add `--tenant test` to read a context that lives in the test tenant — a blueprint applied there is otherwise unreadable. |
|
|
269
|
+
| `get` | `--context <c>`, `--role-id <id>` (both required); `--tenant test\|live` | Also shows the role's `assumable` grant, if any is set. |
|
|
270
|
+
| `delete` | `--context <c>`, `--role-id <id>` (both required); `--tenant test\|live` | No cascade — any access profile still referencing the role fails to mint (not silently downgraded) until reassigned. |
|
|
266
271
|
|
|
267
272
|
`create`/`list`/`get` accept `--json`; `delete` does not.
|
|
268
273
|
|
|
274
|
+
## `vectros scripts` — the script source a trigger fires, or your app runs directly
|
|
275
|
+
|
|
276
|
+
A stored script has two consumers, and this is how the source gets there for both. A **trigger rule**
|
|
277
|
+
names it in `scriptRef` and runs it under the trigger's own grant; that `scriptRef` is validated when
|
|
278
|
+
the rule is DECLARED rather than when it fires, so a script has to exist before any rule can name it.
|
|
279
|
+
**Your own app** runs a stored version directly through `POST /v1/scripts/execute`, under its own
|
|
280
|
+
credential, committing everything the script wrote as a single transaction.
|
|
281
|
+
|
|
282
|
+
Direct execution is separately granted: the calling role needs `scripts:x`, and without it the call
|
|
283
|
+
is a `403` that cannot say the missing grant is the reason. The platform accepts it bare (every
|
|
284
|
+
script in the context) or qualified to one name (`scripts:x:create-case`). A **blueprint** may use
|
|
285
|
+
only the qualified form — its scope gate refuses a bare `scripts:x` and there is no wildcard — so a
|
|
286
|
+
blueprint that ships a script grants exactly the one it ships. See *The scope gate* below, and
|
|
287
|
+
[`AUTHORING.md`](./AUTHORING.md) for where that grant goes in a blueprint.
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
vectros scripts push notify-assignee --context my-app --source-file ./notify.js
|
|
291
|
+
vectros scripts push notify-assignee --context my-app --source-file ./notify.js --if-changed
|
|
292
|
+
vectros scripts list --context my-app # every script, newest first
|
|
293
|
+
vectros scripts list --context my-app --name notify-assignee # every version of one name
|
|
294
|
+
vectros scripts get 3b1c9f2e7a5d… --context my-app # one version, with its source
|
|
295
|
+
vectros scripts delete 3b1c9f2e7a5d… --context my-app # ONE version (prompts)
|
|
296
|
+
vectros scripts delete 3b1c9f2e7a5d… --context my-app --force # ...or skip the prompt, e.g. in CI
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
| Subcommand | Flags | Notes |
|
|
300
|
+
|---|---|---|
|
|
301
|
+
| `push` | `--context <c>`, `--source-file <path>` (both required); `--input-contract <text>` or `--input-contract-file <path>`, `--if-changed`, `--tenant <test\|live>` | **Always stores a new version by default** — pushing is not idempotent, and each push gets the next server-assigned version number, which the output reports. `--if-changed` reads the current version first and pushes only on a genuine difference; use it where the same push re-runs, e.g. in CI. `--source-file` is capped at 300,000 UTF-8 bytes and the input contract at 50,000, both checked locally so an oversized push fails immediately instead of sending the body. The input contract is descriptive only — nothing validates a firing against it. Nothing parses the source either, so a syntax error surfaces the first time the version runs. |
|
|
302
|
+
| `list` | `--context <c>` (required); `--name <name>`, `--tenant <test\|live>` | With `--name`, every version of that name, oldest first; without, every script in the context, newest first. The human view shows a byte count instead of the source — a listing would otherwise print the full text of every version. `--json` includes the source. |
|
|
303
|
+
| `get` | `--context <c>` (required) + the version id; `--tenant <test\|live>` | Shows one version, including its full source. Version ids are opaque — a deterministic hex string, not a prefixed or random id — so copy them from `scripts list`. |
|
|
304
|
+
| `delete` | `--context <c>` (required) + the version id; `--force`, `--tenant <test\|live>` | Deletes ONE version; other versions of the same name are untouched. **Irreversible — the stored source is the only copy** — so it asks you to type the version id back before proceeding. `--force` skips that, and is REQUIRED in a non-interactive shell rather than the prompt being silently waived there. `--json` emits `{id, deleted}` on stdout; the warning always goes to stderr, so it stays visible without breaking a pipe. ⚠️ Deleting a name's LATEST version reuses its identity — the next push of that name recomputes the same version number and the same id, so a pinned reference silently resolves to different source. |
|
|
305
|
+
|
|
306
|
+
There is no `scripts update` — a stored version is immutable, and the platform's update route always
|
|
307
|
+
rejects. Supersede a version by pushing a new one.
|
|
308
|
+
|
|
309
|
+
`--context` is required on all four. Scripts are context-scoped and a trigger resolves its
|
|
310
|
+
`scriptRef` inside its own context, so a defaulted context would push where nobody asked and leave
|
|
311
|
+
the rule that wanted it still unresolvable. `--tenant` defaults to `live`; pass `--tenant test` to
|
|
312
|
+
reach the versions a `blueprint-test --tenant test` run created.
|
|
313
|
+
|
|
314
|
+
⚠️ **`delete` is not reference-checked.** Nothing verifies whether a trigger rule still points at the
|
|
315
|
+
version being removed, and the two outcomes differ: a rule pinned to that version fails on every
|
|
316
|
+
subsequent firing, while a rule on `version: latest` silently resolves to the PREVIOUS version and
|
|
317
|
+
carries on running older code with nothing to indicate it. This CLI has no verb that lists trigger
|
|
318
|
+
rules, so it cannot answer that question for you — check against the API before removing a version a
|
|
319
|
+
rule may still use.
|
|
320
|
+
|
|
321
|
+
A blueprint can ship a script instead of pushing one — see `scripts:` in
|
|
322
|
+
[`@vectros-ai/blueprints`](https://www.npmjs.com/package/@vectros-ai/blueprints). An apply is
|
|
323
|
+
convergent (an unchanged source pushes nothing) and never deletes: removing a `scripts:` entry
|
|
324
|
+
leaves every version in place, which is why this command group exists to remove one deliberately.
|
|
325
|
+
|
|
269
326
|
## `vectros assume` — switch identity namespace values on your own token
|
|
270
327
|
|
|
271
328
|
Re-mints your own verb token with one or more `identity.<namespace>` values
|
|
@@ -283,7 +340,7 @@ vectros assume --context my-app --scope org:org_sales
|
|
|
283
340
|
|---|---|
|
|
284
341
|
| `--context <c>` (required), `--scope <ns:value>` (repeatable, at least one required) | Each `--scope` becomes one `scope:<namespace>` entry in the request. No artificial cap on how many namespaces you name — the platform requires a SINGLE role to jointly grant the whole combination and rejects an unsatisfiable one itself. |
|
|
285
342
|
|
|
286
|
-
Prints the re-minted `access_token`/`expires_in` (accepts `--json`) — there
|
|
343
|
+
Prints the re-minted `access_token`/`expires_in`, plus the resolved `identity` and `allowedActions` the platform returns alongside it — what you actually assumed, without decoding the token (accepts `--json`) — there
|
|
287
344
|
is no `--to-keyring` here, since the result is an ephemeral bearer token
|
|
288
345
|
with the same remaining lifetime as the verb token this command itself
|
|
289
346
|
mints to make the call, not a durable secret the keyring is built to hold.
|
|
@@ -313,11 +370,11 @@ shares this grammar.
|
|
|
313
370
|
|
|
314
371
|
| Subcommand | Flags | Notes |
|
|
315
372
|
|---|---|---|
|
|
316
|
-
| `grant` | `--principal <p>`, `--context <c>` (required); exactly one of `--role <roleId>` (repeatable — 2+ compose additively) / `--actions <csv>`; `--scope <ns:value>` (repeatable), `--identity-overrides <json
|
|
317
|
-
| `revoke` | `--principal <p>`, `--context <c>` (required) | |
|
|
318
|
-
| `list` | Exactly one of `--context <c>` / `--principal <p
|
|
319
|
-
| `get` | `--principal <p>`, `--context <c>` (required) | |
|
|
320
|
-
| `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). |
|
|
373
|
+
| `grant` | `--principal <p>`, `--context <c>` (required); exactly one of `--role <roleId>` (repeatable — 2+ compose additively) / `--actions <csv>`; `--scope <ns:value>` (repeatable), `--identity-overrides <json>`, `--tenant test\|live` | 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. |
|
|
374
|
+
| `revoke` | `--principal <p>`, `--context <c>` (required); `--tenant test\|live` | |
|
|
375
|
+
| `list` | Exactly one of `--context <c>` / `--principal <p>`; `--tenant test\|live` | `--context` lists the context's members; `--principal` lists every context that principal is bound to. |
|
|
376
|
+
| `get` | `--principal <p>`, `--context <c>` (required); `--tenant test\|live` | |
|
|
377
|
+
| `explain` | `--principal <p>`, `--context <c>` (required); `--tenant test\|live` | 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). |
|
|
321
378
|
|
|
322
379
|
`grant`/`list`/`get`/`explain` accept `--json`; `revoke` does not.
|
|
323
380
|
|
|
@@ -473,11 +530,11 @@ CLI runs under an elevated ephemeral bridge token. So the trust boundary is
|
|
|
473
530
|
**this binary, not the blueprint**: a hardcoded **scope gate** bounds every
|
|
474
531
|
mint to a data-plane allowlist (`records` / `schemas` / `search` / `documents`
|
|
475
532
|
/ `folders` / `inference` / `entities`). Most control-plane scope (keys /
|
|
476
|
-
app-contexts / users / billing / admin / namespaces) or a
|
|
477
|
-
**hard-rejects** — the CLI mints nothing and exits non-zero. There is
|
|
533
|
+
app-contexts / users / billing / admin / namespaces / triggers) or a
|
|
534
|
+
`*` wildcard **hard-rejects** — the CLI mints nothing and exits non-zero. There is
|
|
478
535
|
intentionally **no override flag**.
|
|
479
536
|
|
|
480
|
-
**
|
|
537
|
+
**Two narrow, deliberate exceptions. The first:** `profiles` (which configures who has
|
|
481
538
|
access to what — control-plane by nature) admits its `r`/`u`/`d` operations,
|
|
482
539
|
because the platform enforces an unconditional check that a write to an
|
|
483
540
|
AccessProfile can never grant more scope than the requesting credential
|
|
@@ -495,6 +552,16 @@ requires. A literal-qualified `profiles:c:usr_<id>` naming another principal
|
|
|
495
552
|
still hard-rejects regardless of `member-lifecycle`, same as every other
|
|
496
553
|
control-plane scope.
|
|
497
554
|
|
|
555
|
+
**The second exception:** `scripts:x:<script-name>` — execute one named script — is admitted, so a
|
|
556
|
+
blueprint that ships a script in its `scripts:` block can also grant permission to run it. Execution
|
|
557
|
+
consults no data scope at all, and every write the script makes is still checked one by one against
|
|
558
|
+
the caller's own permissions, so this widens *what can be composed*, never *what data is reachable*.
|
|
559
|
+
Everything else on the resource still hard-rejects: a bare `scripts:x`, because it would mean "run
|
|
560
|
+
every script in this context, including ones added later that your reviewer never saw"; `scripts:x:*`,
|
|
561
|
+
because there is **no wildcard form** — the qualifier is compared to the script name exactly, so such
|
|
562
|
+
a grant would store cleanly and then refuse every execution; and `scripts:c`, authoring, which stays
|
|
563
|
+
a design-time admin surface. `triggers` is unchanged.
|
|
564
|
+
|
|
498
565
|
### Capabilities (`granted_capabilities`) — a separate, equally-gated dimension
|
|
499
566
|
|
|
500
567
|
A blueprint's `accessProfile` and role clauses may also declare
|