@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/CHANGELOG.md +67 -0
- package/README.md +7 -0
- package/dist/cli.js +1199 -87
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1199 -87
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
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,
|
|
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} ${
|
|
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: ["
|
|
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: ["
|
|
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 `
|
|
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 {
|
|
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,
|
|
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:
|
|
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, [...
|
|
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 ||
|
|
8928
|
-
walk(v, [...
|
|
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,
|
|
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:
|
|
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, [...
|
|
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 (
|
|
8957
|
-
walk(v, [...
|
|
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(
|
|
9049
|
+
function formatIssuePath(path5) {
|
|
9005
9050
|
let out = "";
|
|
9006
|
-
for (const seg of
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
9286
|
+
return interpolate(val, ctx, path5);
|
|
9179
9287
|
}
|
|
9180
9288
|
if (Array.isArray(val)) {
|
|
9181
|
-
return val.map((v, i) => substituteValue(v, ctx, `${
|
|
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,
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
9326
|
+
issues.push({ path: path5, message: `expected a boolean ('true'|'false'), got '${raw}'` });
|
|
9219
9327
|
return void 0;
|
|
9220
9328
|
}
|
|
9221
|
-
issues.push({ path:
|
|
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.
|
|
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
|
-
|
|
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
|
|
10825
|
-
const url = `${base}${
|
|
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
|
-
|
|
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}
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
14147
|
-
sdk: true ? "0.
|
|
14148
|
-
blueprints: true ? "0.
|
|
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(
|
|
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();
|