@vectros-ai/cli 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AUTHORING.md CHANGED
@@ -78,7 +78,11 @@ A name must be 2–50 chars: a lowercase letter, then lowercase letters / digits
78
78
  ```
79
79
 
80
80
  `bootstrap` shows the same plan and asks for confirmation before minting
81
- (`--yes` skips the prompt for CI/agents; `--print` previews without minting).
81
+ (`--yes` skips the prompt for CI/agents). ⚠️ **`--no-merge` is NOT a dry run** — it still
82
+ mints/reconciles for real; it only changes where the resulting credential goes, printing the MCP
83
+ config snippet to stdout instead of merging it into your client's config file. To preview without
84
+ touching your account, stop at `blueprint plan` (offline) — `bootstrap`, with or without `--no-merge`,
85
+ always applies.
82
86
 
83
87
  **Provisioning a production context — `--no-seed`.** A blueprint's `seed`
84
88
  records are illustrative sample data: handy for a getting-started or demo
@@ -120,7 +124,10 @@ contextId: mcp # 3-31 chars, lowercase-start. The app-context the key b
120
124
  schemas:
121
125
  - typeName: thing # immutable after first apply
122
126
  displayName: Thing
123
- indexMode: HYBRID # HYBRID (default) | SEMANTIC | TEXT
127
+ indexMode: HYBRID # HYBRID | SEMANTIC | TEXT | NONE (store-only).
128
+ # Omitting it does NOT mean HYBRID: a record schema with no
129
+ # indexMode resolves to NONE, and a document schema with none
130
+ # is rejected. Declare the mode you want.
124
131
  fields:
125
132
  - fieldId: title
126
133
  fieldType: string # string | number | boolean | date | enum | array | object | reference
@@ -134,7 +141,9 @@ schemas:
134
141
  required: true
135
142
  lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10);
136
143
  # object form { fieldName, unique?, rangeEnabled?,
137
- # sortBy?, allowOverflow? } for constraints/range.
144
+ # sortBy?, allowOverflow? } for constraints/range, or
145
+ # { fieldNames: [a, b], sortBy?, allowOverflow? } for
146
+ # a composite (conjunctive) lookup over 2-3 fields.
138
147
  # externalId has a built-in finder — don't list it.
139
148
  capabilities: { auditHistory: true } # default true; surfaced for a self-documenting audit posture
140
149
 
@@ -191,12 +200,15 @@ before customers build on the schema. Three knobs, each permanent:
191
200
  set `allowOverflow: true` on it, which accepts a higher-cost secondary index.
192
201
  Keep equality lookups lean.
193
202
 
194
- - **`sortBy` — the order results come back in.** For an *equality* lookup,
195
- `sortBy` sets the listing order: `createdAt` (default), `lastUpdated`, or a
196
- declared field. *Trap:* sorting by an **optional** payload field silently drops
197
- records that don't have it from that lookup — and it's locked. Use `createdAt`/
198
- `lastUpdated` (always present) unless every record is guaranteed to carry the
199
- field.
203
+ - **`sortBy` — the order results come back in, and what a sort-key window
204
+ narrows.** For an *equality* lookup, `sortBy` sets the listing order:
205
+ `createdAt` (default), `lastUpdated`, or a declared field. It is also what
206
+ `sortFrom`/`sortTo` bound when you narrow a `value` lookup. An **optional**
207
+ field is fine: records that don't carry it are listed ahead of those that do,
208
+ and a bounded window never includes them. *Locked:* `sortBy` itself AND the
209
+ sorted field's declared `fieldType` are both migration-locked — pick a new field
210
+ name if you need to change either. An `array`/`object` field is rejected as a
211
+ `sortBy` target (no meaningful order).
200
212
 
201
213
  **Sensitive fields can still be looked up — by exact match only.** A
202
214
  `sensitive` field is HMAC'd into a per-tenant **blind index**, so you *can* list
@@ -205,6 +217,49 @@ without the value ever being stored in the clear or entering the search index. A
205
217
  blind hash is not orderable, so a sensitive lookup **cannot** be `rangeEnabled`,
206
218
  and no lookup's `sortBy` may name a sensitive field (the platform rejects both).
207
219
 
220
+ ### Composite (conjunctive) lookups — matching several fields at once
221
+
222
+ `{ fieldNames: [a, b] }` (2-3 fields) matches ALL of them together in one clause —
223
+ "every record where `status` is `open` **and** `area` is `billing`" — exact,
224
+ complete, in the declared order. This is a different query shape from declaring
225
+ `status` and `area` as two separate equality lookups: two separate lookups can
226
+ each tell you "records with this status" or "records with this area", but never
227
+ "records with both", short of fetching one side and filtering client-side.
228
+
229
+ ```yaml
230
+ lookupFields:
231
+ - externalId
232
+ - { fieldNames: [status, area], sortBy: createdAt, allowOverflow: true }
233
+ ```
234
+
235
+ Like a single-field lookup, the shape is **migration-locked** at create — the same
236
+ "decide deliberately" rule as above applies, doubly so here since a composite
237
+ also can't be swapped for two plain lookups later without a new field name.
238
+ Rules the platform enforces, all offline-checkable and caught by `blueprint
239
+ validate` before they'd 400 at `bootstrap`:
240
+
241
+ - **2-3 fields, order significant.** A query may match a leading run of the list
242
+ (the first field alone, the first two together, …) but never a later field
243
+ alone — declare a separate lookup for that. No leg may repeat.
244
+ - **Each leg must be a scalar, single-valued field** — not `array`/`object`, and
245
+ not a `many`-cardinality `reference`. A leg occupies one sort-key position and
246
+ holds one value; a multi-valued field there would index only one rendering and
247
+ the conjunction would silently return a subset while looking complete.
248
+ Cardinality-`one` references are fine.
249
+ - **A leg can't ALSO be its own `sortBy` target**, on this same lookup or a
250
+ separate one that treats that field as `rangeEnabled`. Within a fixed tuple
251
+ every matching record has the same value for that leg, so ordering by it (or
252
+ range-querying it) is meaningless — pick a different field, `createdAt`, or
253
+ `lastUpdated`.
254
+ - **`unique`/`rangeEnabled` are not available on a composite** — it's an
255
+ exact-match index over its fields; declare a range lookup separately if you
256
+ need one. `sortBy` and `allowOverflow` ARE available; `sortBy` on a composite
257
+ orders *within* a group when a query supplies fewer values than the lookup
258
+ declares, not across the whole result.
259
+ - **Record-only.** The schema's `allowedSurfaces` must be omitted or exactly
260
+ `[record]` — the platform's composite index has no document/user/entity reader
261
+ yet.
262
+
208
263
  ### Reference fields — typed links between record types
209
264
 
210
265
  A `reference` field is a foreign key to another record. It needs **both** the
package/CHANGELOG.md CHANGED
@@ -3,6 +3,155 @@
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.14.0
7
+
8
+ ### Security
9
+
10
+ - **`bootstrap` no longer stores your minted `ssk_*` in plaintext.** The key
11
+ goes to this machine's OS credential store (or an encrypted file where none
12
+ is available); your MCP client config references it by alias only
13
+ (`VECTROS_KEYRING_ALIAS`), never the raw secret. **Breaking:** a config
14
+ merged by an older CLI keeps working, but a fresh `bootstrap` run now
15
+ produces the alias-only shape — if you scripted against the printed
16
+ snippet's `VECTROS_API_KEY` field, it's `VECTROS_KEYRING_ALIAS` now.
17
+
18
+ - **`--print` is renamed `--no-merge`, with no back-compat alias.** It was
19
+ never a dry run — it always applies for real and only changes where the
20
+ result goes. It still applies; it just skips writing your client's config
21
+ file. For a true preview, use `blueprint plan`.
22
+
23
+ ### Added
24
+
25
+ - **`key issue --to-keyring` and `key rotate --to-keyring` store the minted secret
26
+ instead of printing it.** Until now the mint verbs could only *emit* a key, so on
27
+ a desktop the only place to put it was the MCP client's config file, in plaintext.
28
+ `--to-keyring` writes it to this machine's OS credential store where one is in use,
29
+ and to the encrypted file under `~/.vectros/secrets` otherwise — telling you which
30
+ of the two took it either way, because only one of them is login-gated. The key is
31
+ never printed to a terminal, a pipe or a transcript. Anything that resolves a
32
+ credential through `vectros keyring show --format raw` — the MCP server included
33
+ — then reads it with no `VECTROS_API_KEY` set anywhere.
34
+
35
+ `--alias <name>` names the entry (default: the context id) and `--no-activate`
36
+ stores it without making it the active identity. Emitting stays the **default**:
37
+ it is the right behaviour for a headless server, where a secrets manager
38
+ distributes the key and nothing should persist it locally. The two destinations
39
+ are mutually exclusive — `--to-keyring` with `--format raw|env|json` is rejected
40
+ before anything is minted, rather than storing the key and printing it anyway.
41
+
42
+ `rotate --to-keyring` proves the store will accept a write *before* it revokes
43
+ the old key, so a machine that cannot hold a secret fails with your existing
44
+ credential intact.
45
+
46
+ ### Fixed
47
+
48
+ - **`bootstrap --plan` no longer claims `HYBRID` for a schema that declares no
49
+ `indexMode`.** The plan rendered an absent mode as `HYBRID`, but nothing
50
+ substitutes that default — the value is forwarded untouched, and the platform
51
+ then resolves an absent mode to `NONE` (store-only, never searchable) on a record
52
+ schema, and rejects a document schema that declares none. So the plan promised a
53
+ searchable type and the apply produced a store-only one, or previewed a request
54
+ the API would refuse. The plan now names the real outcome in both cases.
55
+
56
+ ### Changed
57
+
58
+ - **A blueprint may now declare `indexMode: NONE`** (store-only: kept and looked up, never
59
+ searchable). The mode has always been accepted by the platform, and `bootstrap`/`validate`/`plan`
60
+ rejected it earlier than that — structural validation runs against this CLI's own bundled copy of
61
+ the blueprint format, which predated the value. That copy now carries it, so a blueprint declaring
62
+ `NONE` validates and applies. This governs blueprints loaded **from a file** as much as bundled
63
+ ones: both funnel through the same structural parse.
64
+
65
+ - **`blueprint init` scaffold and `AUTHORING.md`** document the full `indexMode` set including
66
+ `NONE`, and warn that omitting the field does not mean `HYBRID` — a record schema with no mode of
67
+ its own resolves to `NONE`, and a document schema with none is rejected.
68
+
69
+ ## 0.13.0
70
+
71
+ ### Added
72
+
73
+ - **`access explain` shows the row for "this record IS the identity you asked about".** A clause
74
+ bounding a namespace that belongs to an identity entity — a value the platform derives from the
75
+ entity's own id rather than one stored on the record — previously produced no matrix row for it at
76
+ all, because the matrix only built rows from values a clause spelled out literally. The matrix now
77
+ always carries that row, so a clause confining a credential to its own entity can be checked the same
78
+ way as every other ownership shape.
79
+ - **`access explain`'s grant matrix shows exactly which record types a row reaches, not only whether
80
+ it's granted.** A new "record types" column (`recordTypes` in `--json` output) names the types a
81
+ row's granting clause(s) actually cover — every type, a specific list, or none. Previously an
82
+ ownership-unscoped but type-restricted clause (a curated shared-content role, for example) rendered as
83
+ a plain "granted", which read as reaching every record type sharing that ownership shape, including
84
+ ones the clause never actually named.
85
+ - **`access explain` understands the placement matchers.** `${{ any }}` renders as `any` and matches
86
+ any value present in that dimension — deliberately *not* a record with no value there, since it does
87
+ not subsume the `null` sentinel (`[${{ any }}]` and `[${{ any }}, null]` are different grants).
88
+ `${{ under.self.* }}` renders as `under-self`; deciding it requires the parent edge of every
89
+ candidate value, which only the server has, so the matrix reports it as undecidable rather than
90
+ guessing. Previously both were compared as literal strings, so a clause using either was shown as
91
+ matching only a record whose owner value was the placeholder text itself.
92
+ - **New lint rule `datascope-unknown-placeholder` (error).** A `${{ … }}` value in a `dataScope` that
93
+ is not one of the platform's placeholders is now caught offline, with the correct spellings in the
94
+ message, instead of surfacing as a `400` at `apply`. Blueprint-level tokens (`${{ identities.* }}`,
95
+ `${{ inputs.* }}`, …) are substituted before the value reaches the platform and are not flagged.
96
+ - **A blueprint schema can now declare a composite (conjunctive) lookup** — `lookupFields: [{ fieldNames:
97
+ ['status', 'area'] }]` matches on several fields at once ("every record where `status` is `open` **and**
98
+ `area` is `billing`"), 2-3 fields, exact and complete, in the declared field order. `blueprint validate`
99
+ now catches every leg-shape mistake the platform would otherwise 400 on mid-`bootstrap`: an undeclared
100
+ leg (naming the exact leg, not just the first), a repeated leg, a leg that's `array`/`object`-typed or a
101
+ many-cardinality reference, a leg that's separately declared `rangeEnabled`, and a leg used as its own
102
+ `sortBy` target. `bootstrap`/`blueprint-test apply` forward a composite to the partner API unchanged, and
103
+ the approval plan (`bootstrap`'s "Apply this plan?" preview) now renders one as `(status+area)` rather
104
+ than dropping it from the lookup list. `unique`/`rangeEnabled` aren't available on a composite; `sortBy`
105
+ and `allowOverflow` are. No bundled blueprint declares one yet — adopting the shape in the bundled
106
+ library is a separate, deliberate content decision.
107
+
108
+ ### Changed
109
+
110
+ - **`--scope` now validates the value locally, on every command that takes it** — `join`,
111
+ `access grant` and `identity create`. A scope value must be 1-128 characters, starting with a letter
112
+ or digit and otherwise containing only letters, digits, `_` and `-` — the grammar the API enforces,
113
+ because a scope value becomes part of a storage key. `--scope org:a:b:c` previously passed a
114
+ colon-bearing value straight through; it now fails immediately, naming the offending value, instead
115
+ of returning a server error later in the call.
116
+ - **`access grant --identity-overrides` validates its values too.** The JSON form wins on key
117
+ conflicts with `--scope`, so a value rejected on one flag could previously be reinstated by the other
118
+ in the same command. Both are now checked after the merge.
119
+ - **Dependency maintenance** — repinned the bundled `@vectros-ai/sdk` to `0.38.0` (composite/conjunctive
120
+ record lookups — a schema can now be looked up by 2-3 fields matched together in one clause, not just
121
+ one — plus the placement matchers `${{ any }}` / `${{ under.self.* }}`, the `"*"` dimension wildcard in
122
+ `data_scope`, and sealed pagination cursors) and the bundled `@vectros-ai/blueprints` to `0.10.0` (the
123
+ composite-lookup authoring format above). The CLI bundles both into its published binary, so this is a
124
+ rebuild against their current versions.
125
+
126
+ ### Fixed
127
+
128
+ - **`access explain` no longer reports a fully-permissive scope as granting nothing.** A clause using
129
+ the dimension wildcard — `data_scope: {"*": [...]}`, which grants over every dimension the clause
130
+ does not name explicitly — was read as naming an *unrecognized* dimension, and an unrecognized
131
+ dimension kills a whole clause. The most permissive shape there is therefore rendered as an empty
132
+ matrix. `*` is now recognized, and it is applied only to the dimensions a record actually carries,
133
+ with an explicit entry for a dimension always taking precedence over it.
134
+ - **`access explain` now says plainly that its "granted?" and "actions" columns model reading a record,
135
+ not writing one.** A clause that doesn't mention an ownership dimension no longer authorizes placing a
136
+ value there, but it never narrowed reads to begin with — so a create/update verb next to a row is not
137
+ confirmation that a write with that row's exact ownership shape will succeed. The command's output
138
+ now says so directly instead of leaving it implicit.
139
+ - **`access explain` recognizes a `${{ self.scope.<ns> }}` / `${{ under.self.scope.<ns> }}` placeholder
140
+ for every namespace the platform accepts, not just letters and dots.** A namespace containing a digit,
141
+ `_`, or `-` (`${{ self.scope.eng_team }}`) previously fell through to being matched as an opaque
142
+ literal string instead of resolving to the caller, silently under-reporting that clause's grant.
143
+ - **Corrected the `sortBy` guidance in `AUTHORING.md`.** It carried a "trap"
144
+ warning that sorting an equality lookup by an **optional** payload field
145
+ "silently drops records that don't have it." That was never accurate — an
146
+ equality lookup resolves on the partition key, and `sortBy` only orders within
147
+ it — and records with no value for the sorted field now take their own ordered
148
+ position ahead of the rest, and are never inside a `sortFrom`/`sortTo` window.
149
+ That ordering applies to records written from this platform release onward;
150
+ earlier records take their new position once they are next updated.
151
+ The section now also names what `sortBy` really locks: the sorted field's
152
+ declared `fieldType` is migration-locked alongside `sortBy` itself, and an
153
+ `array`/`object` field is rejected as a `sortBy` target.
154
+
6
155
  ## 0.12.0
7
156
 
8
157
  ### Added
package/README.md CHANGED
@@ -115,7 +115,9 @@ The app-context is intentionally **not** deleted (it's idempotent and reused).
115
115
  A machine can hold several Vectros identities (one per context or principal).
116
116
  The keyring stores them as named entries plus one **active** pointer, so you can
117
117
  switch between them without re-issuing keys. `vectros join <context>` adds an
118
- entry and activates it; `vectros switch <alias>` flips the active one.
118
+ entry and activates it; `vectros key issue`/`key rotate` add one when passed
119
+ `--to-keyring` (which stores the new secret instead of printing it); `vectros switch
120
+ <alias>` flips the active one.
119
121
 
120
122
  ```bash
121
123
  vectros keyring list # the entries; * marks the active one