@vectros-ai/cli 0.7.0 → 0.9.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/dist/cli.mjs CHANGED
@@ -652,6 +652,10 @@ var ReadAccessLogResponse;
652
652
  })(ReadAccessLogResponse || (ReadAccessLogResponse = {}));
653
653
  var RecordRequest;
654
654
  ((RecordRequest2) => {
655
+ RecordRequest2.Status = {
656
+ Active: "ACTIVE",
657
+ Archived: "ARCHIVED"
658
+ };
655
659
  RecordRequest2.IndexMode = {
656
660
  Hybrid: "HYBRID",
657
661
  Semantic: "SEMANTIC",
@@ -661,6 +665,10 @@ var RecordRequest;
661
665
  })(RecordRequest || (RecordRequest = {}));
662
666
  var RecordResponse;
663
667
  ((RecordResponse2) => {
668
+ RecordResponse2.Status = {
669
+ Active: "ACTIVE",
670
+ Archived: "ARCHIVED"
671
+ };
664
672
  RecordResponse2.IndexStatus = {
665
673
  PendingIndex: "PENDING_INDEX",
666
674
  Indexed: "INDEXED",
@@ -2283,7 +2291,7 @@ function withNoOpAuthProvider(options) {
2283
2291
  authProvider: new NoOpAuthProvider()
2284
2292
  };
2285
2293
  }
2286
- function handleNonStatusCodeError(error, rawResponse, method, path4) {
2294
+ function handleNonStatusCodeError(error, rawResponse, method, path5) {
2287
2295
  switch (error.reason) {
2288
2296
  case "non-json":
2289
2297
  throw new VectrosError({
@@ -2297,7 +2305,7 @@ function handleNonStatusCodeError(error, rawResponse, method, path4) {
2297
2305
  rawResponse
2298
2306
  });
2299
2307
  case "timeout":
2300
- throw new VectrosTimeoutError(`Timeout exceeded when calling ${method} ${path4}.`, {
2308
+ throw new VectrosTimeoutError(`Timeout exceeded when calling ${method} ${path5}.`, {
2301
2309
  cause: error.cause
2302
2310
  });
2303
2311
  case "unknown":
@@ -3135,7 +3143,7 @@ var AuthClient = class {
3135
3143
  * roleId: "engineering-member",
3136
3144
  * name: "Engineering Team Member",
3137
3145
  * scopes: [{
3138
- * allowed_actions: ["read", "write"]
3146
+ * allowed_actions: ["records:cru", "search:r"]
3139
3147
  * }]
3140
3148
  * }
3141
3149
  * })
@@ -3683,7 +3691,7 @@ var AuthClient = class {
3683
3691
  * roleId: "engineering-member",
3684
3692
  * name: "Engineering Team Member",
3685
3693
  * scopes: [{
3686
- * allowed_actions: ["read", "write"]
3694
+ * allowed_actions: ["records:cru", "search:r"]
3687
3695
  * }]
3688
3696
  * }
3689
3697
  * })
@@ -4586,7 +4594,7 @@ var DocumentsClient = class {
4586
4594
  this._options = normalizeClientOptionsWithAuth(options);
4587
4595
  }
4588
4596
  /**
4589
- * Returns a paginated list of your documents, optionally filtered by folder (`folderId`) and/or owner (`userId`, `orgId`, or `clientId`). The response is a `{data, nextCursor}` envelope; pass `nextCursor` back as `startFrom` to fetch the next page. Requires the `documents:r` scope.
4597
+ * Returns a paginated list of your documents, optionally filtered by folder (`folderId`) and/or owner (`userId`, `orgId`, `clientId`, or `scope`). The response is a `{data, nextCursor}` envelope; pass `nextCursor` back as `startFrom` to fetch the next page. Requires the `documents:r` scope.
4590
4598
  *
4591
4599
  * @param {Vectros.ListDocumentsRequest} request
4592
4600
  * @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4596,6 +4604,7 @@ var DocumentsClient = class {
4596
4604
  * userId: "550e8400-e29b-41d4-a716-446655440000",
4597
4605
  * orgId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
4598
4606
  * clientId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
4607
+ * scope: "group:eng-team",
4599
4608
  * folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
4600
4609
  * startFrom: "doc_prev123"
4601
4610
  * })
@@ -4604,11 +4613,12 @@ var DocumentsClient = class {
4604
4613
  return HttpResponsePromise.fromPromise(this.__listDocuments(request, requestOptions));
4605
4614
  }
4606
4615
  async __listDocuments(request = {}, requestOptions) {
4607
- const { userId, orgId, clientId, folderId, startFrom, limit } = request;
4616
+ const { userId, orgId, clientId, scope, folderId, startFrom, limit } = request;
4608
4617
  const _queryParams = {
4609
4618
  userId,
4610
4619
  orgId,
4611
4620
  clientId,
4621
+ scope,
4612
4622
  folderId,
4613
4623
  startFrom,
4614
4624
  limit
@@ -4666,9 +4676,10 @@ var DocumentsClient = class {
4666
4676
  return HttpResponsePromise.fromPromise(this.__ingestDocument(request, requestOptions));
4667
4677
  }
4668
4678
  async __ingestDocument(request, requestOptions) {
4669
- const { upsert, body: _body } = request;
4679
+ const { upsert, allowClear, body: _body } = request;
4670
4680
  const _queryParams = {
4671
- upsert
4681
+ upsert,
4682
+ allowClear
4672
4683
  };
4673
4684
  const _authRequest = await this._options.authProvider.getAuthRequest();
4674
4685
  const _headers = mergeHeaders(
@@ -4792,7 +4803,10 @@ var DocumentsClient = class {
4792
4803
  return HttpResponsePromise.fromPromise(this.__updateDocument(request, requestOptions));
4793
4804
  }
4794
4805
  async __updateDocument(request, requestOptions) {
4795
- const { id, body: _body } = request;
4806
+ const { id, allowClear, body: _body } = request;
4807
+ const _queryParams = {
4808
+ allowClear
4809
+ };
4796
4810
  const _authRequest = await this._options.authProvider.getAuthRequest();
4797
4811
  const _headers = mergeHeaders(
4798
4812
  _authRequest.headers,
@@ -4807,7 +4821,7 @@ var DocumentsClient = class {
4807
4821
  method: "PUT",
4808
4822
  headers: _headers,
4809
4823
  contentType: "application/json",
4810
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
4824
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
4811
4825
  requestType: "json",
4812
4826
  body: _body,
4813
4827
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -7543,7 +7557,7 @@ var RecordsClient = class {
7543
7557
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/records/batch");
7544
7558
  }
7545
7559
  /**
7546
- * Returns a paginated list of records in your account as a `{data, nextCursor}` page. Supply exactly one of `type`, `folderId`, or `recent=true` to choose the mode: `type` lists all records of a single type; `folderId` lists all records in a folder (any type); and `recent=true` returns the account-wide recently-updated feed across all types, newest first. You may combine `type` with `folderId` to list a single type within a folder. The owner filters (`userId`, `orgId`, `clientId`) further narrow the type and folder modes; the `recent` feed is standalone and ignores all filters. Each token only sees the record types it is scoped to read. Requires the `records:r` scope. By default the response returns the indexed projection of each record; set `includePayload=true` to include full payloads.
7560
+ * Returns a paginated list of records in your account as a `{data, nextCursor}` page. Supply exactly one of `type`, `folderId`, or `recent=true` to choose the mode: `type` lists all records of a single type; `folderId` lists all records in a folder (any type); and `recent=true` returns the account-wide recently-updated feed across all types, newest first. You may combine `type` with `folderId` to list a single type within a folder. The owner filters (`userId`, `orgId`, `clientId`, `scope`) further narrow the type and folder modes; the `recent` feed is standalone and ignores all filters. Each token only sees the record types it is scoped to read. Requires the `records:r` scope. By default the response returns the indexed projection of each record; set `includePayload=true` to include full payloads.
7547
7561
  *
7548
7562
  * @param {Vectros.ListRecordsRequest} request
7549
7563
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7555,6 +7569,7 @@ var RecordsClient = class {
7555
7569
  * userId: "550e8400-e29b-41d4-a716-446655440000",
7556
7570
  * orgId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
7557
7571
  * clientId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
7572
+ * scope: "group:eng-team",
7558
7573
  * startFrom: "550e8400-e29b-41d4-a716-446655440000"
7559
7574
  * })
7560
7575
  */
@@ -7562,13 +7577,25 @@ var RecordsClient = class {
7562
7577
  return HttpResponsePromise.fromPromise(this.__listRecords(request, requestOptions));
7563
7578
  }
7564
7579
  async __listRecords(request = {}, requestOptions) {
7565
- const { type: type_, folderId, userId, orgId, clientId, startFrom, limit, includePayload, recent } = request;
7580
+ const {
7581
+ type: type_,
7582
+ folderId,
7583
+ userId,
7584
+ orgId,
7585
+ clientId,
7586
+ scope,
7587
+ startFrom,
7588
+ limit,
7589
+ includePayload,
7590
+ recent
7591
+ } = request;
7566
7592
  const _queryParams = {
7567
7593
  type: type_,
7568
7594
  folderId,
7569
7595
  userId,
7570
7596
  orgId,
7571
7597
  clientId,
7598
+ scope,
7572
7599
  startFrom,
7573
7600
  limit,
7574
7601
  includePayload,
@@ -7633,9 +7660,10 @@ var RecordsClient = class {
7633
7660
  return HttpResponsePromise.fromPromise(this.__createRecord(request, requestOptions));
7634
7661
  }
7635
7662
  async __createRecord(request, requestOptions) {
7636
- const { upsert, body: _body } = request;
7663
+ const { upsert, allowClear, body: _body } = request;
7637
7664
  const _queryParams = {
7638
- upsert
7665
+ upsert,
7666
+ allowClear
7639
7667
  };
7640
7668
  const _authRequest = await this._options.authProvider.getAuthRequest();
7641
7669
  const _headers = mergeHeaders(
@@ -7757,7 +7785,10 @@ var RecordsClient = class {
7757
7785
  return HttpResponsePromise.fromPromise(this.__updateRecord(request, requestOptions));
7758
7786
  }
7759
7787
  async __updateRecord(request, requestOptions) {
7760
- const { id, body: _body } = request;
7788
+ const { id, allowClear, body: _body } = request;
7789
+ const _queryParams = {
7790
+ allowClear
7791
+ };
7761
7792
  const _authRequest = await this._options.authProvider.getAuthRequest();
7762
7793
  const _headers = mergeHeaders(
7763
7794
  _authRequest.headers,
@@ -7772,7 +7803,7 @@ var RecordsClient = class {
7772
7803
  method: "PUT",
7773
7804
  headers: _headers,
7774
7805
  contentType: "application/json",
7775
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
7806
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
7776
7807
  requestType: "json",
7777
7808
  body: _body,
7778
7809
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
@@ -8857,7 +8888,14 @@ var BlueprintAccessProfileSchema = z.object({
8857
8888
  // null sentinel (+ ScopeClause). e.g. `{ orgId: ["org_x", null] }` =
8858
8889
  // "org_x's records AND tenant-shared records". Omitting null restricts to
8859
8890
  // the listed owners ONLY (the key will NOT see tenant-level/seed records).
8860
- dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional()
8891
+ dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
8892
+ // Optional identity overrides for the service principal's profile — scope
8893
+ // values its key STAMPS onto everything it writes (a credential can only
8894
+ // stamp ownership its identity carries). Keys: `orgId`/`clientId` (or the
8895
+ // namespaced `scope:<ns>` form); values may be `${{ identities.* }}` tokens
8896
+ // (substituted at apply time). e.g. `{ orgId: '${{ identities.team }}' }`
8897
+ // makes every record the service key writes org/team-owned.
8898
+ identityOverrides: z.record(z.string().min(1)).optional()
8861
8899
  }).strict();
8862
8900
  var BlueprintRoleClauseSchema = z.object({
8863
8901
  allowedActions: z.array(z.string().min(1)).min(1),
@@ -8883,7 +8921,14 @@ var SeedCommonShape = {
8883
8921
  * seeds resolve a `reference` against (across surfaces: a record seed may
8884
8922
  * reference a document seed by its externalId, and vice versa).
8885
8923
  */
8886
- externalId: z.string().min(1)
8924
+ externalId: z.string().min(1),
8925
+ /**
8926
+ * Optional scope ownership for the seeded item, as `namespace:value` entries
8927
+ * (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
8928
+ * time). `[]` = a private, user-owned item. Omitted = the seeding
8929
+ * credential's full identity (the default).
8930
+ */
8931
+ scopes: z.array(z.string()).optional()
8887
8932
  };
8888
8933
  var RecordSeedSchema = z.object({
8889
8934
  surface: z.literal("record"),
@@ -8907,25 +8952,25 @@ var BlueprintSeedRecordSchema = z.discriminatedUnion("surface", [
8907
8952
  ]);
8908
8953
  var SELF_TOKEN_RE = /\$\{\{\s*self\.[A-Za-z_]\w*\s*\}\}/;
8909
8954
  function lintSelfTokenPlacement(value, ctx) {
8910
- const walk = (node, path4, inRoleDataScope) => {
8955
+ const walk = (node, path5, inRoleDataScope) => {
8911
8956
  if (typeof node === "string") {
8912
8957
  if (!inRoleDataScope && SELF_TOKEN_RE.test(node)) {
8913
8958
  ctx.addIssue({
8914
8959
  code: z.ZodIssueCode.custom,
8915
- path: path4,
8960
+ path: path5,
8916
8961
  message: "'${{ self.* }}' is a runtime per-principal placeholder \u2014 it is only valid inside a roles[].dataScope value"
8917
8962
  });
8918
8963
  }
8919
8964
  return;
8920
8965
  }
8921
8966
  if (Array.isArray(node)) {
8922
- node.forEach((v, i) => walk(v, [...path4, i], inRoleDataScope));
8967
+ node.forEach((v, i) => walk(v, [...path5, i], inRoleDataScope));
8923
8968
  return;
8924
8969
  }
8925
8970
  if (node && typeof node === "object") {
8926
8971
  for (const [k, v] of Object.entries(node)) {
8927
- const entering = inRoleDataScope || path4.length === 3 && path4[0] === "roles" && k === "dataScope";
8928
- walk(v, [...path4, k], entering);
8972
+ const entering = inRoleDataScope || path5.length === 3 && path5[0] === "roles" && k === "dataScope";
8973
+ walk(v, [...path5, k], entering);
8929
8974
  }
8930
8975
  }
8931
8976
  };
@@ -8936,13 +8981,13 @@ function lintIdentityRefsDeclared(value, ctx) {
8936
8981
  const declared = new Set(
8937
8982
  value && typeof value === "object" && "identities" in value && value.identities ? Object.keys(value.identities) : []
8938
8983
  );
8939
- const walk = (node, path4) => {
8984
+ const walk = (node, path5) => {
8940
8985
  if (typeof node === "string") {
8941
8986
  for (const m of node.matchAll(IDENTITY_REF_RE)) {
8942
8987
  if (!declared.has(m[1])) {
8943
8988
  ctx.addIssue({
8944
8989
  code: z.ZodIssueCode.custom,
8945
- path: path4,
8990
+ path: path5,
8946
8991
  message: `'\${{ identities.${m[1]} }}' references an undeclared identity \u2014 add '${m[1]}' to the top-level 'identities' block`
8947
8992
  });
8948
8993
  }
@@ -8950,11 +8995,11 @@ function lintIdentityRefsDeclared(value, ctx) {
8950
8995
  return;
8951
8996
  }
8952
8997
  if (Array.isArray(node)) {
8953
- node.forEach((v, i) => walk(v, [...path4, i]));
8998
+ node.forEach((v, i) => walk(v, [...path5, i]));
8954
8999
  } else if (node && typeof node === "object") {
8955
9000
  for (const [k, v] of Object.entries(node)) {
8956
- if (path4.length === 0 && k === "identities") continue;
8957
- walk(v, [...path4, k]);
9001
+ if (path5.length === 0 && k === "identities") continue;
9002
+ walk(v, [...path5, k]);
8958
9003
  }
8959
9004
  }
8960
9005
  };
@@ -9001,9 +9046,9 @@ var BlueprintSchema = z.object({
9001
9046
  lintIdentityRefsDeclared(bp, ctx);
9002
9047
  lintSeedSurfaces(bp, ctx);
9003
9048
  });
9004
- function formatIssuePath(path4) {
9049
+ function formatIssuePath(path5) {
9005
9050
  let out = "";
9006
- for (const seg of path4) {
9051
+ for (const seg of path5) {
9007
9052
  if (typeof seg === "number") out += `[${seg}]`;
9008
9053
  else out += out.length ? `.${seg}` : seg;
9009
9054
  }
@@ -9041,6 +9086,14 @@ function contextNameOf(blueprint) {
9041
9086
  return blueprint.contextName ?? `MCP \u2014 ${blueprint.name}`;
9042
9087
  }
9043
9088
  var IDENTITY_TOKEN_RE = /\$\{\{\s*identities\.([A-Za-z_]\w*)\s*\}\}/g;
9089
+ var BlueprintIdentityError = class extends Error {
9090
+ issues;
9091
+ constructor(message, issues = []) {
9092
+ super(message);
9093
+ this.name = "BlueprintIdentityError";
9094
+ this.issues = issues;
9095
+ }
9096
+ };
9044
9097
  function collectIdentityReferences(value) {
9045
9098
  const found = /* @__PURE__ */ new Set();
9046
9099
  const walk = (node) => {
@@ -9055,6 +9108,61 @@ function collectIdentityReferences(value) {
9055
9108
  walk(value);
9056
9109
  return [...found];
9057
9110
  }
9111
+ function substitute(node, idMap) {
9112
+ if (typeof node === "string") {
9113
+ return node.replace(IDENTITY_TOKEN_RE, (_full, name) => idMap[name]);
9114
+ }
9115
+ if (Array.isArray(node)) return node.map((v) => substitute(v, idMap));
9116
+ if (node && typeof node === "object") {
9117
+ const out = {};
9118
+ for (const [k, v] of Object.entries(node)) out[k] = substitute(v, idMap);
9119
+ return out;
9120
+ }
9121
+ return node;
9122
+ }
9123
+ async function resolveBlueprintIdentities(raw, resolve) {
9124
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
9125
+ const { identities: rawIdentities, ...body } = raw;
9126
+ const refs = collectIdentityReferences(body);
9127
+ if (rawIdentities === void 0 && refs.length === 0) return raw;
9128
+ const parsed = IdentitiesDeclSchema.safeParse(rawIdentities ?? {});
9129
+ if (!parsed.success) {
9130
+ throw new BlueprintIdentityError(
9131
+ "Blueprint identities block is invalid",
9132
+ parsed.error.issues.map((i) => ({
9133
+ path: i.path.length ? `identities.${i.path.join(".")}` : "identities",
9134
+ message: i.message
9135
+ }))
9136
+ );
9137
+ }
9138
+ const declared = parsed.data;
9139
+ const undeclared = refs.filter((name) => !(name in declared));
9140
+ if (undeclared.length) {
9141
+ throw new BlueprintIdentityError(
9142
+ `Blueprint references undeclared identities: ${undeclared.join(", ")}`,
9143
+ undeclared.map((name) => ({
9144
+ path: `identities.${name}`,
9145
+ message: `'\${{ identities.${name} }}' is referenced but not declared in the 'identities' block`
9146
+ }))
9147
+ );
9148
+ }
9149
+ const idMap = {};
9150
+ for (const [name, decl] of Object.entries(declared)) {
9151
+ try {
9152
+ const id = await resolve(name, decl);
9153
+ if (typeof id !== "string" || id.length === 0) {
9154
+ throw new Error(`resolver returned a non-string id (${JSON.stringify(id)})`);
9155
+ }
9156
+ idMap[name] = id;
9157
+ } catch (err) {
9158
+ throw new BlueprintIdentityError(
9159
+ `Failed to resolve identity '${name}' (${decl.kind} externalId=${decl.externalId}): ${err instanceof Error ? err.message : String(err)}`,
9160
+ [{ path: `identities.${name}`, message: err instanceof Error ? err.message : String(err) }]
9161
+ );
9162
+ }
9163
+ }
9164
+ return substitute(body, idMap);
9165
+ }
9058
9166
  var INPUT_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
9059
9167
  var InputDeclSchema = z2.object({
9060
9168
  type: z2.enum(["string", "number", "boolean"]),
@@ -9092,18 +9200,18 @@ var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
9092
9200
  var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
9093
9201
  var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
9094
9202
  var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities"]);
9095
- function resolveRef(ns, name, ctx, path4) {
9203
+ function resolveRef(ns, name, ctx, path5) {
9096
9204
  if (ns === "inputs") {
9097
9205
  if (!(name in ctx.declared)) {
9098
9206
  ctx.issues.push({
9099
- path: path4,
9207
+ path: path5,
9100
9208
  message: `unknown input 'inputs.${name}' \u2014 declare it in the top-level 'inputs:' block`
9101
9209
  });
9102
9210
  return void 0;
9103
9211
  }
9104
9212
  if (!(name in ctx.values)) {
9105
9213
  ctx.issues.push({
9106
- path: path4,
9214
+ path: path5,
9107
9215
  message: `input 'inputs.${name}' has no value \u2014 supply it (--set ${name}=\u2026 or --values) or give it a default`
9108
9216
  });
9109
9217
  return void 0;
@@ -9113,14 +9221,14 @@ function resolveRef(ns, name, ctx, path4) {
9113
9221
  if (ns === "vectros") {
9114
9222
  if (name !== "context" && name !== "suffix") {
9115
9223
  ctx.issues.push({
9116
- path: path4,
9224
+ path: path5,
9117
9225
  message: `unknown built-in 'vectros.${name}' (available: vectros.context, vectros.suffix)`
9118
9226
  });
9119
9227
  return void 0;
9120
9228
  }
9121
9229
  if (!ctx.builtins) {
9122
9230
  ctx.issues.push({
9123
- path: path4,
9231
+ path: path5,
9124
9232
  message: `'vectros.${name}' is unavailable: contextId must be a literal string to derive built-ins`
9125
9233
  });
9126
9234
  return void 0;
@@ -9128,12 +9236,12 @@ function resolveRef(ns, name, ctx, path4) {
9128
9236
  return ctx.builtins[name];
9129
9237
  }
9130
9238
  ctx.issues.push({
9131
- path: path4,
9239
+ path: path5,
9132
9240
  message: `unknown namespace '${ns}' in '\${{ ${ns}.${name} }}' (available: inputs, vectros)`
9133
9241
  });
9134
9242
  return void 0;
9135
9243
  }
9136
- function interpolate(str, ctx, path4) {
9244
+ function interpolate(str, ctx, path5) {
9137
9245
  let out = "";
9138
9246
  let last = 0;
9139
9247
  TOKEN_SCAN_RE.lastIndex = 0;
@@ -9144,7 +9252,7 @@ function interpolate(str, ctx, path4) {
9144
9252
  out += "${{";
9145
9253
  } else if (m[0] === "${{" && m[1] === void 0) {
9146
9254
  ctx.issues.push({
9147
- path: path4,
9255
+ path: path5,
9148
9256
  message: `unterminated token '\${{' \u2014 expected a closing '}}' (escape a literal with '$\${{')`
9149
9257
  });
9150
9258
  } else {
@@ -9152,13 +9260,13 @@ function interpolate(str, ctx, path4) {
9152
9260
  const ref = inner.match(INNER_REF_RE);
9153
9261
  if (!ref) {
9154
9262
  ctx.issues.push({
9155
- path: path4,
9263
+ path: path5,
9156
9264
  message: `malformed reference '\${{ ${inner} }}' \u2014 expected '\${{ namespace.name }}' (escape a literal with '$\${{')`
9157
9265
  });
9158
9266
  } else if (DEFERRED_NAMESPACES.has(ref[1])) {
9159
9267
  out += m[0];
9160
9268
  } else {
9161
- const v = resolveRef(ref[1], ref[2], ctx, path4);
9269
+ const v = resolveRef(ref[1], ref[2], ctx, path5);
9162
9270
  if (v !== void 0) out += String(v);
9163
9271
  }
9164
9272
  }
@@ -9167,34 +9275,34 @@ function interpolate(str, ctx, path4) {
9167
9275
  out += str.slice(last);
9168
9276
  return out;
9169
9277
  }
9170
- function substituteValue(val, ctx, path4) {
9278
+ function substituteValue(val, ctx, path5) {
9171
9279
  if (typeof val === "string") {
9172
9280
  const whole = val.match(WHOLE_TOKEN_RE);
9173
9281
  if (whole) {
9174
9282
  if (DEFERRED_NAMESPACES.has(whole[1])) return val;
9175
- const v = resolveRef(whole[1], whole[2], ctx, path4);
9283
+ const v = resolveRef(whole[1], whole[2], ctx, path5);
9176
9284
  return v === void 0 ? val : v;
9177
9285
  }
9178
- return interpolate(val, ctx, path4);
9286
+ return interpolate(val, ctx, path5);
9179
9287
  }
9180
9288
  if (Array.isArray(val)) {
9181
- return val.map((v, i) => substituteValue(v, ctx, `${path4}[${i}]`));
9289
+ return val.map((v, i) => substituteValue(v, ctx, `${path5}[${i}]`));
9182
9290
  }
9183
9291
  if (val !== null && typeof val === "object") {
9184
9292
  const out = {};
9185
9293
  for (const [k, v] of Object.entries(val)) {
9186
- out[k] = substituteValue(v, ctx, path4 ? `${path4}.${k}` : k);
9294
+ out[k] = substituteValue(v, ctx, path5 ? `${path5}.${k}` : k);
9187
9295
  }
9188
9296
  return out;
9189
9297
  }
9190
9298
  return val;
9191
9299
  }
9192
9300
  function coerce(raw, type, name, issues) {
9193
- const path4 = `inputs.${name}`;
9301
+ const path5 = `inputs.${name}`;
9194
9302
  if (type === "string") {
9195
9303
  if (typeof raw === "string") return raw;
9196
9304
  if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
9197
- issues.push({ path: path4, message: `expected a string value` });
9305
+ issues.push({ path: path5, message: `expected a string value` });
9198
9306
  return void 0;
9199
9307
  }
9200
9308
  if (type === "number") {
@@ -9202,12 +9310,12 @@ function coerce(raw, type, name, issues) {
9202
9310
  if (typeof raw === "string") {
9203
9311
  const n = Number(raw.trim());
9204
9312
  if (raw.trim() === "" || Number.isNaN(n)) {
9205
- issues.push({ path: path4, message: `expected a number, got '${raw}'` });
9313
+ issues.push({ path: path5, message: `expected a number, got '${raw}'` });
9206
9314
  return void 0;
9207
9315
  }
9208
9316
  return n;
9209
9317
  }
9210
- issues.push({ path: path4, message: `expected a number` });
9318
+ issues.push({ path: path5, message: `expected a number` });
9211
9319
  return void 0;
9212
9320
  }
9213
9321
  if (typeof raw === "boolean") return raw;
@@ -9215,10 +9323,10 @@ function coerce(raw, type, name, issues) {
9215
9323
  const t = raw.trim().toLowerCase();
9216
9324
  if (t === "true") return true;
9217
9325
  if (t === "false") return false;
9218
- issues.push({ path: path4, message: `expected a boolean ('true'|'false'), got '${raw}'` });
9326
+ issues.push({ path: path5, message: `expected a boolean ('true'|'false'), got '${raw}'` });
9219
9327
  return void 0;
9220
9328
  }
9221
- issues.push({ path: path4, message: `expected a boolean` });
9329
+ issues.push({ path: path5, message: `expected a boolean` });
9222
9330
  return void 0;
9223
9331
  }
9224
9332
  function resolveValues(declared, supplied, issues) {
@@ -9670,9 +9778,20 @@ var DATA_PLANE_ACTIONS = [
9670
9778
  "folders:c"
9671
9779
  ];
9672
9780
  var EDITOR_ACTIONS = [...DATA_PLANE_ACTIONS, "records:d", "documents:d", "folders:d"];
9781
+ var MEMBER_SHARED_READ_ACTIONS = [
9782
+ "records:r:control",
9783
+ "records:r:convention",
9784
+ "records:r:gotcha",
9785
+ "records:r:term",
9786
+ "documents:r",
9787
+ "search:r",
9788
+ "inference:r",
9789
+ "schemas:r",
9790
+ "folders:r"
9791
+ ];
9673
9792
  var agenticSdlc = {
9674
9793
  name: "agentic-sdlc",
9675
- version: "1.3.0",
9794
+ version: "1.6.0",
9676
9795
  description: "A whole-SDLC system of record for an AI development team \u2014 decisions, designs, references, runbooks, post-mortems (as documents) plus controls, conventions, gotchas, and a glossary (as records), cross-linked and recalled by meaning.",
9677
9796
  contextId: "agentic-sdlc",
9678
9797
  contextName: "Agentic SDLC Knowledge Base",
@@ -10371,6 +10490,173 @@ var agenticSdlc = {
10371
10490
  { fieldName: "updatedOn", rangeEnabled: true },
10372
10491
  "sourceRef"
10373
10492
  ]
10493
+ },
10494
+ {
10495
+ // ISOLATED AGENT MEMORY (the "other half" of the KB). Distinct from the
10496
+ // shared records above — which are the team's GLOBAL, curated knowledge —
10497
+ // `memory` is per-principal: a fact an agent (or the human it works for)
10498
+ // records for ITSELF, isolated by ownership via a `${{ self.userId }}`
10499
+ // role dataScope (the private tier; a team tier keyed on a shared scope is
10500
+ // a deliberate future addition, not baked in this version). Deliberately a
10501
+ // SINGLE flexible type so the recall hook queries ONE typeName; the flavor
10502
+ // is the `kind` field, mirroring the file-memory taxonomy it replaces, so
10503
+ // migration is 1:1. Isolation is enforced by the ROLE (type-scoped
10504
+ // `records:*:memory` actions + owner dataScope), NOT the schema — the
10505
+ // schema is tier-agnostic.
10506
+ typeName: "memory",
10507
+ displayName: "Memory",
10508
+ indexMode: "HYBRID",
10509
+ // keyword on the title + semantic on the body (the recalled fact)
10510
+ fields: [
10511
+ {
10512
+ fieldId: "title",
10513
+ fieldType: "string",
10514
+ required: true,
10515
+ searchable: true,
10516
+ validation: { minLength: 1, maxLength: 200 },
10517
+ description: 'A short handle for the memory (the file-memory "name" slug analog).',
10518
+ renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
10519
+ },
10520
+ {
10521
+ fieldId: "body",
10522
+ fieldType: "string",
10523
+ searchable: true,
10524
+ description: "The memory itself \u2014 the durable fact, retrieved by meaning. The RAG-able body.",
10525
+ renderHints: { label: "Body", widget: "textarea", order: 2 }
10526
+ },
10527
+ {
10528
+ // The flavor of memory — mirrors the file-memory frontmatter `type:` so
10529
+ // a migration maps 1:1. `observation` is the episodic/scratch flavor
10530
+ // (TTL-able when the thread tier lands); the rest are durable.
10531
+ fieldId: "kind",
10532
+ fieldType: "enum",
10533
+ filterable: true,
10534
+ enumValues: ["user", "feedback", "project", "reference", "observation"],
10535
+ renderHints: { label: "Kind", widget: "select", order: 3 }
10536
+ },
10537
+ {
10538
+ // Structure axis 2 of 4: WHAT the memory is about — the same
10539
+ // subsystem/topic label every curated KB type carries, so one `area`
10540
+ // filter narrows recall across curated + memory content uniformly.
10541
+ fieldId: "area",
10542
+ fieldType: "string",
10543
+ filterable: true,
10544
+ description: `Subsystem/topic the memory applies to (e.g. "auth", "search") \u2014 matches the shared types' area vocabulary.`,
10545
+ renderHints: { label: "Area", widget: "text", order: 4 }
10546
+ },
10547
+ {
10548
+ // Structure axis 3: WHICH FUNCTION recorded it. A small stable ROLE
10549
+ // vocabulary (e.g. "pm", "builder", "reviewer") — not an instance id:
10550
+ // instances are already distinguished by the owning principal (who)
10551
+ // + threadId (which conversation).
10552
+ fieldId: "agent",
10553
+ fieldType: "string",
10554
+ filterable: true,
10555
+ description: 'The agent role that recorded it (e.g. "pm", "builder") \u2014 the function, not the instance.',
10556
+ renderHints: { label: "Agent", widget: "text", order: 5 }
10557
+ },
10558
+ {
10559
+ fieldId: "tags",
10560
+ fieldType: "array",
10561
+ filterable: true,
10562
+ description: 'Freeform labels for filtering recall (e.g. "auth", "testing").',
10563
+ renderHints: { label: "Tags", order: 6 }
10564
+ },
10565
+ {
10566
+ // The reversible supersede pattern shared with the records above — a
10567
+ // memory is retired by a status flip, never hard-deleted, so how the
10568
+ // agent's understanding evolved stays inspectable. Promotion to a
10569
+ // higher tier = supersede here + re-create at the new scope.
10570
+ fieldId: "status",
10571
+ fieldType: "enum",
10572
+ filterable: true,
10573
+ enumValues: ["active", "superseded"],
10574
+ renderHints: { label: "Status", widget: "select", order: 7 }
10575
+ },
10576
+ {
10577
+ // Structure axis 4: WHICH CONVERSATION (episodic slice). The caller's
10578
+ // session/thread identifier — whatever the agent runtime calls it. A
10579
+ // lookup filter WITHIN what the principal owns, NOT an isolation
10580
+ // boundary; sessions of the same principal share trust.
10581
+ fieldId: "threadId",
10582
+ fieldType: "string",
10583
+ filterable: true,
10584
+ description: "Optional conversation/thread id (your runtime's session identifier) \u2014 slices episodic memory within your own store.",
10585
+ renderHints: { label: "Thread", widget: "text", order: 8 }
10586
+ },
10587
+ {
10588
+ fieldId: "updatedOn",
10589
+ fieldType: "date",
10590
+ description: 'ISO-8601 \u2014 when last revised. Range-queryable \u2014 "what did I learn this week".',
10591
+ renderHints: { label: "Updated on", widget: "date", order: 9 }
10592
+ },
10593
+ {
10594
+ // Importance BAND for the always-load pinned set + recall ranking. Coarse bands
10595
+ // (0 normal / 10 pinned / 20 high / 30 critical) — pick a band, not a fiddly
10596
+ // integer, to avoid priority inflation. NULLABLE and set only on the elevated
10597
+ // subset, so the range-index cost lands only on that low-cardinality set (normal
10598
+ // memories write no range row). Orientation enumerates `priority order=desc
10599
+ // limit=X` — the read query IS the cap (overflow falls below the cutoff, never
10600
+ // evicted). MEMORY-ONLY: a pin is a memory that REFERENCES a curated item, it
10601
+ // never pins the curated item in place (pins = always-load; curated KB = recalled
10602
+ // when relevant).
10603
+ fieldId: "priority",
10604
+ fieldType: "number",
10605
+ filterable: true,
10606
+ description: "Importance band (0/10/20/30) for the always-load pinned set + recall ranking. Optional \u2014 set only on elevated memories.",
10607
+ renderHints: { label: "Priority", order: 10 }
10608
+ },
10609
+ {
10610
+ // Opaque provenance — the file / issue / URL this memory is ABOUT (records parity
10611
+ // with the curated types' sourceRef). Not a typed graph edge.
10612
+ fieldId: "sourceRef",
10613
+ fieldType: "string",
10614
+ description: "Provenance: the file path / issue / URL this memory is about.",
10615
+ renderHints: { label: "Source ref", widget: "text", order: 11 }
10616
+ },
10617
+ {
10618
+ // The evolution/promotion trail WITHIN memory (self-ref, SAME owner): the memory
10619
+ // that replaced this one. Pairs with ARCHIVE-retire. Promotion to the curated tier
10620
+ // is copy-up + archive-down with NO shared->private back-reference, so a curated
10621
+ // (shared) artifact never embeds a private memory's id/slug.
10622
+ fieldId: "supersededBy",
10623
+ fieldType: "reference",
10624
+ targetTypeName: "memory",
10625
+ targetSurface: "record",
10626
+ targetField: "externalId",
10627
+ cardinality: "one",
10628
+ description: "The memory that superseded this one (by externalId) \u2014 the evolution trail.",
10629
+ renderHints: { label: "Superseded by", order: 12 }
10630
+ },
10631
+ {
10632
+ // The `[[see-also]]` link between a principal's own memories (self-ref, SAME
10633
+ // owner). One-cardinality in this version (the proven reference shape); a
10634
+ // many-cardinality see-also graph is a later extension.
10635
+ fieldId: "relatedTo",
10636
+ fieldType: "reference",
10637
+ targetTypeName: "memory",
10638
+ targetSurface: "record",
10639
+ targetField: "externalId",
10640
+ cardinality: "one",
10641
+ description: "A related memory (by externalId) \u2014 the see-also link within your own store.",
10642
+ renderHints: { label: "Related to", order: 13 }
10643
+ }
10644
+ ],
10645
+ // externalId is the memory's stable slug (the file-memory `name:`) — the idempotent
10646
+ // upsert key — sent top-level, not declared as a field.
10647
+ // ⚖ LEAN by design: `memory` is the HIGHEST-VOLUME, churniest record here, so it only
10648
+ // carries lookups for fields we ENUMERATE DETERMINISTICLY. `area`/`agent`/`status` are
10649
+ // `filterable` (typed SEARCH metadata — narrow recall without a per-write lookup row),
10650
+ // NOT lookups; `sourceRef` is a plain provenance field. A typical write pays just
10651
+ // `kind` (eq) + `updatedOn` (range); `threadId`/`priority` cost only when set (both
10652
+ // nullable). Contrast the curated types, which carry more lookups because they are
10653
+ // low-volume and repo-synced (`sourceRef` enumeration).
10654
+ lookupFields: [
10655
+ "kind",
10656
+ "threadId",
10657
+ { fieldName: "updatedOn", rangeEnabled: true },
10658
+ { fieldName: "priority", rangeEnabled: true }
10659
+ ]
10374
10660
  }
10375
10661
  ],
10376
10662
  // Least-privilege, data-plane only. The scope of the `ssk_*` key the bootstrap
@@ -10393,7 +10679,41 @@ var agenticSdlc = {
10393
10679
  // owner may permanently remove genuine strays. Still no control-plane action, so
10394
10680
  // the scope gate accepts it as a data-plane-only role.
10395
10681
  roles: {
10396
- editor: [{ allowedActions: EDITOR_ACTIONS }]
10682
+ // The human curator. NOTE: this role is deliberately UNSCOPED (whole-context),
10683
+ // which includes members' private memory — the trusted-administrator tradeoff,
10684
+ // called out in the bundled guide. Scope it down if your deployment wants
10685
+ // curator access without private-memory visibility.
10686
+ editor: [{ allowedActions: EDITOR_ACTIONS }],
10687
+ // A team member (or their agent) — TWO memory tiers as two UNIONed clauses
10688
+ // (a record is accessible when ANY clause grants it; within one clause every
10689
+ // dataScope dimension must match):
10690
+ // 1) the CURATED shared KB (+ semantic recall). UNSCOPED, but safe: search
10691
+ // and grounded answers are enforced per row against the per-type action
10692
+ // qualifier, so recall over this clause admits only the content it grants
10693
+ // a TYPED read for (the curated record types + documents) — never the
10694
+ // `memory` type, so a member's recall never returns another principal's
10695
+ // private memory.
10696
+ // 2) PRIVATE memory: the member's OWN `memory` records, isolated per-request
10697
+ // by `${{ self.userId }}` (the principal dimension — the stable ownership
10698
+ // axis, unaffected by the evolution of shared/team scopes).
10699
+ // Team-shared *working* memory (a group-owned tier every member reads and
10700
+ // writes) is a planned addition, intentionally NOT in this version: it rides
10701
+ // the ownership axis for shared scopes, which is being finalized, and this
10702
+ // blueprint avoids baking a shape that is about to change. Bind with
10703
+ // `vectros join agentic-sdlc --role member`; verify a binding with
10704
+ // `vectros access explain --principal me --context agentic-sdlc`.
10705
+ member: [
10706
+ { allowedActions: MEMBER_SHARED_READ_ACTIONS },
10707
+ {
10708
+ // `inference:r` is IN this self-scoped clause (not only clause 1) so a
10709
+ // member's `rag_ask` can ground on its OWN private memory — the read
10710
+ // grant (`records:r:memory`) and the inference capability travel in the
10711
+ // SAME clause, so grounding works regardless of cross-clause admission
10712
+ // semantics. Still self-fenced by dataScope: never another principal's.
10713
+ allowedActions: ["records:cru:memory", "search:r", "inference:r"],
10714
+ dataScope: { userId: ["${{ self.userId }}"] }
10715
+ }
10716
+ ]
10397
10717
  },
10398
10718
  servicePrincipal: {
10399
10719
  externalId: "agentic-sdlc",
@@ -10821,13 +11141,13 @@ async function mintBridgeToken(opts) {
10821
11141
  fetchImpl = fetch
10822
11142
  } = opts;
10823
11143
  const base = apiBaseUrl.replace(/\/$/, "");
10824
- const path4 = bridgeTokenPath(useScopedToken);
10825
- const url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
11144
+ const path5 = bridgeTokenPath(useScopedToken);
11145
+ const url = `${base}${path5}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
10826
11146
  return fetchStToken(
10827
11147
  url,
10828
11148
  cognitoToken,
10829
11149
  fetchImpl,
10830
- path4,
11150
+ path5,
10831
11151
  (status) => status === 404 && !useScopedToken ? ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)` : ""
10832
11152
  );
10833
11153
  }
@@ -11333,6 +11653,54 @@ async function readPersistedKey(pack, dir = vectrosHomeDir()) {
11333
11653
  }
11334
11654
  }
11335
11655
 
11656
+ // src/identity-resolver.ts
11657
+ function userPayload(decl) {
11658
+ const meta = decl.metadata;
11659
+ if (decl.displayName === void 0) return meta;
11660
+ return { displayName: decl.displayName, ...meta ?? {} };
11661
+ }
11662
+ function requireId(id, kind, externalId) {
11663
+ if (typeof id !== "string" || id.length === 0) {
11664
+ throw new Error(`identity ${kind} '${externalId}' create returned no id \u2014 cannot substitute its token.`);
11665
+ }
11666
+ return id;
11667
+ }
11668
+ function makeIdentityResolver(client, log) {
11669
+ return async (name, decl) => {
11670
+ switch (decl.kind) {
11671
+ case "user": {
11672
+ const u = await client.identity.createUser({
11673
+ body: { externalId: decl.externalId, type: "HUMAN", payload: userPayload(decl) }
11674
+ });
11675
+ const id = requireId(u.id, "user", decl.externalId);
11676
+ log.debug({ component: "identity-resolver", name, kind: "user", externalId: decl.externalId, id }, "identity ensured");
11677
+ return id;
11678
+ }
11679
+ case "org": {
11680
+ const o = await client.identity.createOrg({
11681
+ body: { externalId: decl.externalId, name: decl.displayName, payload: decl.metadata }
11682
+ });
11683
+ const id = requireId(o.id, "org", decl.externalId);
11684
+ log.debug({ component: "identity-resolver", name, kind: "org", externalId: decl.externalId, id }, "identity ensured");
11685
+ return id;
11686
+ }
11687
+ case "client": {
11688
+ const c = await client.identity.createClient({
11689
+ body: { externalId: decl.externalId, name: decl.displayName, payload: decl.metadata }
11690
+ });
11691
+ const id = requireId(c.id, "client", decl.externalId);
11692
+ log.debug({ component: "identity-resolver", name, kind: "client", externalId: decl.externalId, id }, "identity ensured");
11693
+ return id;
11694
+ }
11695
+ }
11696
+ };
11697
+ }
11698
+ async function resolveApplyTimeIdentities(blueprint, client, log) {
11699
+ const resolver = makeIdentityResolver(client, log);
11700
+ const resolved = await resolveBlueprintIdentities(blueprint, resolver);
11701
+ return parseBlueprint(resolved);
11702
+ }
11703
+
11336
11704
  // src/loader.ts
11337
11705
  var LostKeyError = class extends Error {
11338
11706
  constructor(keyName) {
@@ -11424,7 +11792,7 @@ function assertNoUnresolvedIdentities(blueprint) {
11424
11792
  if (hasBlock || refs.length > 0) {
11425
11793
  const detail = hasBlock ? `declares an 'identities' block` : `references ${refs.map((r) => `\${{ identities.${r} }}`).join(", ")}`;
11426
11794
  throw new UnresolvedIdentitiesError(
11427
- `Blueprint ${detail}, but the CLI loader does not yet resolve identities. Applying it would write literal '\${{ identities.* }}' tokens into ownership/dataScope fields. Remove the identities usage, or wait for identity-resolution support.`
11795
+ `Blueprint ${detail} that reached the loader UNRESOLVED \u2014 apply-time identity resolution did not run before provisioning (an internal wiring error). Applying it would write literal '\${{ identities.* }}' tokens into ownership/dataScope fields. This is a defense-in-depth backstop; resolution happens in the CLI apply orchestrator (resolveApplyTimeIdentities) before the loader.`
11428
11796
  );
11429
11797
  }
11430
11798
  }
@@ -11496,7 +11864,12 @@ async function runLoader(client, input) {
11496
11864
  // to the SDK's loosely-typed Record<string, Record<string,unknown>>.
11497
11865
  data_scope: blueprint.accessProfile.dataScope
11498
11866
  }
11499
- ]
11867
+ ],
11868
+ // Optional identity overrides — scope values the service key STAMPS onto
11869
+ // everything it writes (e.g. the blueprint's team org, so the curated
11870
+ // knowledge it seeds/writes is org-owned). `${{ identities.* }}` tokens
11871
+ // were substituted before the loader ran.
11872
+ identityOverrides: blueprint.accessProfile.identityOverrides
11500
11873
  // NB: `status` is intentionally omitted — the SDK's body type does not model it
11501
11874
  // and the platform defaults a new profile to 'active' (the only status the loader
11502
11875
  // ever wants). Sending it was redundant.
@@ -11572,7 +11945,9 @@ async function runLoader(client, input) {
11572
11945
  schemaId,
11573
11946
  externalId: rec.externalId,
11574
11947
  // Optional structured payload (the document's `fields`); omitted when absent.
11575
- payload: rec.fields
11948
+ payload: rec.fields,
11949
+ // Optional scope ownership (`namespace:value` entries; `[]` = private).
11950
+ scopes: rec.scopes
11576
11951
  }
11577
11952
  });
11578
11953
  if (created.id) seededDocumentIds.push(created.id);
@@ -11582,7 +11957,8 @@ async function runLoader(client, input) {
11582
11957
  typeName: rec.typeName,
11583
11958
  schemaId,
11584
11959
  externalId: rec.externalId,
11585
- payload: rec.fields
11960
+ payload: rec.fields,
11961
+ scopes: rec.scopes
11586
11962
  }
11587
11963
  });
11588
11964
  if (created.id) seededRecordIds.push(created.id);
@@ -11743,7 +12119,6 @@ function validateBlueprint(input) {
11743
12119
  for (const clauses of Object.values(blueprint.roles ?? {})) {
11744
12120
  for (const clause of clauses) assertScopeGate(clause.allowedActions);
11745
12121
  }
11746
- assertNoUnresolvedIdentities(blueprint);
11747
12122
  return blueprint;
11748
12123
  }
11749
12124
  var BLANK_SLATE = {
@@ -11999,13 +12374,6 @@ ${BOOTSTRAP_HELP}
11999
12374
  process.stderr.write(" Minting nothing.\n");
12000
12375
  return 2;
12001
12376
  }
12002
- if (err instanceof UnresolvedIdentitiesError) {
12003
- process.stderr.write(`
12004
- \u2716 ${err.message}
12005
- Minting nothing.
12006
- `);
12007
- return 2;
12008
- }
12009
12377
  throw err;
12010
12378
  }
12011
12379
  const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
@@ -12030,6 +12398,14 @@ ${BOOTSTRAP_HELP}
12030
12398
  process.stdout.write(`
12031
12399
  ${renderPlan(blueprint, planSteps)}
12032
12400
  `);
12401
+ const declaredIdentities = Object.entries(blueprint.identities ?? {});
12402
+ if (declaredIdentities.length > 0) {
12403
+ process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
12404
+ for (const [name, decl] of declaredIdentities) {
12405
+ process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
12406
+ `);
12407
+ }
12408
+ }
12033
12409
  if (!args.yes && !args.print && isTty) {
12034
12410
  const ans = (await promptCtx.prompt("\n Apply this plan? [y/N]: ")).trim().toLowerCase();
12035
12411
  if (ans !== "y" && ans !== "yes") {
@@ -12054,17 +12430,25 @@ ${renderPlan(blueprint, planSteps)}
12054
12430
  });
12055
12431
  process.stdout.write(` \u2714 Bootstrap token minted (5-min TTL, ${args.tenant} tenant)
12056
12432
  `);
12433
+ const pingIdentity = deps.resolveIdentity ?? resolveIdentity;
12434
+ const resolveIdentities = deps.resolveApplyTimeIdentities ?? resolveApplyTimeIdentities;
12435
+ const loader = deps.runLoader ?? runLoader;
12057
12436
  const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
12058
- const identity = await resolveIdentity({ log, apiKey: bridge.token, environment: baseUrl });
12437
+ const identity = await pingIdentity({ log, apiKey: bridge.token, environment: baseUrl });
12059
12438
  const tenantId = identity.tenantId;
12060
12439
  if (!tenantId || typeof tenantId !== "string") {
12061
12440
  throw new Error(
12062
12441
  "Could not resolve tenantId from /v1/ping \u2014 the extended ping (tenant_id) must be deployed for bootstrap."
12063
12442
  );
12064
12443
  }
12065
- const machineId = await resolveMachineId();
12066
- const result = await runLoader(client, {
12444
+ const resolvedBlueprint = await resolveIdentities(
12067
12445
  blueprint,
12446
+ client,
12447
+ log
12448
+ );
12449
+ const machineId = await resolveMachineId();
12450
+ const result = await loader(client, {
12451
+ blueprint: resolvedBlueprint,
12068
12452
  tenantId,
12069
12453
  machineId,
12070
12454
  rotate: args.rotate,
@@ -12142,6 +12526,13 @@ ${codeAddHint}`);
12142
12526
  `);
12143
12527
  return 3;
12144
12528
  }
12529
+ if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
12530
+ process.stderr.write(`
12531
+ \u2716 ${err.message}
12532
+ No context or key was provisioned.
12533
+ `);
12534
+ return 2;
12535
+ }
12145
12536
  process.stderr.write(`
12146
12537
  \u2716 Bootstrap failed: ${err instanceof Error ? err.message : String(err)}
12147
12538
  `);
@@ -12421,13 +12812,6 @@ async function resolveAndValidate(arg, verb, supplied) {
12421
12812
  }
12422
12813
  throw err;
12423
12814
  }
12424
- try {
12425
- assertNoUnresolvedIdentities(blueprint);
12426
- } catch (err) {
12427
- process.stderr.write(` \u2716 ${err instanceof Error ? err.message : String(err)}
12428
- `);
12429
- return null;
12430
- }
12431
12815
  const findings = lintBlueprint(blueprint);
12432
12816
  for (const f of findings) {
12433
12817
  const mark = f.severity === "error" ? "\u2716" : "\u26A0";
@@ -12475,6 +12859,14 @@ async function runPlan(args) {
12475
12859
  process.stdout.write(`
12476
12860
  ${renderPlan(blueprint, steps)}
12477
12861
  `);
12862
+ const declaredIdentities = Object.entries(blueprint.identities ?? {});
12863
+ if (declaredIdentities.length > 0) {
12864
+ process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
12865
+ for (const [name, decl] of declaredIdentities) {
12866
+ process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
12867
+ `);
12868
+ }
12869
+ }
12478
12870
  const setEcho = [...args.set].join(" --set ");
12479
12871
  process.stdout.write(
12480
12872
  `
@@ -12673,6 +13065,18 @@ function normalizePrincipalUserId(principal) {
12673
13065
  function splitCsv(csv) {
12674
13066
  return csv.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
12675
13067
  }
13068
+ function parseJsonObjectFlag(flag, raw) {
13069
+ let parsed;
13070
+ try {
13071
+ parsed = JSON.parse(raw);
13072
+ } catch {
13073
+ throw new Error(`${flag} is not valid JSON: ${raw}`);
13074
+ }
13075
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
13076
+ throw new Error(`${flag} must be a JSON object (e.g. '{"userId":["<id>"]}').`);
13077
+ }
13078
+ return parsed;
13079
+ }
12676
13080
 
12677
13081
  // src/paginate.ts
12678
13082
  var PAGE_LIMIT = 100;
@@ -13168,6 +13572,23 @@ async function runAccessGrant(opts, deps = {}) {
13168
13572
  process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
13169
13573
  return 2;
13170
13574
  }
13575
+ if (hasActions && splitCsv(opts.actions).length === 0) {
13576
+ process.stderr.write(" \u2716 --actions needs at least one action (an empty/blank list grants nothing).\n");
13577
+ return 2;
13578
+ }
13579
+ let identityOverrides;
13580
+ if (opts.org || opts.identityOverrides) {
13581
+ try {
13582
+ identityOverrides = {
13583
+ ...opts.org ? { orgId: opts.org } : {},
13584
+ ...opts.identityOverrides ? parseJsonObjectFlag("--identity-overrides", opts.identityOverrides) : {}
13585
+ };
13586
+ } catch (err) {
13587
+ process.stderr.write(` \u2716 ${formatError(err)}
13588
+ `);
13589
+ return 2;
13590
+ }
13591
+ }
13171
13592
  if (opts.principal !== SELF_PRINCIPAL_ALIAS) {
13172
13593
  try {
13173
13594
  assertPrincipalShape(opts.principal);
@@ -13184,10 +13605,12 @@ async function runAccessGrant(opts, deps = {}) {
13184
13605
  const scopes = hasActions ? [{ allowed_actions: splitCsv(opts.actions) }] : void 0;
13185
13606
  const profile = await client.auth.createAccessProfile({
13186
13607
  contextId: opts.context,
13608
+ upsert: true,
13187
13609
  body: {
13188
13610
  principalId: principal,
13189
13611
  roleId: opts.role,
13190
- scopes
13612
+ scopes,
13613
+ identityOverrides
13191
13614
  }
13192
13615
  });
13193
13616
  if (opts.json) {
@@ -13310,6 +13733,166 @@ ${renderProfile(p)}
13310
13733
  });
13311
13734
  }
13312
13735
 
13736
+ // src/commands/access-explain.ts
13737
+ var SELF = /* @__PURE__ */ Symbol("self");
13738
+ function canonicalDim(key) {
13739
+ if (key === "userId" || key === "partnerUserId") return "user";
13740
+ if (key === "orgId") return "scope:org";
13741
+ if (key === "clientId") return "scope:client";
13742
+ return key;
13743
+ }
13744
+ var SELF_TOKEN_RE2 = /^\$\{\{\s*self\.[a-zA-Z.]+\s*\}\}$/;
13745
+ function normalizeClauses(raw) {
13746
+ const out = [];
13747
+ for (const c of raw) {
13748
+ if (!c || typeof c !== "object") continue;
13749
+ const obj = c;
13750
+ const actions = obj.allowed_actions ?? obj.allowedActions ?? [];
13751
+ const ds = obj.data_scope ?? obj.dataScope ?? {};
13752
+ const dataScope = {};
13753
+ for (const [k, v] of Object.entries(ds)) {
13754
+ const values = Array.isArray(v) ? v : [v];
13755
+ dataScope[canonicalDim(k)] = values.map((val) => {
13756
+ if (val === null) return null;
13757
+ const s = String(val);
13758
+ return SELF_TOKEN_RE2.test(s) ? SELF : s;
13759
+ });
13760
+ }
13761
+ out.push({ actions: Array.isArray(actions) ? actions.map(String) : [], dataScope });
13762
+ }
13763
+ return out;
13764
+ }
13765
+ function buildArchetypes(clauses) {
13766
+ const rows = [
13767
+ { label: "a record you own alone (private)", owners: { user: SELF } },
13768
+ { label: "another user's private record", owners: { user: "someone-else" } }
13769
+ ];
13770
+ const scopeDims = /* @__PURE__ */ new Map();
13771
+ for (const c of clauses) {
13772
+ for (const [dim, values] of Object.entries(c.dataScope)) {
13773
+ if (dim === "user") continue;
13774
+ const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
13775
+ for (const v of values) if (v !== null) set.add(v);
13776
+ scopeDims.set(dim, set);
13777
+ }
13778
+ }
13779
+ for (const [dim, values] of scopeDims) {
13780
+ for (const v of values) {
13781
+ const shown = v === SELF ? `your ${dim.replace("scope:", "")}` : `${dim}=${String(v)}`;
13782
+ rows.push({ label: `yours, shared to ${shown}`, owners: { user: SELF, [dim]: v } });
13783
+ rows.push({ label: `another user's, shared to ${shown}`, owners: { user: "someone-else", [dim]: v } });
13784
+ rows.push({ label: `${shown} only (no user owner)`, owners: { [dim]: v } });
13785
+ }
13786
+ rows.push({
13787
+ label: `another user's, in a DIFFERENT ${dim.replace("scope:", "")}`,
13788
+ owners: { user: "someone-else", [dim]: `other-${dim.replace("scope:", "")}` }
13789
+ });
13790
+ }
13791
+ rows.push({ label: "no owner values at all (tenant-shared)", owners: {} });
13792
+ return rows;
13793
+ }
13794
+ function clauseMatches(clause, arch) {
13795
+ for (const [dim, allowed] of Object.entries(clause.dataScope)) {
13796
+ const recordValue = arch.owners[dim];
13797
+ const ok = allowed.some((a) => {
13798
+ if (a === null) return recordValue === void 0;
13799
+ if (a === SELF) return recordValue === SELF;
13800
+ return recordValue === a;
13801
+ });
13802
+ if (!ok) return false;
13803
+ }
13804
+ return true;
13805
+ }
13806
+ function explainClauses(clauses) {
13807
+ const archetypes = buildArchetypes(clauses);
13808
+ return archetypes.map((arch) => {
13809
+ const matched = [];
13810
+ const actions = /* @__PURE__ */ new Set();
13811
+ clauses.forEach((c, i) => {
13812
+ if (clauseMatches(c, arch)) {
13813
+ matched.push(i + 1);
13814
+ for (const a of c.actions) actions.add(a);
13815
+ }
13816
+ });
13817
+ return { archetype: arch.label, clauses: matched, actions: [...actions] };
13818
+ });
13819
+ }
13820
+ function renderDataScope(ds) {
13821
+ const entries = Object.entries(ds);
13822
+ if (entries.length === 0) return "(unscoped \u2014 all data in the context)";
13823
+ return entries.map(
13824
+ ([dim, values]) => `${dim}:[${values.map((v) => v === SELF ? "self" : v === null ? "null" : v).join(", ")}]`
13825
+ ).join(" AND ");
13826
+ }
13827
+ var SELF_PRINCIPAL_ALIAS2 = "me";
13828
+ async function runAccessExplain(opts, deps = {}) {
13829
+ return withAuth(opts, deps, async (auth) => {
13830
+ const client = auth.client;
13831
+ try {
13832
+ let principal = opts.principal;
13833
+ if (principal === SELF_PRINCIPAL_ALIAS2) {
13834
+ const self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
13835
+ if (!self2) {
13836
+ process.stderr.write(
13837
+ " \u2716 Could not resolve your own user id from your login. Pass --principal usr_<id> explicitly.\n"
13838
+ );
13839
+ return 1;
13840
+ }
13841
+ principal = self2.principalId;
13842
+ }
13843
+ const profile = await client.auth.getAccessProfile({ contextId: opts.context, principalId: principal });
13844
+ let rawClauses = profile.scopes ?? [];
13845
+ let via = "inline scopes";
13846
+ if (profile.roleId) {
13847
+ const role = await client.auth.getRole({ contextId: opts.context, roleId: profile.roleId });
13848
+ rawClauses = role.scopes ?? [];
13849
+ via = `role '${profile.roleId}'`;
13850
+ }
13851
+ const clauses = normalizeClauses(rawClauses);
13852
+ const rows = explainClauses(clauses);
13853
+ if (opts.json) {
13854
+ process.stdout.write(
13855
+ `${formatJson({
13856
+ principal,
13857
+ context: opts.context,
13858
+ via,
13859
+ identityOverrides: profile.identityOverrides,
13860
+ clauses: clauses.map((c, i) => ({ n: i + 1, actions: c.actions, dataScope: renderDataScope(c.dataScope) })),
13861
+ matrix: rows,
13862
+ note: "Ownership dimensions only: per-type action qualifiers narrow further, and search/RAG visibility is bounded per row by the same clause's typed read grants. Client-side preview; the platform decides at request time."
13863
+ })}
13864
+ `
13865
+ );
13866
+ return 0;
13867
+ }
13868
+ process.stdout.write(`
13869
+ ${principal} in '${opts.context}' \u2014 via ${via}
13870
+ `);
13871
+ if (profile.identityOverrides && Object.keys(profile.identityOverrides).length > 0) {
13872
+ process.stdout.write(` identity overrides: ${JSON.stringify(profile.identityOverrides)}
13873
+ `);
13874
+ }
13875
+ process.stdout.write("\n Clauses (a record is accessible when ANY clause grants it):\n");
13876
+ clauses.forEach((c, i) => {
13877
+ process.stdout.write(` ${i + 1}. [${c.actions.join(", ")}] where ${renderDataScope(c.dataScope)}
13878
+ `);
13879
+ });
13880
+ process.stdout.write(
13881
+ "\n" + formatTable(rows, [
13882
+ { header: "record owned by\u2026", get: (r) => r.archetype },
13883
+ { header: "granted?", get: (r) => r.clauses.length ? `via clause ${r.clauses.join(", ")}` : "NO" },
13884
+ { header: "actions", get: (r) => r.actions.length ? r.actions.join(", ") : "\u2014" }
13885
+ ]) + "\n\n NOTE: this matrix models the OWNERSHIP dimensions only. Per-type qualifiers narrow further:\n a `records:<ops>:<type>` action reaches only that type, and search/RAG visibility is bounded\n per row by the SAME clause's typed read grants \u2014 a row shown with `search:r` is searchable\n only for the content types that clause can read.\n Preview only \u2014 computed client-side from the documented matching rules; the platform decides at request time.\n"
13886
+ );
13887
+ return 0;
13888
+ } catch (err) {
13889
+ process.stderr.write(` \u2716 access explain failed: ${formatError(err)}
13890
+ `);
13891
+ return 1;
13892
+ }
13893
+ });
13894
+ }
13895
+
13313
13896
  // src/commands/role.ts
13314
13897
  function renderRole(r) {
13315
13898
  return formatKeyValues([
@@ -13326,6 +13909,16 @@ async function runRoleCreate(opts, deps = {}) {
13326
13909
  process.stderr.write(" \u2716 --actions must list at least one action (e.g. records:cru,search:r).\n");
13327
13910
  return 2;
13328
13911
  }
13912
+ let dataScope;
13913
+ if (opts.dataScope !== void 0) {
13914
+ try {
13915
+ dataScope = parseJsonObjectFlag("--data-scope", opts.dataScope);
13916
+ } catch (err) {
13917
+ process.stderr.write(` \u2716 ${formatError(err)}
13918
+ `);
13919
+ return 2;
13920
+ }
13921
+ }
13329
13922
  return withAuth(opts, deps, async (auth) => {
13330
13923
  const client = auth.client;
13331
13924
  try {
@@ -13335,7 +13928,11 @@ async function runRoleCreate(opts, deps = {}) {
13335
13928
  roleId: opts.roleId,
13336
13929
  name: opts.name,
13337
13930
  description: opts.description,
13338
- scopes: [{ allowed_actions: actions }]
13931
+ // data_scope is included only when supplied — the clause wire shape
13932
+ // stays byte-identical for unscoped roles.
13933
+ scopes: [
13934
+ dataScope ? { allowed_actions: actions, data_scope: dataScope } : { allowed_actions: actions }
13935
+ ]
13339
13936
  }
13340
13937
  });
13341
13938
  if (opts.json) {
@@ -13967,7 +14564,12 @@ async function runTeardown(client, blueprint, machineId, snapshot, result) {
13967
14564
  return steps;
13968
14565
  }
13969
14566
  async function runBlueprintHarness(client, input, deps) {
13970
- const { blueprint, tenantId, machineId, vectrosDir, keep = false, log, remintForContext } = input;
14567
+ const { tenantId, machineId, vectrosDir, keep = false, log, remintForContext } = input;
14568
+ const blueprint = await resolveApplyTimeIdentities(
14569
+ input.blueprint,
14570
+ client,
14571
+ log
14572
+ );
13971
14573
  const remint = remintForContext ?? (async () => client);
13972
14574
  let snapshot;
13973
14575
  if (remintForContext) {
@@ -14141,11 +14743,461 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
14141
14743
  });
14142
14744
  }
14143
14745
 
14746
+ // src/keyring.ts
14747
+ import { promises as fs4 } from "fs";
14748
+ import path4 from "path";
14749
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, scryptSync } from "crypto";
14750
+ var KEYRING_FILE = "keyring.json";
14751
+ var SALT_FILE = "keyring.salt";
14752
+ var SECRETS_DIR = "secrets";
14753
+ var ALGO = "aes-256-gcm";
14754
+ var IV_LEN = 12;
14755
+ var TAG_LEN = 16;
14756
+ function keyringPath(dir = vectrosHomeDir()) {
14757
+ return path4.join(dir, KEYRING_FILE);
14758
+ }
14759
+ var CryptoFileSecretStore = class {
14760
+ dir;
14761
+ keyPromise;
14762
+ constructor(dir = vectrosHomeDir()) {
14763
+ this.dir = dir;
14764
+ }
14765
+ secretsDir() {
14766
+ return path4.join(this.dir, SECRETS_DIR);
14767
+ }
14768
+ secretPath(ref) {
14769
+ return path4.join(this.secretsDir(), `${encodeURIComponent(ref)}.enc`);
14770
+ }
14771
+ /** Resolve (and cache) the machine-derived AES key, creating the salt once. */
14772
+ async encryptionKey() {
14773
+ if (!this.keyPromise) {
14774
+ this.keyPromise = (async () => {
14775
+ const machineId = await resolveMachineId(this.dir);
14776
+ const salt = await this.readOrCreateSalt();
14777
+ return scryptSync(machineId, salt, 32);
14778
+ })();
14779
+ }
14780
+ return this.keyPromise;
14781
+ }
14782
+ async readOrCreateSalt() {
14783
+ const saltPath = path4.join(this.dir, SALT_FILE);
14784
+ try {
14785
+ const existing = await fs4.readFile(saltPath);
14786
+ if (existing.length >= 16) return existing;
14787
+ } catch {
14788
+ }
14789
+ const salt = randomBytes2(16);
14790
+ await fs4.mkdir(this.dir, { recursive: true });
14791
+ await fs4.writeFile(saltPath, salt, { mode: 384 });
14792
+ return salt;
14793
+ }
14794
+ async setSecret(ref, secret) {
14795
+ const key = await this.encryptionKey();
14796
+ const iv = randomBytes2(IV_LEN);
14797
+ const cipher = createCipheriv(ALGO, key, iv);
14798
+ const enc = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
14799
+ const tag = cipher.getAuthTag();
14800
+ const blob = Buffer.concat([iv, tag, enc]);
14801
+ await fs4.mkdir(this.secretsDir(), { recursive: true });
14802
+ await fs4.writeFile(this.secretPath(ref), blob.toString("base64"), {
14803
+ encoding: "utf8",
14804
+ mode: 384
14805
+ });
14806
+ }
14807
+ async getSecret(ref) {
14808
+ let blob;
14809
+ try {
14810
+ blob = Buffer.from((await fs4.readFile(this.secretPath(ref), "utf8")).trim(), "base64");
14811
+ } catch {
14812
+ return void 0;
14813
+ }
14814
+ if (blob.length <= IV_LEN + TAG_LEN) return void 0;
14815
+ try {
14816
+ const key = await this.encryptionKey();
14817
+ const iv = blob.subarray(0, IV_LEN);
14818
+ const tag = blob.subarray(IV_LEN, IV_LEN + TAG_LEN);
14819
+ const enc = blob.subarray(IV_LEN + TAG_LEN);
14820
+ const decipher = createDecipheriv(ALGO, key, iv);
14821
+ decipher.setAuthTag(tag);
14822
+ return Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8");
14823
+ } catch {
14824
+ return void 0;
14825
+ }
14826
+ }
14827
+ async deleteSecret(ref) {
14828
+ try {
14829
+ await fs4.rm(this.secretPath(ref));
14830
+ } catch {
14831
+ }
14832
+ }
14833
+ };
14834
+ async function readKeyring(dir = vectrosHomeDir()) {
14835
+ let raw;
14836
+ try {
14837
+ raw = await fs4.readFile(keyringPath(dir), "utf8");
14838
+ } catch (err) {
14839
+ if (err.code === "ENOENT") return { version: 1, entries: [] };
14840
+ throw err;
14841
+ }
14842
+ let parsed;
14843
+ try {
14844
+ parsed = JSON.parse(raw);
14845
+ } catch {
14846
+ throw new Error(`Keyring index at ${keyringPath(dir)} is corrupt (invalid JSON). Fix or remove the file.`);
14847
+ }
14848
+ if (!Array.isArray(parsed.entries)) {
14849
+ throw new Error(`Keyring index at ${keyringPath(dir)} is malformed (no entries array). Fix or remove the file.`);
14850
+ }
14851
+ return { version: 1, active: parsed.active, entries: parsed.entries };
14852
+ }
14853
+ async function writeKeyring(kr, dir = vectrosHomeDir()) {
14854
+ await fs4.mkdir(dir, { recursive: true });
14855
+ const finalPath = keyringPath(dir);
14856
+ const tmpPath = `${finalPath}.${process.pid}.${randomBytes2(4).toString("hex")}.tmp`;
14857
+ await fs4.writeFile(tmpPath, `${JSON.stringify(kr, null, 2)}
14858
+ `, { encoding: "utf8", mode: 384 });
14859
+ await fs4.rename(tmpPath, finalPath);
14860
+ }
14861
+ var Keyring = class {
14862
+ dir;
14863
+ store;
14864
+ constructor(deps = {}) {
14865
+ this.dir = deps.dir ?? vectrosHomeDir();
14866
+ this.store = deps.store ?? new CryptoFileSecretStore(this.dir);
14867
+ }
14868
+ /**
14869
+ * This machine's stable id — used to namespace a scoped-key NAME so each
14870
+ * machine holds its OWN independently-rotatable `ssk_*` under one shared
14871
+ * (idempotent) AccessProfile (see `machine-id.ts`). Resolved from the SAME
14872
+ * dir the secrets live in, so a test keyring rooted at a temp dir stays
14873
+ * self-consistent.
14874
+ */
14875
+ async machineId() {
14876
+ return resolveMachineId(this.dir);
14877
+ }
14878
+ async list() {
14879
+ return (await readKeyring(this.dir)).entries;
14880
+ }
14881
+ async get(alias) {
14882
+ return (await this.list()).find((e) => e.alias === alias);
14883
+ }
14884
+ async activeAlias() {
14885
+ return (await readKeyring(this.dir)).active;
14886
+ }
14887
+ async getActive() {
14888
+ const kr = await readKeyring(this.dir);
14889
+ return kr.active ? kr.entries.find((e) => e.alias === kr.active) : void 0;
14890
+ }
14891
+ /**
14892
+ * Upsert an entry (by alias) and store its secret. Activates it by default —
14893
+ * `join` wants the freshly-created identity to become current.
14894
+ */
14895
+ async add(entry, secret, opts = {}) {
14896
+ const kr = await readKeyring(this.dir);
14897
+ const prior = kr.entries.find((e) => e.alias === entry.alias);
14898
+ await this.store.setSecret(entry.secretRef, secret);
14899
+ if (prior && prior.secretRef !== entry.secretRef) {
14900
+ await this.store.deleteSecret(prior.secretRef);
14901
+ }
14902
+ const entries = kr.entries.filter((e) => e.alias !== entry.alias);
14903
+ entries.push(entry);
14904
+ const active = opts.activate === false ? kr.active : entry.alias;
14905
+ await writeKeyring({ version: 1, active, entries }, this.dir);
14906
+ }
14907
+ /** Point the active alias at an existing entry. Throws if the alias is unknown. */
14908
+ async setActive(alias) {
14909
+ const kr = await readKeyring(this.dir);
14910
+ if (!kr.entries.some((e) => e.alias === alias)) {
14911
+ throw new Error(`No keyring entry named '${alias}'. Run \`vectros keyring list\` to see entries.`);
14912
+ }
14913
+ await writeKeyring({ ...kr, version: 1, active: alias }, this.dir);
14914
+ }
14915
+ /** Remove an entry + its secret. Clears the active pointer if it named this alias. Returns true if removed. */
14916
+ async remove(alias) {
14917
+ const kr = await readKeyring(this.dir);
14918
+ const entry = kr.entries.find((e) => e.alias === alias);
14919
+ if (!entry) return false;
14920
+ await this.store.deleteSecret(entry.secretRef);
14921
+ const entries = kr.entries.filter((e) => e.alias !== alias);
14922
+ const active = kr.active === alias ? void 0 : kr.active;
14923
+ await writeKeyring({ version: 1, active, entries }, this.dir);
14924
+ return true;
14925
+ }
14926
+ /** The raw secret for a named entry (via the secret store), or undefined. */
14927
+ async resolveSecret(alias) {
14928
+ const entry = await this.get(alias);
14929
+ return entry ? this.store.getSecret(entry.secretRef) : void 0;
14930
+ }
14931
+ /** The raw secret for the active entry, or undefined if none is active. */
14932
+ async resolveActiveSecret() {
14933
+ const entry = await this.getActive();
14934
+ return entry ? this.store.getSecret(entry.secretRef) : void 0;
14935
+ }
14936
+ };
14937
+
14938
+ // src/commands/join.ts
14939
+ var SELF_ALIAS = "me";
14940
+ async function resolvePrincipal(as, auth) {
14941
+ const raw = as ?? SELF_ALIAS;
14942
+ if (raw === SELF_ALIAS) {
14943
+ const self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
14944
+ if (!self2) {
14945
+ throw new Error(
14946
+ "Could not resolve your own user id from your login. Pass --as usr_<id> explicitly (find it with `vectros identity list --type user`)."
14947
+ );
14948
+ }
14949
+ return self2.principalId;
14950
+ }
14951
+ if (!raw.startsWith("usr_")) {
14952
+ throw new Error(`--as must be 'me' or 'usr_<id>' (got '${raw}').`);
14953
+ }
14954
+ return raw;
14955
+ }
14956
+ async function issueOrRotateKey(client, tenantId, context, userId, keyName) {
14957
+ const first = await client.auth.createScopedKey({ keyName, tenantId, contextId: context, userId });
14958
+ if (first.rawKey) return first;
14959
+ const existing = (await drainPages(() => client.auth.listScopedKeys())).find(
14960
+ (k) => k.contextId === context && k.userId === userId && k.keyName === keyName && k.status === "active"
14961
+ );
14962
+ if (existing?.keyId) await client.auth.revokeScopedKey({ keyId: existing.keyId });
14963
+ const rotated = await client.auth.createScopedKey({ keyName, tenantId, contextId: context, userId });
14964
+ if (!rotated.rawKey) {
14965
+ throw new Error(
14966
+ "The key already exists and its secret could not be re-minted (the prior key may not have cleared yet). Retry shortly."
14967
+ );
14968
+ }
14969
+ return rotated;
14970
+ }
14971
+ async function runJoin(opts, deps = {}) {
14972
+ const hasRole = Boolean(opts.role);
14973
+ const hasActions = Boolean(opts.actions);
14974
+ if (hasRole === hasActions) {
14975
+ process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
14976
+ return 2;
14977
+ }
14978
+ if (hasActions && splitCsv(opts.actions).length === 0) {
14979
+ process.stderr.write(" \u2716 --actions needs at least one action (an empty/blank list grants nothing).\n");
14980
+ return 2;
14981
+ }
14982
+ if (opts.as && opts.as !== SELF_ALIAS && !opts.as.startsWith("usr_")) {
14983
+ process.stderr.write(` \u2716 --as must be 'me' or 'usr_<id>' (got '${opts.as}').
14984
+ `);
14985
+ return 2;
14986
+ }
14987
+ const context = opts.contextArg;
14988
+ const alias = opts.alias ?? context;
14989
+ const keyring = deps.keyring ?? new Keyring();
14990
+ return withAuth({ ...opts, context }, deps, async (auth) => {
14991
+ const client = auth.client;
14992
+ try {
14993
+ const tenantId = requireTenantId(auth);
14994
+ const principalId = await resolvePrincipal(opts.as, auth);
14995
+ const userId = normalizePrincipalUserId(principalId);
14996
+ const scopes = hasActions ? [{ allowed_actions: splitCsv(opts.actions) }] : void 0;
14997
+ await client.auth.createAccessProfile({
14998
+ contextId: context,
14999
+ upsert: true,
15000
+ body: {
15001
+ principalId,
15002
+ roleId: opts.role,
15003
+ scopes,
15004
+ identityOverrides: opts.org ? { orgId: opts.org } : void 0
15005
+ }
15006
+ });
15007
+ const existingEntry = await keyring.get(alias);
15008
+ const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
15009
+ const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
15010
+ if (canReuse && existingEntry) {
15011
+ await keyring.setActive(alias);
15012
+ emitJoined(alias, context, principalId, opts.role, existingEntry.keyId, true);
15013
+ return 0;
15014
+ }
15015
+ if (existingEntry) {
15016
+ process.stdout.write(
15017
+ `
15018
+ \u26A0 Replacing keyring entry '${alias}' (was ${existingEntry.principalId} in '${existingEntry.context}' @ ${existingEntry.env}). Pass --alias to keep both.
15019
+ `
15020
+ );
15021
+ }
15022
+ const keyName = keyNameFor(context, await keyring.machineId());
15023
+ const key = await issueOrRotateKey(client, tenantId, context, userId, keyName);
15024
+ if (!key.keyId) {
15025
+ throw new Error(
15026
+ "The minted key has no id \u2014 it could not be recorded or later rotated. Retry, or use `vectros key issue`."
15027
+ );
15028
+ }
15029
+ const entry = {
15030
+ alias,
15031
+ context,
15032
+ principalId,
15033
+ role: opts.role,
15034
+ keyId: key.keyId,
15035
+ env: auth.env,
15036
+ secretRef: key.keyId,
15037
+ createdAt: nowIso()
15038
+ };
15039
+ await keyring.add(entry, key.rawKey, { activate: true });
15040
+ emitJoined(alias, context, principalId, opts.role, entry.keyId, false);
15041
+ return 0;
15042
+ } catch (err) {
15043
+ process.stderr.write(` \u2716 join failed: ${formatError(err)}
15044
+ `);
15045
+ return 1;
15046
+ }
15047
+ });
15048
+ }
15049
+ function nowIso() {
15050
+ return (/* @__PURE__ */ new Date()).toISOString();
15051
+ }
15052
+ function emitJoined(alias, context, principalId, role, keyId, reused) {
15053
+ process.stdout.write(
15054
+ `
15055
+ \u2714 Joined '${context}' as ${principalId}` + (reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
15056
+ ["alias", alias],
15057
+ ["context", context],
15058
+ ["principal", principalId],
15059
+ ["role", role],
15060
+ ["key id", keyId]
15061
+ ]) + `
15062
+
15063
+ Now active. Use \`vectros switch ${alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
15064
+ `
15065
+ );
15066
+ }
15067
+
15068
+ // src/commands/keyring.ts
15069
+ async function runKeyringList(opts, deps = {}) {
15070
+ const keyring = deps.keyring ?? new Keyring();
15071
+ try {
15072
+ const entries = await keyring.list();
15073
+ const active = await keyring.activeAlias();
15074
+ if (opts.json) {
15075
+ process.stdout.write(`${formatJson({ active, entries })}
15076
+ `);
15077
+ return 0;
15078
+ }
15079
+ if (entries.length === 0) {
15080
+ process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
15081
+ return 0;
15082
+ }
15083
+ process.stdout.write(
15084
+ "\n" + formatTable(entries, [
15085
+ { header: "", get: (e) => e.alias === active ? "*" : "" },
15086
+ { header: "alias", get: (e) => e.alias },
15087
+ { header: "context", get: (e) => e.context },
15088
+ { header: "principal", get: (e) => e.principalId },
15089
+ { header: "role", get: (e) => e.role },
15090
+ { header: "env", get: (e) => e.env },
15091
+ { header: "key id", get: (e) => e.keyId }
15092
+ ]) + "\n"
15093
+ );
15094
+ return 0;
15095
+ } catch (err) {
15096
+ process.stderr.write(` \u2716 keyring list failed: ${formatError(err)}
15097
+ `);
15098
+ return 1;
15099
+ }
15100
+ }
15101
+ async function runKeyringUse(alias, opts, deps = {}) {
15102
+ const keyring = deps.keyring ?? new Keyring();
15103
+ try {
15104
+ await keyring.setActive(alias);
15105
+ if (opts.json) {
15106
+ process.stdout.write(`${formatJson({ active: alias })}
15107
+ `);
15108
+ return 0;
15109
+ }
15110
+ process.stdout.write(`
15111
+ \u2714 Active identity is now '${alias}'.
15112
+ `);
15113
+ return 0;
15114
+ } catch (err) {
15115
+ process.stderr.write(` \u2716 ${formatError(err)}
15116
+ `);
15117
+ return 2;
15118
+ }
15119
+ }
15120
+ async function runKeyringRemove(alias, opts, deps = {}) {
15121
+ const keyring = deps.keyring ?? new Keyring();
15122
+ try {
15123
+ const removed = await keyring.remove(alias);
15124
+ if (!removed) {
15125
+ process.stderr.write(` \u2716 No keyring entry named '${alias}'.
15126
+ `);
15127
+ return 2;
15128
+ }
15129
+ if (opts.json) {
15130
+ process.stdout.write(`${formatJson({ removed: alias })}
15131
+ `);
15132
+ return 0;
15133
+ }
15134
+ process.stdout.write(`
15135
+ \u2714 Removed '${alias}' from the keyring.
15136
+ `);
15137
+ return 0;
15138
+ } catch (err) {
15139
+ process.stderr.write(` \u2716 keyring remove failed: ${formatError(err)}
15140
+ `);
15141
+ return 1;
15142
+ }
15143
+ }
15144
+ async function runKeyringShow(opts, deps = {}) {
15145
+ const keyring = deps.keyring ?? new Keyring();
15146
+ try {
15147
+ const entry = opts.alias ? await keyring.get(opts.alias) : await keyring.getActive();
15148
+ if (!entry) {
15149
+ process.stderr.write(
15150
+ opts.alias ? ` \u2716 No keyring entry named '${opts.alias}'.
15151
+ ` : " \u2716 No active identity. Run `vectros join <context>` or `vectros switch <alias>`.\n"
15152
+ );
15153
+ return 2;
15154
+ }
15155
+ const secret = await keyring.resolveSecret(entry.alias);
15156
+ if (!secret) {
15157
+ process.stderr.write(
15158
+ ` \u2716 The stored secret for '${entry.alias}' is missing or unreadable. Re-issue it with \`vectros join ${entry.context} --alias ${entry.alias}\`.
15159
+ `
15160
+ );
15161
+ return 1;
15162
+ }
15163
+ switch (opts.format) {
15164
+ case "raw":
15165
+ process.stdout.write(`${secret}
15166
+ `);
15167
+ return 0;
15168
+ case "env":
15169
+ process.stdout.write(`VECTROS_API_KEY=${secret}
15170
+ `);
15171
+ return 0;
15172
+ case "human":
15173
+ process.stdout.write(
15174
+ "\n" + formatKeyValues([
15175
+ ["alias", entry.alias],
15176
+ ["context", entry.context],
15177
+ ["principal", entry.principalId],
15178
+ ["role", entry.role],
15179
+ ["env", entry.env],
15180
+ ["key id", entry.keyId]
15181
+ ]) + `
15182
+
15183
+ VECTROS_API_KEY=${secret}
15184
+ \u26A0 This is a live credential \u2014 treat it like a password.
15185
+ `
15186
+ );
15187
+ return 0;
15188
+ }
15189
+ } catch (err) {
15190
+ process.stderr.write(` \u2716 keyring show failed: ${formatError(err)}
15191
+ `);
15192
+ return 1;
15193
+ }
15194
+ }
15195
+
14144
15196
  // src/build-info.ts
14145
15197
  var BUILD_INFO = {
14146
- cli: true ? "0.7.0" : "dev",
14147
- sdk: true ? "0.33.0" : "dev",
14148
- blueprints: true ? "0.6.5" : "dev"
15198
+ cli: true ? "0.9.0" : "dev",
15199
+ sdk: true ? "0.34.0" : "dev",
15200
+ blueprints: true ? "0.7.0" : "dev"
14149
15201
  };
14150
15202
  function formatBuildInfo() {
14151
15203
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -14215,8 +15267,56 @@ function buildProgram(deps = {}) {
14215
15267
  registerAccessCommands(program, deps);
14216
15268
  registerRoleCommands(program, deps);
14217
15269
  registerContextCommands(program, deps);
15270
+ registerJoinCommand(program, deps);
15271
+ registerKeyringCommands(program);
14218
15272
  return program;
14219
15273
  }
15274
+ function registerJoinCommand(program, deps) {
15275
+ const join4 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--org <orgId>", "identity override: the org the member acts under (enables org/team-scoped writes)").option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
15276
+ addAuthOptions(join4).action(
15277
+ async (contextArg, flags) => {
15278
+ if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
15279
+ process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
15280
+ `);
15281
+ process.exit(2);
15282
+ }
15283
+ process.exit(
15284
+ await runJoin(
15285
+ {
15286
+ ...toCommonOptions(flags),
15287
+ contextArg,
15288
+ role: flags.role,
15289
+ actions: flags.actions,
15290
+ as: flags.as,
15291
+ org: flags.org,
15292
+ alias: flags.alias,
15293
+ tenant: flags.tenant
15294
+ },
15295
+ deps
15296
+ )
15297
+ );
15298
+ }
15299
+ );
15300
+ }
15301
+ function registerKeyringCommands(program) {
15302
+ const KEYRING_SHOW_FORMATS = ["human", "env", "raw"];
15303
+ const keyring = program.command("keyring").description("Inspect and manage the local multi-principal keyring.");
15304
+ keyring.command("list").description("List keyring entries and which one is active.").option("--json", "emit machine-readable JSON").action(async (flags) => {
15305
+ process.exit(await runKeyringList({ json: Boolean(flags.json) }));
15306
+ });
15307
+ keyring.command("use").description("Make an entry the active identity.").argument("<alias>", "the keyring entry to activate").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
15308
+ process.exit(await runKeyringUse(alias, { json: Boolean(flags.json) }));
15309
+ });
15310
+ keyring.command("show").description("Reveal the active (or a named) entry\u2019s key.").option("--alias <name>", "show this entry instead of the active one").addOption(new Option("--format <fmt>", "how to emit the key").choices(KEYRING_SHOW_FORMATS).default("human")).action(async (flags) => {
15311
+ process.exit(await runKeyringShow({ alias: flags.alias, format: flags.format }));
15312
+ });
15313
+ keyring.command("remove").description("Drop an entry and its stored secret.").argument("<alias>", "the keyring entry to remove").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
15314
+ process.exit(await runKeyringRemove(alias, { json: Boolean(flags.json) }));
15315
+ });
15316
+ program.command("switch").description("Shortcut for `keyring use` \u2014 flip the active identity.").argument("<alias>", "the keyring entry to activate").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
15317
+ process.exit(await runKeyringUse(alias, { json: Boolean(flags.json) }));
15318
+ });
15319
+ }
14220
15320
  function registerContextCommands(program, deps) {
14221
15321
  const context = program.command("context").description("Manage app contexts (the namespace all data lives in).");
14222
15322
  const create = context.command("create").description("Create (idempotent) an app context.").argument("<contextId>", "context id (3-31 chars; lowercase letter + lowercase/digit/dash)").option("--name <name>", "human-readable name").option("--tenant <tenant>", "tenant to create the context in: 'test' or 'live' (default live)");
@@ -14291,7 +15391,10 @@ function registerContextCommands(program, deps) {
14291
15391
  }
14292
15392
  function registerRoleCommands(program, deps) {
14293
15393
  const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
14294
- const create = role.command("create").description("Create (idempotent by roleId) a single-clause role from --actions.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--name <name>", "human-readable name").requiredOption("--actions <csv>", "allowed actions, comma-separated (e.g. records:cru,search:r)").option("--description <text>", "free-text description");
15394
+ const create = role.command("create").description("Create (idempotent by roleId) a single-clause role from --actions.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--name <name>", "human-readable name").requiredOption("--actions <csv>", "allowed actions, comma-separated (e.g. records:cru,search:r)").option(
15395
+ "--data-scope <json>",
15396
+ `confine the clause to owned data \u2014 JSON map of dimension \u2192 allowed values, e.g. '{"userId":["\${{ self.userId }}"]}' or '{"scope:org":["<orgId>", null]}'`
15397
+ ).option("--description <text>", "free-text description");
14295
15398
  addCommonAuthOptions(create).action(
14296
15399
  async (flags) => {
14297
15400
  process.exit(
@@ -14301,6 +15404,7 @@ function registerRoleCommands(program, deps) {
14301
15404
  roleId: flags.roleId,
14302
15405
  name: flags.name,
14303
15406
  actions: flags.actions,
15407
+ dataScope: flags.dataScope,
14304
15408
  description: flags.description
14305
15409
  }, deps)
14306
15410
  );
@@ -14321,7 +15425,7 @@ function registerRoleCommands(program, deps) {
14321
15425
  }
14322
15426
  function registerAccessCommands(program, deps) {
14323
15427
  const access2 = program.command("access").description("Manage access bindings: principal \u2194 context \u2194 role/scopes.");
14324
- const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (XOR --actions)").option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)");
15428
+ const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (XOR --actions)").option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)").option("--org <orgId>", "identity override: the org this principal acts under (enables org/team-scoped writes)").option("--identity-overrides <json>", "general identity overrides as a JSON object (merged over --org)");
14325
15429
  addCommonAuthOptions(grant).action(
14326
15430
  async (flags) => {
14327
15431
  process.exit(
@@ -14330,7 +15434,9 @@ function registerAccessCommands(program, deps) {
14330
15434
  principal: flags.principal,
14331
15435
  context: flags.context,
14332
15436
  role: flags.role,
14333
- actions: flags.actions
15437
+ actions: flags.actions,
15438
+ org: flags.org,
15439
+ identityOverrides: flags.identityOverrides
14334
15440
  }, deps)
14335
15441
  );
14336
15442
  }
@@ -14347,6 +15453,12 @@ function registerAccessCommands(program, deps) {
14347
15453
  addCommonAuthOptions(get).action(async (flags) => {
14348
15454
  process.exit(await runAccessGet({ ...toCommonOptions(flags), principal: flags.principal, context: flags.context }, deps));
14349
15455
  });
15456
+ const explain = access2.command("explain").description("Show what a principal\u2019s binding grants: its clauses + a concrete access matrix.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context");
15457
+ addCommonAuthOptions(explain).action(async (flags) => {
15458
+ process.exit(
15459
+ await runAccessExplain({ ...toCommonOptions(flags), principal: flags.principal, context: flags.context }, deps)
15460
+ );
15461
+ });
14350
15462
  }
14351
15463
  function dimensionOption() {
14352
15464
  return new Option("--type <dim>", "principal dimension").choices(DIMENSIONS).makeOptionMandatory();