@stigmer/server 3.19.0 → 3.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authorization/model/index.d.ts +1 -1
- package/dist/authorization/model/index.d.ts.map +1 -1
- package/dist/authorization/model/index.js +0 -2
- package/dist/authorization/model/index.js.map +1 -1
- package/dist/authorization/model/organization.d.ts.map +1 -1
- package/dist/authorization/model/organization.js +0 -1
- package/dist/authorization/model/organization.js.map +1 -1
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +2 -15
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/inprocess.d.ts +2 -13
- package/dist/boot/inprocess.d.ts.map +1 -1
- package/dist/boot/inprocess.js +6 -16
- package/dist/boot/inprocess.js.map +1 -1
- package/dist/domain/workflow/registry/data/task-kind-registry.json +0 -1
- package/dist/pipeline/interceptors/auth.d.ts +1 -1
- package/dist/pipeline/interceptors/auth.js +1 -1
- package/dist/query/search/registry.d.ts +3 -5
- package/dist/query/search/registry.d.ts.map +1 -1
- package/dist/query/search/registry.js +3 -7
- package/dist/query/search/registry.js.map +1 -1
- package/dist/store/postgres/migrations.d.ts +3 -1
- package/dist/store/postgres/migrations.d.ts.map +1 -1
- package/dist/store/postgres/migrations.js +19 -1
- package/dist/store/postgres/migrations.js.map +1 -1
- package/dist/store/sqlite/migrations.d.ts +3 -1
- package/dist/store/sqlite/migrations.d.ts.map +1 -1
- package/dist/store/sqlite/migrations.js +22 -1
- package/dist/store/sqlite/migrations.js.map +1 -1
- package/package.json +6 -6
- package/src/authorization/__tests__/derived-tuples.test.ts +1 -1
- package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +6 -6
- package/src/authorization/__tests__/wire-permissions.test.ts +0 -1
- package/src/authorization/model/__tests__/registry.test.ts +0 -9
- package/src/authorization/model/index.ts +1 -3
- package/src/authorization/model/organization.ts +0 -1
- package/src/boot/compose.ts +2 -15
- package/src/boot/inprocess.ts +8 -29
- package/src/domain/iampolicy/__tests__/role-lifecycle.test.ts +0 -1
- package/src/domain/workflow/registry/data/task-kind-registry.json +0 -1
- package/src/pipeline/__tests__/router-transport.test.ts +1 -1
- package/src/pipeline/interceptors/auth.ts +1 -1
- package/src/query/__tests__/query-services.test.ts +16 -18
- package/src/query/search/__tests__/criteria.test.ts +3 -6
- package/src/query/search/__tests__/query-store.test.ts +21 -21
- package/src/query/search/registry.ts +3 -7
- package/src/store/postgres/__tests__/migrations.test.ts +23 -4
- package/src/store/postgres/migrations.ts +20 -1
- package/src/store/sqlite/__tests__/migrations.test.ts +26 -2
- package/src/store/sqlite/migrations.ts +23 -1
- package/dist/authorization/model/project.d.ts +0 -2
- package/dist/authorization/model/project.d.ts.map +0 -1
- package/dist/authorization/model/project.js +0 -32
- package/dist/authorization/model/project.js.map +0 -1
- package/dist/domain/project/controller.d.ts +0 -48
- package/dist/domain/project/controller.d.ts.map +0 -1
- package/dist/domain/project/controller.js +0 -192
- package/dist/domain/project/controller.js.map +0 -1
- package/dist/domain/project/reconcile.d.ts +0 -77
- package/dist/domain/project/reconcile.d.ts.map +0 -1
- package/dist/domain/project/reconcile.js +0 -179
- package/dist/domain/project/reconcile.js.map +0 -1
- package/dist/domain/project/search-extractor.d.ts +0 -3
- package/dist/domain/project/search-extractor.d.ts.map +0 -1
- package/dist/domain/project/search-extractor.js +0 -43
- package/dist/domain/project/search-extractor.js.map +0 -1
- package/src/authorization/model/project.ts +0 -47
- package/src/domain/project/__tests__/project.composed.test.ts +0 -381
- package/src/domain/project/__tests__/reconcile.test.ts +0 -447
- package/src/domain/project/controller.ts +0 -387
- package/src/domain/project/reconcile.ts +0 -296
- package/src/domain/project/search-extractor.ts +0 -71
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project membership reconciliation — ports pkg/domain/project/reconcile/
|
|
3
|
-
* (service.go, execution_engine.go, reconciliation_{result,options,error}.go).
|
|
4
|
-
* Proven by __tests__/reconcile.test.ts and the apply→prune composed arms in
|
|
5
|
-
* __tests__/project.composed.test.ts.
|
|
6
|
-
*
|
|
7
|
-
* Apply compares the previous project's spec.members with the newly persisted
|
|
8
|
-
* spec.members, both keyed "{kind}:{slug}" (the kind's enum NAME — Go's %s).
|
|
9
|
-
* Members only in the current list are ADDED; members only in the previous
|
|
10
|
-
* list are ORPHANS and, with pruning enabled, are deleted through the four
|
|
11
|
-
* downstream domains' in-process clients — their FULL delete pipelines, so a
|
|
12
|
-
* referentially-blocked orphan fails exactly as an external delete would.
|
|
13
|
-
*
|
|
14
|
-
* Error posture (Go, verified): reconcile NEVER throws. Per-orphan failures
|
|
15
|
-
* are collected on the result and are log-only — ToProtoSummary carries only
|
|
16
|
-
* created/deleted, so a failed orphan is simply absent from `deleted`. The
|
|
17
|
-
* wire surface of this whole module is those two lists.
|
|
18
|
-
*
|
|
19
|
-
* Deliberately not ported (plan-gate decisions, tasks/T01_0_plan.md):
|
|
20
|
-
* - Go's nil-deleter stub arm (service.go:95-98) and the
|
|
21
|
-
* SetReconciliationService late-bind — both exist only for Go's
|
|
22
|
-
* registration-before-clients boot window; the compose root's lazy
|
|
23
|
-
* provider closes that window, so here a deleter always exists.
|
|
24
|
-
* - ResultBuilder and the DryRun/NoPrune option factories — zero non-test
|
|
25
|
-
* uses in Go; the sole production call is
|
|
26
|
-
* reconcile(previous, current, DEFAULT_RECONCILIATION_OPTIONS).
|
|
27
|
-
*/
|
|
28
|
-
import { create } from "@bufbuild/protobuf";
|
|
29
|
-
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
30
|
-
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
31
|
-
import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
32
|
-
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
33
|
-
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
34
|
-
import { ReconciliationSummarySchema } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/status_pb";
|
|
35
|
-
import { findResourceBySlug } from "../../pipeline/steps/helpers.js";
|
|
36
|
-
import { metadataOf } from "../../pipeline/steps/shapes.js";
|
|
37
|
-
export const DEFAULT_RECONCILIATION_OPTIONS = {
|
|
38
|
-
pruneEnabled: true,
|
|
39
|
-
dryRun: false,
|
|
40
|
-
};
|
|
41
|
-
export function newReconciliationService(deps) {
|
|
42
|
-
return {
|
|
43
|
-
async reconcile(previousMembers, currentMembers, options) {
|
|
44
|
-
const previousSet = buildReferenceSet(previousMembers);
|
|
45
|
-
const currentSet = buildReferenceSet(currentMembers);
|
|
46
|
-
const added = currentMembers.filter((ref) => !previousSet.has(referenceKey(ref)));
|
|
47
|
-
const orphans = previousMembers.filter((ref) => !currentSet.has(referenceKey(ref)));
|
|
48
|
-
if (added.length === 0 && orphans.length === 0) {
|
|
49
|
-
return { added: [], removed: [], errors: [] };
|
|
50
|
-
}
|
|
51
|
-
// Dry run reports the orphans in `removed` WITHOUT deleting (Go
|
|
52
|
-
// service.go:61-63 passes them straight through).
|
|
53
|
-
if (options.dryRun) {
|
|
54
|
-
return { added, removed: orphans, errors: [] };
|
|
55
|
-
}
|
|
56
|
-
if (!options.pruneEnabled || orphans.length === 0) {
|
|
57
|
-
return { added, removed: [], errors: [] };
|
|
58
|
-
}
|
|
59
|
-
const { removed, errors } = await deleteOrphans(deps, orphans);
|
|
60
|
-
return { added, removed, errors };
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Deletes each orphan, continuing on failure (Go deleteOrphans). Failures
|
|
66
|
-
* are warned here — Go collects them onto a result nothing in production
|
|
67
|
-
* reads, which leaves operators blind; the log is edition-local and the
|
|
68
|
-
* wire behavior is identical.
|
|
69
|
-
*/
|
|
70
|
-
async function deleteOrphans(deps, orphans) {
|
|
71
|
-
const removed = [];
|
|
72
|
-
const errors = [];
|
|
73
|
-
for (const ref of orphans) {
|
|
74
|
-
const refKey = referenceKey(ref);
|
|
75
|
-
let resourceId;
|
|
76
|
-
try {
|
|
77
|
-
resourceId = await resolveResourceId(deps.store, ref);
|
|
78
|
-
}
|
|
79
|
-
catch (cause) {
|
|
80
|
-
errors.push({
|
|
81
|
-
resourceKey: refKey,
|
|
82
|
-
message: "failed to resolve resource for deletion",
|
|
83
|
-
cause,
|
|
84
|
-
});
|
|
85
|
-
deps.logger.warn("reconciliation: failed to resolve orphan for deletion", {
|
|
86
|
-
resource: refKey,
|
|
87
|
-
error: cause instanceof Error ? cause.message : String(cause),
|
|
88
|
-
});
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
await deps.orphanDeleter().delete(ref.kind, resourceId);
|
|
93
|
-
}
|
|
94
|
-
catch (cause) {
|
|
95
|
-
errors.push({
|
|
96
|
-
resourceKey: refKey,
|
|
97
|
-
message: "failed to delete orphaned resource",
|
|
98
|
-
cause,
|
|
99
|
-
});
|
|
100
|
-
deps.logger.warn("reconciliation: failed to delete orphaned resource", {
|
|
101
|
-
resource: refKey,
|
|
102
|
-
resourceId,
|
|
103
|
-
error: cause instanceof Error ? cause.message : String(cause),
|
|
104
|
-
});
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
removed.push(ref);
|
|
108
|
-
}
|
|
109
|
-
return { removed, errors };
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Resolves an orphan reference to its resource id, within the org the
|
|
113
|
-
* reference names. Slugs are org-scoped identifiers: a local backend holds
|
|
114
|
-
* the `stigmer` system org beside the user's, a self-host holds many, and
|
|
115
|
-
* the same slug may exist in several. The Go port matched by slug alone and
|
|
116
|
-
* could prune another org's resource (stigmer/stigmer#1151); the CLI has
|
|
117
|
-
* always stamped `org` on every member reference, so scoping is a change
|
|
118
|
-
* of lookup, not of contract. A reference without an org (a project written
|
|
119
|
-
* before members carried one) keeps the unscoped match, the helper's
|
|
120
|
-
* documented empty-org arm.
|
|
121
|
-
*/
|
|
122
|
-
async function resolveResourceId(store, ref) {
|
|
123
|
-
const schema = schemaForKind(ref.kind);
|
|
124
|
-
const resource = await findResourceBySlug(store, ref.kind, schema, ref.slug, ref.org);
|
|
125
|
-
if (resource === undefined) {
|
|
126
|
-
const where = ref.org === "" ? "" : ` in org '${ref.org}'`;
|
|
127
|
-
throw new Error(`resource ${kindName(ref.kind)}/${ref.slug} not found${where}`);
|
|
128
|
-
}
|
|
129
|
-
const id = metadataOf(resource)?.id ?? "";
|
|
130
|
-
if (id === "") {
|
|
131
|
-
throw new Error(`resource ${kindName(ref.kind)}/${ref.slug} has no ID`);
|
|
132
|
-
}
|
|
133
|
-
return id;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* The four member kinds the reconciler supports (Go newProtoForKind);
|
|
137
|
-
* anything else is an unsupported-kind resolution error.
|
|
138
|
-
*/
|
|
139
|
-
function schemaForKind(kind) {
|
|
140
|
-
switch (kind) {
|
|
141
|
-
case ApiResourceKind.agent:
|
|
142
|
-
return AgentSchema;
|
|
143
|
-
case ApiResourceKind.workflow:
|
|
144
|
-
return WorkflowSchema;
|
|
145
|
-
case ApiResourceKind.mcp_server:
|
|
146
|
-
return McpServerSchema;
|
|
147
|
-
case ApiResourceKind.skill:
|
|
148
|
-
return SkillSchema;
|
|
149
|
-
default:
|
|
150
|
-
throw new Error(`unsupported resource kind: ${kindName(kind)}`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* "{kind}:{slug}" with the kind's enum NAME — Go fmt %s of the enum (e.g.
|
|
155
|
-
* "agent:my-agent", "mcp_server:tools"). The numeric form would diverge
|
|
156
|
-
* from Go's log/error copy.
|
|
157
|
-
*/
|
|
158
|
-
export function referenceKey(ref) {
|
|
159
|
-
return `${kindName(ref.kind)}:${ref.slug}`;
|
|
160
|
-
}
|
|
161
|
-
function kindName(kind) {
|
|
162
|
-
return ApiResourceKind[kind] ?? String(kind);
|
|
163
|
-
}
|
|
164
|
-
function buildReferenceSet(refs) {
|
|
165
|
-
return new Set(refs.map(referenceKey));
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Go ToProtoSummary: added → created, updated ALWAYS empty (not applicable
|
|
169
|
-
* in the reference model), removed → deleted. Every successful apply sets
|
|
170
|
-
* the summary — even an all-empty one is a PRESENT message on the wire.
|
|
171
|
-
*/
|
|
172
|
-
export function toProtoSummary(result) {
|
|
173
|
-
return create(ReconciliationSummarySchema, {
|
|
174
|
-
created: [...result.added],
|
|
175
|
-
updated: [],
|
|
176
|
-
deleted: [...result.removed],
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
//# sourceMappingURL=reconcile.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../../src/domain/project/reconcile.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"search-extractor.d.ts","sourceRoot":"","sources":["../../../src/domain/project/search-extractor.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAE3E,eAAO,MAAM,sBAAsB,EAAE,mBA0CpC,CAAC"}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
2
|
-
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
3
|
-
import { ProjectSchema } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/api_pb";
|
|
4
|
-
import { buildSearchResult } from "../../query/search/extractor.js";
|
|
5
|
-
export const projectSearchExtractor = {
|
|
6
|
-
kind: ApiResourceKind.project,
|
|
7
|
-
schema: ProjectSchema,
|
|
8
|
-
getSearchSummary(resource) {
|
|
9
|
-
// Go ProjectExtractor.GetSearchSummary: spec.description.
|
|
10
|
-
const project = resource;
|
|
11
|
-
return project.spec?.description ?? "";
|
|
12
|
-
},
|
|
13
|
-
toSearchResult(resource, score) {
|
|
14
|
-
const project = resource;
|
|
15
|
-
return buildSearchResult({
|
|
16
|
-
kind: ApiResourceKind.project,
|
|
17
|
-
metadata: project.metadata,
|
|
18
|
-
summary: project.spec?.description ?? "",
|
|
19
|
-
score,
|
|
20
|
-
createdAt: project.status?.audit?.specAudit?.createdAt,
|
|
21
|
-
updatedAt: project.status?.audit?.specAudit?.updatedAt,
|
|
22
|
-
});
|
|
23
|
-
},
|
|
24
|
-
getSearchIndexEntry(resource) {
|
|
25
|
-
const project = resource;
|
|
26
|
-
const metadata = project.metadata;
|
|
27
|
-
if (metadata === undefined) {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
name: metadata.name,
|
|
32
|
-
description: project.spec?.description ?? "",
|
|
33
|
-
// Tags join space-separated — the index entry carries one tags
|
|
34
|
-
// string (Go extractor.JoinTags).
|
|
35
|
-
tags: metadata.tags.join(" "),
|
|
36
|
-
org: metadata.org,
|
|
37
|
-
// The enum NAME string, exactly Go's visibility.String().
|
|
38
|
-
visibility: ApiResourceVisibility[metadata.visibility] ?? "",
|
|
39
|
-
createdAt: Number(project.status?.audit?.specAudit?.createdAt?.seconds ?? 0n),
|
|
40
|
-
};
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
//# sourceMappingURL=search-extractor.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"search-extractor.js","sourceRoot":"","sources":["../../../src/domain/project/search-extractor.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,eAAe,EAAE,MAAM,qFAAqF,CAAC;AACtH,OAAO,EAAE,qBAAqB,EAAE,MAAM,wDAAwD,CAAC;AAE/F,OAAO,EAAE,aAAa,EAAE,MAAM,sDAAsD,CAAC;AAIrF,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAGpE,MAAM,CAAC,MAAM,sBAAsB,GAAwB;IACzD,IAAI,EAAE,eAAe,CAAC,OAAO;IAC7B,MAAM,EAAE,aAAa;IAErB,gBAAgB,CAAC,QAAiB;QAChC,0DAA0D;QAC1D,MAAM,OAAO,GAAG,QAA8B,CAAC;QAC/C,OAAO,OAAO,CAAC,IAAI,EAAE,WAAW,IAAI,EAAE,CAAC;IACzC,CAAC;IAED,cAAc,CAAC,QAAiB,EAAE,KAAa;QAC7C,MAAM,OAAO,GAAG,QAA8B,CAAC;QAC/C,OAAO,iBAAiB,CAAC;YACvB,IAAI,EAAE,eAAe,CAAC,OAAO;YAC7B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,WAAW,IAAI,EAAE;YACxC,KAAK;YACL,SAAS,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;YACtD,SAAS,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;SACvD,CAAC,CAAC;IACL,CAAC;IAED,mBAAmB,CAAC,QAAiB;QACnC,MAAM,OAAO,GAAG,QAA8B,CAAC;QAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QAClC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO;YACL,IAAI,EAAE,QAAQ,CAAC,IAAI;YACnB,WAAW,EAAE,OAAO,CAAC,IAAI,EAAE,WAAW,IAAI,EAAE;YAC5C,+DAA+D;YAC/D,kCAAkC;YAClC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;YAC7B,GAAG,EAAE,QAAQ,CAAC,GAAG;YACjB,0DAA0D;YAC1D,UAAU,EAAE,qBAAqB,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE;YAC5D,SAAS,EAAE,MAAM,CACf,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,IAAI,EAAE,CAC3D;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transcript of fga/model/tenancy/project.fga — an organization's
|
|
3
|
-
* grouping: admins own it, and `viewer from organization` lets every
|
|
4
|
-
* organization viewer read it without a visibility tuple (projects have
|
|
5
|
-
* no visibility axis in `kind_meta`; the whole organization is their
|
|
6
|
-
* audience by the model's own line).
|
|
7
|
-
*/
|
|
8
|
-
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
9
|
-
import { ProjectSchema } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/api_pb";
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
computed,
|
|
13
|
-
declareKind,
|
|
14
|
-
direct,
|
|
15
|
-
from,
|
|
16
|
-
objectOf,
|
|
17
|
-
union,
|
|
18
|
-
} from "./rewrite.js";
|
|
19
|
-
|
|
20
|
-
export const projectDeclaration = declareKind({
|
|
21
|
-
kind: ApiResourceKind.project,
|
|
22
|
-
schema: ProjectSchema,
|
|
23
|
-
source: "fga/model/tenancy/project.fga",
|
|
24
|
-
relations: [
|
|
25
|
-
["organization", direct(objectOf("organization"))],
|
|
26
|
-
[
|
|
27
|
-
"owner",
|
|
28
|
-
union(
|
|
29
|
-
direct(objectOf("identity_account")),
|
|
30
|
-
from("admin", "organization"),
|
|
31
|
-
),
|
|
32
|
-
],
|
|
33
|
-
[
|
|
34
|
-
"viewer",
|
|
35
|
-
union(
|
|
36
|
-
direct(objectOf("identity_account")),
|
|
37
|
-
computed("owner"),
|
|
38
|
-
from("viewer", "organization"),
|
|
39
|
-
),
|
|
40
|
-
],
|
|
41
|
-
["can_view", computed("viewer")],
|
|
42
|
-
["can_edit", computed("owner")],
|
|
43
|
-
["can_delete", computed("owner")],
|
|
44
|
-
["can_grant_access", computed("owner")],
|
|
45
|
-
["can_view_access", computed("viewer")],
|
|
46
|
-
],
|
|
47
|
-
});
|
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pins the project domain against Go's controller tests — through the REAL
|
|
3
|
-
* stack: a composed server on an ephemeral port, a native gRPC client, the
|
|
4
|
-
* full interceptor chain, and REAL orphan deletes through the in-process
|
|
5
|
-
* command clients' full pipelines.
|
|
6
|
-
*
|
|
7
|
-
* The load-bearing pins the conformance suite does NOT cover (it never
|
|
8
|
-
* touches spec.members or last_reconciliation):
|
|
9
|
-
* - every successful apply response carries status.last_reconciliation,
|
|
10
|
-
* even as a present-but-EMPTY message — and it is NEVER persisted;
|
|
11
|
-
* - apply→prune end to end: a dropped member is actually deleted through
|
|
12
|
-
* its owning domain's pipeline; an unresolvable member is silently
|
|
13
|
-
* absent from `deleted` while apply still succeeds;
|
|
14
|
-
* - members referencing nonexistent resources persist silently (no
|
|
15
|
-
* ValidateReferences in any project chain);
|
|
16
|
-
* - the create/update pipeline ASYMMETRY: ValidateVisibility and
|
|
17
|
-
* NormalizeReferences run on create only;
|
|
18
|
-
* - project delete never cascades to members.
|
|
19
|
-
*/
|
|
20
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
21
|
-
import { tmpdir } from "node:os";
|
|
22
|
-
import path from "node:path";
|
|
23
|
-
|
|
24
|
-
import { create } from "@bufbuild/protobuf";
|
|
25
|
-
import { Code, ConnectError, createClient } from "@connectrpc/connect";
|
|
26
|
-
import type { Client, Transport } from "@connectrpc/connect";
|
|
27
|
-
import { createGrpcTransport } from "@connectrpc/connect-node";
|
|
28
|
-
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
29
|
-
|
|
30
|
-
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
31
|
-
import { AgentQueryController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/query_pb";
|
|
32
|
-
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
33
|
-
import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
|
|
34
|
-
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
35
|
-
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
36
|
-
import { ProjectCommandController } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/command_pb";
|
|
37
|
-
import { ProjectQueryController } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/query_pb";
|
|
38
|
-
|
|
39
|
-
import { loadConfig } from "../../../boot/config.js";
|
|
40
|
-
import { composeServer } from "../../../boot/compose.js";
|
|
41
|
-
import type { ComposedServer } from "../../../boot/compose.js";
|
|
42
|
-
import { createLogger } from "../../../boot/logger.js";
|
|
43
|
-
|
|
44
|
-
const silentLogger = createLogger({ level: "error", pretty: false, write: () => {} });
|
|
45
|
-
|
|
46
|
-
let dir: string;
|
|
47
|
-
let server: ComposedServer;
|
|
48
|
-
let transport: Transport;
|
|
49
|
-
let command: Client<typeof ProjectCommandController>;
|
|
50
|
-
let query: Client<typeof ProjectQueryController>;
|
|
51
|
-
let agentQuery: Client<typeof AgentQueryController>;
|
|
52
|
-
let mcpServerQuery: Client<typeof McpServerQueryController>;
|
|
53
|
-
|
|
54
|
-
beforeAll(async () => {
|
|
55
|
-
dir = mkdtempSync(path.join(tmpdir(), "project-domain-test-"));
|
|
56
|
-
server = await composeServer({
|
|
57
|
-
config: loadConfig({
|
|
58
|
-
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
59
|
-
// No engine behind composed tests: 127.0.0.1:1 is deterministically
|
|
60
|
-
// closed, so boots fail the non-fatal connect fast and can never touch
|
|
61
|
-
// a live local Temporal (the conformance CRUD harness does the same).
|
|
62
|
-
TEMPORAL_HOST_PORT: "127.0.0.1:1",
|
|
63
|
-
DB_PATH: path.join(dir, "stigmer.db"),
|
|
64
|
-
// Everything filesystem-backed stays inside the test dir — the
|
|
65
|
-
// defaults resolve into ~/.stigmer, which tests must never touch.
|
|
66
|
-
STORAGE_PATH: path.join(dir, "storage"),
|
|
67
|
-
ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
|
|
68
|
-
}),
|
|
69
|
-
logger: silentLogger,
|
|
70
|
-
portOverride: 0,
|
|
71
|
-
host: "127.0.0.1",
|
|
72
|
-
});
|
|
73
|
-
const port = await server.start();
|
|
74
|
-
transport = createGrpcTransport({ baseUrl: `http://127.0.0.1:${port}` });
|
|
75
|
-
command = createClient(ProjectCommandController, transport);
|
|
76
|
-
query = createClient(ProjectQueryController, transport);
|
|
77
|
-
agentQuery = createClient(AgentQueryController, transport);
|
|
78
|
-
mcpServerQuery = createClient(McpServerQueryController, transport);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
afterAll(async () => {
|
|
82
|
-
await server.shutdown();
|
|
83
|
-
rmSync(dir, { recursive: true, force: true });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const API_VERSION = "tenancy.stigmer.ai/v1";
|
|
87
|
-
const KIND = "Project";
|
|
88
|
-
const ORG = "local";
|
|
89
|
-
|
|
90
|
-
let seq = 0;
|
|
91
|
-
function uniqueName(prefix: string): string {
|
|
92
|
-
seq += 1;
|
|
93
|
-
return `${prefix} ${seq}`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface MemberInit {
|
|
97
|
-
kind: ApiResourceKind;
|
|
98
|
-
slug: string;
|
|
99
|
-
org?: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function projectInput(
|
|
103
|
-
name: string,
|
|
104
|
-
extras?: { members?: MemberInit[]; visibility?: ApiResourceVisibility },
|
|
105
|
-
) {
|
|
106
|
-
return {
|
|
107
|
-
apiVersion: API_VERSION,
|
|
108
|
-
kind: KIND,
|
|
109
|
-
metadata: {
|
|
110
|
-
name,
|
|
111
|
-
org: ORG,
|
|
112
|
-
...(extras?.visibility !== undefined ? { visibility: extras.visibility } : {}),
|
|
113
|
-
},
|
|
114
|
-
spec: {
|
|
115
|
-
description: "created by the domain test",
|
|
116
|
-
members: (extras?.members ?? []).map((m) => ({
|
|
117
|
-
kind: m.kind,
|
|
118
|
-
slug: m.slug,
|
|
119
|
-
org: m.org ?? ORG,
|
|
120
|
-
})),
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function seedAgent(id: string, slug: string): Promise<void> {
|
|
126
|
-
await server.store.saveResource(
|
|
127
|
-
ApiResourceKind.agent,
|
|
128
|
-
id,
|
|
129
|
-
AgentSchema,
|
|
130
|
-
create(AgentSchema, {
|
|
131
|
-
apiVersion: "agentic.stigmer.ai/v1",
|
|
132
|
-
kind: "Agent",
|
|
133
|
-
metadata: { id, name: slug, slug, org: ORG },
|
|
134
|
-
}),
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function seedMcpServer(id: string, slug: string): Promise<void> {
|
|
139
|
-
await server.store.saveResource(
|
|
140
|
-
ApiResourceKind.mcp_server,
|
|
141
|
-
id,
|
|
142
|
-
McpServerSchema,
|
|
143
|
-
create(McpServerSchema, {
|
|
144
|
-
apiVersion: "agentic.stigmer.ai/v1",
|
|
145
|
-
kind: "McpServer",
|
|
146
|
-
metadata: { id, name: slug, slug, org: ORG },
|
|
147
|
-
}),
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function grpcCode(run: () => Promise<unknown>): Promise<ConnectError> {
|
|
152
|
-
try {
|
|
153
|
-
await run();
|
|
154
|
-
throw new Error("expected the call to fail");
|
|
155
|
-
} catch (error) {
|
|
156
|
-
if (error instanceof ConnectError) {
|
|
157
|
-
return error;
|
|
158
|
-
}
|
|
159
|
-
throw error;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
describe("apply reconciliation summary (Go apply_test.go)", () => {
|
|
164
|
-
it("first apply reports all members as created and the prj_ id shape", async () => {
|
|
165
|
-
const applied = await command.apply(
|
|
166
|
-
projectInput(uniqueName("Members Project"), {
|
|
167
|
-
members: [
|
|
168
|
-
{ kind: ApiResourceKind.agent, slug: "member-a" },
|
|
169
|
-
{ kind: ApiResourceKind.workflow, slug: "member-b" },
|
|
170
|
-
],
|
|
171
|
-
}),
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
expect(applied.metadata?.id).toMatch(/^prj_[0-9a-z]{26}$/);
|
|
175
|
-
expect(applied.status?.audit?.specAudit?.event).toBe("created");
|
|
176
|
-
expect(applied.status?.lastReconciliation).toBeDefined();
|
|
177
|
-
expect(
|
|
178
|
-
applied.status?.lastReconciliation?.created.map((r) => r.slug),
|
|
179
|
-
).toEqual(["member-a", "member-b"]);
|
|
180
|
-
expect(applied.status?.lastReconciliation?.updated).toEqual([]);
|
|
181
|
-
expect(applied.status?.lastReconciliation?.deleted).toEqual([]);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("a no-change apply still carries a PRESENT (empty) summary — and it is never persisted", async () => {
|
|
185
|
-
const name = uniqueName("Summary Presence");
|
|
186
|
-
const first = await command.apply(projectInput(name));
|
|
187
|
-
// No members at all: the summary message must still be present.
|
|
188
|
-
expect(first.status?.lastReconciliation).toBeDefined();
|
|
189
|
-
expect(first.status?.lastReconciliation?.created).toEqual([]);
|
|
190
|
-
expect(first.status?.lastReconciliation?.deleted).toEqual([]);
|
|
191
|
-
|
|
192
|
-
// Response-only: the persisted resource has NO last_reconciliation.
|
|
193
|
-
const fetched = await query.get({ value: first.metadata!.id });
|
|
194
|
-
expect(fetched.status?.lastReconciliation).toBeUndefined();
|
|
195
|
-
|
|
196
|
-
// And the second apply's summary is computed fresh from the delta.
|
|
197
|
-
const second = await command.apply(projectInput(name));
|
|
198
|
-
expect(second.metadata?.id).toBe(first.metadata?.id);
|
|
199
|
-
expect(second.status?.audit?.specAudit?.event).toBe("updated");
|
|
200
|
-
expect(second.status?.lastReconciliation).toBeDefined();
|
|
201
|
-
expect(second.status?.lastReconciliation?.created).toEqual([]);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("prunes a dropped member FOR REAL through its domain's delete pipeline", async () => {
|
|
205
|
-
await seedAgent("agt_prune_keep", "prune-keep-agent");
|
|
206
|
-
await seedMcpServer("mcps_prune_drop", "prune-drop-mcps");
|
|
207
|
-
const name = uniqueName("Orphan Prune");
|
|
208
|
-
|
|
209
|
-
await command.apply(
|
|
210
|
-
projectInput(name, {
|
|
211
|
-
members: [
|
|
212
|
-
{ kind: ApiResourceKind.agent, slug: "prune-keep-agent" },
|
|
213
|
-
{ kind: ApiResourceKind.mcp_server, slug: "prune-drop-mcps" },
|
|
214
|
-
],
|
|
215
|
-
}),
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
// The second apply both DROPS the mcpserver and INTRODUCES a new
|
|
219
|
-
// member, so one wire summary carries created and deleted together.
|
|
220
|
-
const second = await command.apply(
|
|
221
|
-
projectInput(name, {
|
|
222
|
-
members: [
|
|
223
|
-
{ kind: ApiResourceKind.agent, slug: "prune-keep-agent" },
|
|
224
|
-
{ kind: ApiResourceKind.workflow, slug: "prune-new-member" },
|
|
225
|
-
],
|
|
226
|
-
}),
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
expect(
|
|
230
|
-
second.status?.lastReconciliation?.deleted.map((r) => r.slug),
|
|
231
|
-
).toEqual(["prune-drop-mcps"]);
|
|
232
|
-
expect(
|
|
233
|
-
second.status?.lastReconciliation?.created.map((r) => r.slug),
|
|
234
|
-
).toEqual(["prune-new-member"]);
|
|
235
|
-
|
|
236
|
-
// The orphan is GONE (deleted through mcpserver's own pipeline)…
|
|
237
|
-
const gone = await grpcCode(() =>
|
|
238
|
-
mcpServerQuery.get({ value: "mcps_prune_drop" }),
|
|
239
|
-
);
|
|
240
|
-
expect(gone.code).toBe(Code.NotFound);
|
|
241
|
-
// …and the retained member is untouched.
|
|
242
|
-
const kept = await agentQuery.get({ value: "agt_prune_keep" });
|
|
243
|
-
expect(kept.metadata?.slug).toBe("prune-keep-agent");
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("an id-carrying apply captures previous members and prunes on the update path", async () => {
|
|
247
|
-
await seedAgent("agt_idpath", "idpath-agent");
|
|
248
|
-
const name = uniqueName("Id Carrying Apply");
|
|
249
|
-
|
|
250
|
-
const first = await command.apply(
|
|
251
|
-
projectInput(name, {
|
|
252
|
-
members: [{ kind: ApiResourceKind.agent, slug: "idpath-agent" }],
|
|
253
|
-
}),
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
// Second apply addresses the project by its ID (the CLI's steady-state
|
|
257
|
-
// shape) with the member dropped — previousMembers must still be
|
|
258
|
-
// captured from the loaded existing resource.
|
|
259
|
-
const second = await command.apply({
|
|
260
|
-
...projectInput(name),
|
|
261
|
-
metadata: { id: first.metadata!.id, name, org: ORG },
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
expect(second.metadata?.id).toBe(first.metadata?.id);
|
|
265
|
-
expect(
|
|
266
|
-
second.status?.lastReconciliation?.deleted.map((r) => r.slug),
|
|
267
|
-
).toEqual(["idpath-agent"]);
|
|
268
|
-
const gone = await grpcCode(() => agentQuery.get({ value: "agt_idpath" }));
|
|
269
|
-
expect(gone.code).toBe(Code.NotFound);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("a member dropped via plain UPDATE is stranded — the next apply prunes nothing", async () => {
|
|
273
|
-
// Update runs NO reconciliation, and apply diffs against the STORED
|
|
274
|
-
// members: dropping a member through update shrinks the stored list
|
|
275
|
-
// first, so the later apply computes no orphan and the resource
|
|
276
|
-
// survives. User-visible Go behavior (update strands, apply prunes).
|
|
277
|
-
await seedAgent("agt_stranded", "stranded-agent");
|
|
278
|
-
const name = uniqueName("Update Strands");
|
|
279
|
-
|
|
280
|
-
await command.apply(
|
|
281
|
-
projectInput(name, {
|
|
282
|
-
members: [{ kind: ApiResourceKind.agent, slug: "stranded-agent" }],
|
|
283
|
-
}),
|
|
284
|
-
);
|
|
285
|
-
await command.update(projectInput(name));
|
|
286
|
-
|
|
287
|
-
const applied = await command.apply(projectInput(name));
|
|
288
|
-
expect(applied.status?.lastReconciliation?.deleted).toEqual([]);
|
|
289
|
-
const survivor = await agentQuery.get({ value: "agt_stranded" });
|
|
290
|
-
expect(survivor.metadata?.slug).toBe("stranded-agent");
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it("an unresolvable orphan is silently absent from deleted; apply still succeeds", async () => {
|
|
294
|
-
const name = uniqueName("Ghost Member");
|
|
295
|
-
// No ValidateReferences in any project chain: a member that points at
|
|
296
|
-
// nothing persists silently (pinned Go behavior).
|
|
297
|
-
const first = await command.apply(
|
|
298
|
-
projectInput(name, {
|
|
299
|
-
members: [{ kind: ApiResourceKind.agent, slug: "ghost-agent" }],
|
|
300
|
-
}),
|
|
301
|
-
);
|
|
302
|
-
expect(first.spec?.members.map((m) => m.slug)).toEqual(["ghost-agent"]);
|
|
303
|
-
|
|
304
|
-
const second = await command.apply(projectInput(name));
|
|
305
|
-
// Resolution failed (nothing to delete) → collected log-only, absent
|
|
306
|
-
// from the wire's deleted list, and apply SUCCEEDS.
|
|
307
|
-
expect(second.status?.lastReconciliation).toBeDefined();
|
|
308
|
-
expect(second.status?.lastReconciliation?.deleted).toEqual([]);
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
describe("create/update pipeline asymmetry (create.go vs update.go)", () => {
|
|
313
|
-
it("create rejects an unsupported visibility level; update IGNORES the carried level", async () => {
|
|
314
|
-
// Project's kind_meta has no VisibilityConfig → private-only.
|
|
315
|
-
const rejected = await grpcCode(() =>
|
|
316
|
-
command.create(
|
|
317
|
-
projectInput(uniqueName("Visibility Reject"), {
|
|
318
|
-
visibility: ApiResourceVisibility.visibility_org,
|
|
319
|
-
}),
|
|
320
|
-
),
|
|
321
|
-
);
|
|
322
|
-
expect(rejected.code).toBe(Code.InvalidArgument);
|
|
323
|
-
|
|
324
|
-
// Update has NO ValidateVisibility step; the carried level is ignored
|
|
325
|
-
// (visibility is update-immutable, oss#573) — never rejected.
|
|
326
|
-
const name = uniqueName("Visibility Ignore");
|
|
327
|
-
const created = await command.create(projectInput(name));
|
|
328
|
-
const updated = await command.update(
|
|
329
|
-
projectInput(name, { visibility: ApiResourceVisibility.visibility_org }),
|
|
330
|
-
);
|
|
331
|
-
expect(updated.metadata?.id).toBe(created.metadata?.id);
|
|
332
|
-
expect(updated.metadata?.visibility).toBe(
|
|
333
|
-
ApiResourceVisibility.visibility_private,
|
|
334
|
-
);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it("create normalizes empty member orgs; update persists them EMPTY", async () => {
|
|
338
|
-
const name = uniqueName("Normalize Asymmetry");
|
|
339
|
-
const created = await command.create(
|
|
340
|
-
projectInput(name, {
|
|
341
|
-
members: [{ kind: ApiResourceKind.agent, slug: "norm-a", org: "" }],
|
|
342
|
-
}),
|
|
343
|
-
);
|
|
344
|
-
// NormalizeReferences filled the empty org from metadata.org.
|
|
345
|
-
expect(created.spec?.members[0]?.org).toBe(ORG);
|
|
346
|
-
|
|
347
|
-
const updated = await command.update(
|
|
348
|
-
projectInput(name, {
|
|
349
|
-
members: [{ kind: ApiResourceKind.agent, slug: "norm-b", org: "" }],
|
|
350
|
-
}),
|
|
351
|
-
);
|
|
352
|
-
// Update runs NO NormalizeReferences: the empty org persists as-is.
|
|
353
|
-
expect(updated.spec?.members[0]?.slug).toBe("norm-b");
|
|
354
|
-
expect(updated.spec?.members[0]?.org).toBe("");
|
|
355
|
-
});
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
describe("delete has no member cascade (delete.go)", () => {
|
|
359
|
-
it("returns the pre-delete resource and leaves members untouched", async () => {
|
|
360
|
-
await seedAgent("agt_cascade_safe", "cascade-safe-agent");
|
|
361
|
-
const created = await command.create(
|
|
362
|
-
projectInput(uniqueName("No Cascade"), {
|
|
363
|
-
members: [{ kind: ApiResourceKind.agent, slug: "cascade-safe-agent" }],
|
|
364
|
-
}),
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
const deleted = await command.delete({ value: created.metadata!.id });
|
|
368
|
-
expect(deleted.metadata?.id).toBe(created.metadata?.id);
|
|
369
|
-
expect(deleted.spec?.members.map((m) => m.slug)).toEqual([
|
|
370
|
-
"cascade-safe-agent",
|
|
371
|
-
]);
|
|
372
|
-
|
|
373
|
-
const notFound = await grpcCode(() =>
|
|
374
|
-
query.get({ value: created.metadata!.id }),
|
|
375
|
-
);
|
|
376
|
-
expect(notFound.code).toBe(Code.NotFound);
|
|
377
|
-
// The member survives: delete removes ONLY the project entity.
|
|
378
|
-
const survivor = await agentQuery.get({ value: "agt_cascade_safe" });
|
|
379
|
-
expect(survivor.metadata?.slug).toBe("cascade-safe-agent");
|
|
380
|
-
});
|
|
381
|
-
});
|