@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,387 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project controller — ports pkg/domain/project/controller (command + query
|
|
3
|
-
* sides). Project is the aggregate root grouping related resources: its spec
|
|
4
|
-
* carries lightweight ApiResourceReference members the CLI derives from
|
|
5
|
-
* individual apply responses (never user-authored); apply reconciles the
|
|
6
|
-
* membership and prunes orphans through the four downstream domains'
|
|
7
|
-
* in-process clients — the widest client fan-out of any CRUD domain, which
|
|
8
|
-
* is why it ported last of Class A (D4 #16).
|
|
9
|
-
*
|
|
10
|
-
* Pipeline per RPC mirrors the Go step chains character-for-character; the
|
|
11
|
-
* create/update ASYMMETRY is contract: create runs ValidateVisibility and
|
|
12
|
-
* NormalizeReferences, update runs neither (an updated members list persists
|
|
13
|
-
* without reference normalization — Go update.go). No chain runs
|
|
14
|
-
* ValidateReferences: members referencing nonexistent resources persist
|
|
15
|
-
* silently. Delete removes ONLY the project — members are never cascaded.
|
|
16
|
-
*
|
|
17
|
-
* Proven by project.conformance.test.ts (CONFORMANCE_TARGET=local) and
|
|
18
|
-
* __tests__/{reconcile,project.composed}.test.ts (the conformance suite
|
|
19
|
-
* never touches spec.members or last_reconciliation; the composed tests own
|
|
20
|
-
* those pins).
|
|
21
|
-
*
|
|
22
|
-
* Versus Stigmer Cloud, OSS excludes the Authorize, CreateIamPolicies,
|
|
23
|
-
* Publish, and TransformResponse steps (no multi-tenant auth, IAM/FGA, or
|
|
24
|
-
* event publishing here).
|
|
25
|
-
*/
|
|
26
|
-
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
27
|
-
import { create } from "@bufbuild/protobuf";
|
|
28
|
-
|
|
29
|
-
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
30
|
-
import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
31
|
-
import { ProjectSchema } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/api_pb";
|
|
32
|
-
import type { Project } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/api_pb";
|
|
33
|
-
import { ProjectCommandController } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/command_pb";
|
|
34
|
-
import type { ProjectId } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/io_pb";
|
|
35
|
-
import { ProjectQueryController } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/query_pb";
|
|
36
|
-
import { ProjectStatusSchema } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/status_pb";
|
|
37
|
-
|
|
38
|
-
import type { Logger } from "../../boot/logger.js";
|
|
39
|
-
import type { Authorizer } from "../../extensions/authorizer.js";
|
|
40
|
-
import type { ResourceAuthorizationLifecycle } from "../../extensions/resource-authorization.js";
|
|
41
|
-
import { internalError } from "../../pipeline/errors.js";
|
|
42
|
-
import { apiResourceKindKey } from "../../pipeline/interceptors/apiresource.js";
|
|
43
|
-
import { newPipeline } from "../../pipeline/pipeline.js";
|
|
44
|
-
import { callerIdentityOf } from "../../pipeline/interceptors/auth.js";
|
|
45
|
-
import { RequestContext } from "../../pipeline/request-context.js";
|
|
46
|
-
import { newAuthorizeStep } from "../../pipeline/steps/authorize.js";
|
|
47
|
-
import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-labels.js";
|
|
48
|
-
import { newBuildUpdateStateStep } from "../../pipeline/steps/build-update-state.js";
|
|
49
|
-
import { newBuildNewStateStep } from "../../pipeline/steps/defaults.js";
|
|
50
|
-
import {
|
|
51
|
-
newDeleteResourceStep,
|
|
52
|
-
newExtractResourceIdStep,
|
|
53
|
-
newLoadExistingForDeleteStep,
|
|
54
|
-
} from "../../pipeline/steps/delete.js";
|
|
55
|
-
import { newCheckDuplicateStep } from "../../pipeline/steps/duplicate.js";
|
|
56
|
-
import {
|
|
57
|
-
newDeleteSearchIndexStep,
|
|
58
|
-
newIndexSearchStep,
|
|
59
|
-
} from "../../pipeline/steps/index-search.js";
|
|
60
|
-
import { newLoadByReferenceStep } from "../../pipeline/steps/load-by-reference.js";
|
|
61
|
-
import {
|
|
62
|
-
EXISTING_RESOURCE_KEY,
|
|
63
|
-
newLoadExistingStep,
|
|
64
|
-
} from "../../pipeline/steps/load-existing.js";
|
|
65
|
-
import {
|
|
66
|
-
SHOULD_CREATE_KEY,
|
|
67
|
-
newLoadForApplyStep,
|
|
68
|
-
withResolvedApplyId,
|
|
69
|
-
} from "../../pipeline/steps/load-for-apply.js";
|
|
70
|
-
import {
|
|
71
|
-
TARGET_RESOURCE_KEY,
|
|
72
|
-
newLoadTargetStep,
|
|
73
|
-
} from "../../pipeline/steps/load-target.js";
|
|
74
|
-
import {
|
|
75
|
-
newCleanupIamPoliciesStep,
|
|
76
|
-
newCreateAuthorizationTuplesStep,
|
|
77
|
-
} from "../../pipeline/steps/authorization-tuples.js";
|
|
78
|
-
import { newPersistStep } from "../../pipeline/steps/persist.js";
|
|
79
|
-
import { newNormalizeReferencesStep } from "../../pipeline/steps/references.js";
|
|
80
|
-
import { newResolveSlugStep } from "../../pipeline/steps/slug.js";
|
|
81
|
-
import { newValidateVisibilityStep } from "../../pipeline/steps/validate-visibility.js";
|
|
82
|
-
import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
|
|
83
|
-
import type { Store } from "../../store/interface.js";
|
|
84
|
-
import {
|
|
85
|
-
DEFAULT_RECONCILIATION_OPTIONS,
|
|
86
|
-
newReconciliationService,
|
|
87
|
-
toProtoSummary,
|
|
88
|
-
} from "./reconcile.js";
|
|
89
|
-
import type { OrphanDeleter, ReconciliationService } from "./reconcile.js";
|
|
90
|
-
import { projectSearchExtractor } from "./search-extractor.js";
|
|
91
|
-
|
|
92
|
-
export interface ProjectControllerDeps {
|
|
93
|
-
readonly store: Store;
|
|
94
|
-
readonly logger: Logger;
|
|
95
|
-
/** The composed authorization seam — the Authorize step at position 1 of every chain calls it (O2, DD-007 §3). */
|
|
96
|
-
readonly authorizer: Authorizer;
|
|
97
|
-
/** The composed tuple-lifecycle driver — undefined = the shared steps no-op (C2). */
|
|
98
|
-
readonly authorizationLifecycle: ResourceAuthorizationLifecycle | undefined;
|
|
99
|
-
/**
|
|
100
|
-
* The four downstream delete edges (agent/workflow/mcp_server/skill),
|
|
101
|
-
* lazy per the compose root's boot-ordering idiom — the TS replacement
|
|
102
|
-
* for Go's SetReconciliationService late-bind (server.go:635-648).
|
|
103
|
-
*/
|
|
104
|
-
readonly orphanDeleter: () => OrphanDeleter;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Registers both project services on the router (routes stage). */
|
|
108
|
-
export function registerProjectServices(
|
|
109
|
-
router: ConnectRouter,
|
|
110
|
-
deps: ProjectControllerDeps,
|
|
111
|
-
): void {
|
|
112
|
-
const reconciliation = newReconciliationService({
|
|
113
|
-
store: deps.store,
|
|
114
|
-
orphanDeleter: deps.orphanDeleter,
|
|
115
|
-
logger: deps.logger,
|
|
116
|
-
});
|
|
117
|
-
router.service(ProjectCommandController, {
|
|
118
|
-
apply: (project, ctx) => apply(deps, reconciliation, project, ctx),
|
|
119
|
-
create: (project, ctx) => createProject(deps, project, ctx),
|
|
120
|
-
update: (project, ctx) => update(deps, project, ctx),
|
|
121
|
-
delete: (projectId, ctx) => deleteProject(deps, projectId, ctx),
|
|
122
|
-
});
|
|
123
|
-
router.service(ProjectQueryController, {
|
|
124
|
-
get: (projectId, ctx) => get(deps, projectId, ctx),
|
|
125
|
-
getByReference: (ref, ctx) => getByReference(deps, ref, ctx),
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function kindOf(ctx: HandlerContext): ApiResourceKind {
|
|
130
|
-
return ctx.values.get(apiResourceKindKey);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Create — chain per Go buildCreatePipeline (create.go:54-63). */
|
|
134
|
-
async function createProject(
|
|
135
|
-
deps: ProjectControllerDeps,
|
|
136
|
-
project: Project,
|
|
137
|
-
ctx: HandlerContext,
|
|
138
|
-
): Promise<Project> {
|
|
139
|
-
const reqCtx = new RequestContext(
|
|
140
|
-
ProjectSchema,
|
|
141
|
-
project,
|
|
142
|
-
callerIdentityOf(ctx),
|
|
143
|
-
kindOf(ctx),
|
|
144
|
-
);
|
|
145
|
-
await newPipeline<typeof ProjectSchema>("project-create", deps.logger)
|
|
146
|
-
.addStep(
|
|
147
|
-
newAuthorizeStep(ProjectCommandController.method.create, deps.authorizer),
|
|
148
|
-
)
|
|
149
|
-
.addStep(newValidateProtoStep())
|
|
150
|
-
.addStep(newValidateVisibilityStep())
|
|
151
|
-
.addStep(newResolveSlugStep())
|
|
152
|
-
.addStep(newCheckDuplicateStep(deps.store))
|
|
153
|
-
.addStep(newBuildNewStateStep())
|
|
154
|
-
.addStep(newGuardReservedLabelsStep(deps.authorizer))
|
|
155
|
-
.addStep(newNormalizeReferencesStep())
|
|
156
|
-
.addStep(newPersistStep(deps.store))
|
|
157
|
-
.addStep(
|
|
158
|
-
newCreateAuthorizationTuplesStep(
|
|
159
|
-
deps.authorizationLifecycle,
|
|
160
|
-
deps.logger,
|
|
161
|
-
),
|
|
162
|
-
)
|
|
163
|
-
.addStep(
|
|
164
|
-
newIndexSearchStep(deps.store, projectSearchExtractor, deps.logger),
|
|
165
|
-
)
|
|
166
|
-
.build()
|
|
167
|
-
.execute(reqCtx);
|
|
168
|
-
return reqCtx.newState;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Update — chain per Go buildUpdatePipeline (update.go:55-62). Deliberately
|
|
173
|
-
* narrower than create (no ValidateVisibility, no NormalizeReferences —
|
|
174
|
-
* the asymmetry is contract; see the module header).
|
|
175
|
-
*/
|
|
176
|
-
async function update(
|
|
177
|
-
deps: ProjectControllerDeps,
|
|
178
|
-
project: Project,
|
|
179
|
-
ctx: HandlerContext,
|
|
180
|
-
): Promise<Project> {
|
|
181
|
-
const reqCtx = new RequestContext(
|
|
182
|
-
ProjectSchema,
|
|
183
|
-
project,
|
|
184
|
-
callerIdentityOf(ctx),
|
|
185
|
-
kindOf(ctx),
|
|
186
|
-
);
|
|
187
|
-
await newPipeline<typeof ProjectSchema>("project-update", deps.logger)
|
|
188
|
-
.addStep(
|
|
189
|
-
newAuthorizeStep(ProjectCommandController.method.update, deps.authorizer),
|
|
190
|
-
)
|
|
191
|
-
.addStep(newValidateProtoStep())
|
|
192
|
-
.addStep(newResolveSlugStep())
|
|
193
|
-
.addStep(newLoadExistingStep(deps.store))
|
|
194
|
-
.addStep(newBuildUpdateStateStep())
|
|
195
|
-
.addStep(newPersistStep(deps.store))
|
|
196
|
-
.addStep(
|
|
197
|
-
newIndexSearchStep(deps.store, projectSearchExtractor, deps.logger),
|
|
198
|
-
)
|
|
199
|
-
.build()
|
|
200
|
-
.execute(reqCtx);
|
|
201
|
-
return reqCtx.newState;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Apply — declarative create-or-update PLUS membership reconciliation
|
|
206
|
-
* (apply.go:29-85): a minimal pipeline decides existence, the previous
|
|
207
|
-
* members are captured BEFORE delegation overwrites them, the full Create
|
|
208
|
-
* or Update handler runs, then the reconciler diffs previous vs persisted
|
|
209
|
-
* members and prunes orphans.
|
|
210
|
-
*
|
|
211
|
-
* Two ported postures:
|
|
212
|
-
* - A reconcile failure is logged and SWALLOWED — apply still succeeds,
|
|
213
|
-
* returning the persisted project without a summary (apply.go:77-82;
|
|
214
|
-
* defensive in both editions — the reconciler never throws).
|
|
215
|
-
* - On success the summary is set UNCONDITIONALLY, so every successful
|
|
216
|
-
* apply response carries status.last_reconciliation, even as an empty
|
|
217
|
-
* message. Response-only — never persisted.
|
|
218
|
-
*/
|
|
219
|
-
async function apply(
|
|
220
|
-
deps: ProjectControllerDeps,
|
|
221
|
-
reconciliation: ReconciliationService,
|
|
222
|
-
project: Project,
|
|
223
|
-
ctx: HandlerContext,
|
|
224
|
-
): Promise<Project> {
|
|
225
|
-
const reqCtx = new RequestContext(
|
|
226
|
-
ProjectSchema,
|
|
227
|
-
project,
|
|
228
|
-
callerIdentityOf(ctx),
|
|
229
|
-
kindOf(ctx),
|
|
230
|
-
);
|
|
231
|
-
await newPipeline<typeof ProjectSchema>("project-apply", deps.logger)
|
|
232
|
-
.addStep(
|
|
233
|
-
newAuthorizeStep(ProjectCommandController.method.apply, deps.authorizer),
|
|
234
|
-
)
|
|
235
|
-
.addStep(newValidateProtoStep())
|
|
236
|
-
.addStep(newResolveSlugStep())
|
|
237
|
-
.addStep(newLoadForApplyStep(deps.store))
|
|
238
|
-
.build()
|
|
239
|
-
.execute(reqCtx);
|
|
240
|
-
|
|
241
|
-
const shouldCreate = reqCtx.get(SHOULD_CREATE_KEY);
|
|
242
|
-
if (typeof shouldCreate !== "boolean") {
|
|
243
|
-
throw internalError(
|
|
244
|
-
new Error("apply pipeline did not set shouldCreate flag"),
|
|
245
|
-
"apply operation failed to determine create vs update",
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Captured before Create/Update runs; on first apply (create) there are
|
|
250
|
-
// no previous members (Go apply.go:45-50).
|
|
251
|
-
const previousMembers: ReadonlyArray<ApiResourceReference> = shouldCreate
|
|
252
|
-
? []
|
|
253
|
-
: ((reqCtx.get(EXISTING_RESOURCE_KEY) as Project | undefined)?.spec
|
|
254
|
-
?.members ?? []);
|
|
255
|
-
|
|
256
|
-
const persisted = shouldCreate
|
|
257
|
-
? await createProject(deps, project, ctx)
|
|
258
|
-
: await update(
|
|
259
|
-
deps,
|
|
260
|
-
withResolvedApplyId(ProjectSchema, project, reqCtx),
|
|
261
|
-
ctx,
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
const currentMembers = persisted.spec?.members ?? [];
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
const result = await reconciliation.reconcile(
|
|
268
|
-
previousMembers,
|
|
269
|
-
currentMembers,
|
|
270
|
-
DEFAULT_RECONCILIATION_OPTIONS,
|
|
271
|
-
);
|
|
272
|
-
persisted.status ??= create(ProjectStatusSchema);
|
|
273
|
-
persisted.status.lastReconciliation = toProtoSummary(result);
|
|
274
|
-
} catch (error) {
|
|
275
|
-
deps.logger.error("Reconciliation failed", {
|
|
276
|
-
projectId: persisted.metadata?.id ?? "",
|
|
277
|
-
error: error instanceof Error ? error.message : String(error),
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
return persisted;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Delete — returns the pre-delete resource (delete.go). Deletes ONLY the
|
|
285
|
-
* project entity: member resources are never cascaded.
|
|
286
|
-
*/
|
|
287
|
-
async function deleteProject(
|
|
288
|
-
deps: ProjectControllerDeps,
|
|
289
|
-
projectId: ProjectId,
|
|
290
|
-
ctx: HandlerContext,
|
|
291
|
-
): Promise<Project> {
|
|
292
|
-
const reqCtx = new RequestContext(
|
|
293
|
-
ProjectCommandController.method.delete.input,
|
|
294
|
-
projectId,
|
|
295
|
-
callerIdentityOf(ctx),
|
|
296
|
-
kindOf(ctx),
|
|
297
|
-
);
|
|
298
|
-
await newPipeline<typeof ProjectCommandController.method.delete.input>(
|
|
299
|
-
"project-delete",
|
|
300
|
-
deps.logger,
|
|
301
|
-
)
|
|
302
|
-
.addStep(
|
|
303
|
-
newAuthorizeStep(ProjectCommandController.method.delete, deps.authorizer),
|
|
304
|
-
)
|
|
305
|
-
.addStep(newValidateProtoStep())
|
|
306
|
-
.addStep(newExtractResourceIdStep())
|
|
307
|
-
.addStep(newLoadExistingForDeleteStep(deps.store, ProjectSchema))
|
|
308
|
-
.addStep(newDeleteResourceStep(deps.store))
|
|
309
|
-
.addStep(
|
|
310
|
-
newCleanupIamPoliciesStep(deps.authorizationLifecycle, deps.logger),
|
|
311
|
-
)
|
|
312
|
-
.addStep(newDeleteSearchIndexStep(deps.store, deps.logger))
|
|
313
|
-
.build()
|
|
314
|
-
.execute(reqCtx);
|
|
315
|
-
|
|
316
|
-
const deleted = reqCtx.get(EXISTING_RESOURCE_KEY);
|
|
317
|
-
if (deleted === undefined) {
|
|
318
|
-
// Go puts this text ON THE WIRE (delete.go:43, InternalError(nil, …));
|
|
319
|
-
// the arm is unreachable in practice but the copy is byte-pinned.
|
|
320
|
-
throw internalError(
|
|
321
|
-
new Error("delete pipeline completed without a loaded resource"),
|
|
322
|
-
"deleted project not found in context",
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
return deleted as Project;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** Get — LoadTarget by id; NotFound when absent (get.go). */
|
|
329
|
-
async function get(
|
|
330
|
-
deps: ProjectControllerDeps,
|
|
331
|
-
projectId: ProjectId,
|
|
332
|
-
ctx: HandlerContext,
|
|
333
|
-
): Promise<Project> {
|
|
334
|
-
const reqCtx = new RequestContext(
|
|
335
|
-
ProjectQueryController.method.get.input,
|
|
336
|
-
projectId,
|
|
337
|
-
callerIdentityOf(ctx),
|
|
338
|
-
kindOf(ctx),
|
|
339
|
-
);
|
|
340
|
-
await newPipeline<typeof ProjectQueryController.method.get.input>(
|
|
341
|
-
"project-get",
|
|
342
|
-
deps.logger,
|
|
343
|
-
)
|
|
344
|
-
.addStep(
|
|
345
|
-
newAuthorizeStep(ProjectQueryController.method.get, deps.authorizer),
|
|
346
|
-
)
|
|
347
|
-
.addStep(newValidateProtoStep())
|
|
348
|
-
.addStep(newLoadTargetStep(deps.store, ProjectSchema))
|
|
349
|
-
.build()
|
|
350
|
-
.execute(reqCtx);
|
|
351
|
-
return reqCtx.get(TARGET_RESOURCE_KEY) as Project;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* GetByReference — slug lookup within an org (get_by_reference.go). Project
|
|
356
|
-
* is org-scoped, so an empty-org reference is rejected InvalidArgument by
|
|
357
|
-
* the shared step's kind_meta scope check — a bare slug is under-specified,
|
|
358
|
-
* never a global search (the conformance suite pins this; Go's file-level
|
|
359
|
-
* doc comment claiming a platform-scoped fallback is stale).
|
|
360
|
-
*/
|
|
361
|
-
async function getByReference(
|
|
362
|
-
deps: ProjectControllerDeps,
|
|
363
|
-
ref: ApiResourceReference,
|
|
364
|
-
ctx: HandlerContext,
|
|
365
|
-
): Promise<Project> {
|
|
366
|
-
const reqCtx = new RequestContext(
|
|
367
|
-
ProjectQueryController.method.getByReference.input,
|
|
368
|
-
ref,
|
|
369
|
-
callerIdentityOf(ctx),
|
|
370
|
-
kindOf(ctx),
|
|
371
|
-
);
|
|
372
|
-
await newPipeline<typeof ProjectQueryController.method.getByReference.input>(
|
|
373
|
-
"project-get-by-reference",
|
|
374
|
-
deps.logger,
|
|
375
|
-
)
|
|
376
|
-
.addStep(
|
|
377
|
-
newAuthorizeStep(
|
|
378
|
-
ProjectQueryController.method.getByReference,
|
|
379
|
-
deps.authorizer,
|
|
380
|
-
),
|
|
381
|
-
)
|
|
382
|
-
.addStep(newValidateProtoStep())
|
|
383
|
-
.addStep(newLoadByReferenceStep(deps.store, ProjectSchema))
|
|
384
|
-
.build()
|
|
385
|
-
.execute(reqCtx);
|
|
386
|
-
return reqCtx.get(TARGET_RESOURCE_KEY) as Project;
|
|
387
|
-
}
|
|
@@ -1,296 +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
|
-
|
|
30
|
-
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
31
|
-
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
32
|
-
import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
33
|
-
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
34
|
-
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
35
|
-
import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
36
|
-
import { ReconciliationSummarySchema } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/status_pb";
|
|
37
|
-
import type { ReconciliationSummary } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/status_pb";
|
|
38
|
-
|
|
39
|
-
import type { Logger } from "../../boot/logger.js";
|
|
40
|
-
import { findResourceBySlug } from "../../pipeline/steps/helpers.js";
|
|
41
|
-
import { metadataOf } from "../../pipeline/steps/shapes.js";
|
|
42
|
-
import type { Store } from "../../store/interface.js";
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Deletes one orphaned resource by kind and id — Go's ResourceDeleter, the
|
|
46
|
-
* only execution capability the reference model needs (resources are
|
|
47
|
-
* created/updated individually by the CLI; the server only deletes orphans).
|
|
48
|
-
* Implemented over the four in-process command clients (boot/inprocess.ts).
|
|
49
|
-
*/
|
|
50
|
-
export interface OrphanDeleter {
|
|
51
|
-
delete(kind: ApiResourceKind, resourceId: string): Promise<void>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Go ReconciliationOptions, reduced to the two flags the algorithm reads.
|
|
56
|
-
* Production always passes DEFAULT_RECONCILIATION_OPTIONS; the other
|
|
57
|
-
* combinations remain reachable for tests because the branches are part of
|
|
58
|
-
* the ported algorithm.
|
|
59
|
-
*/
|
|
60
|
-
export interface ReconciliationOptions {
|
|
61
|
-
/** When true, orphaned resources are deleted (Go default). */
|
|
62
|
-
readonly pruneEnabled: boolean;
|
|
63
|
-
/** When true, the plan is computed but nothing is deleted. */
|
|
64
|
-
readonly dryRun: boolean;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export const DEFAULT_RECONCILIATION_OPTIONS: ReconciliationOptions = {
|
|
68
|
-
pruneEnabled: true,
|
|
69
|
-
dryRun: false,
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
/** One per-orphan failure — log-only, never wire-visible (Go, verified). */
|
|
73
|
-
export interface ReconciliationError {
|
|
74
|
-
/** "{kind}:{slug}" of the orphan that failed. */
|
|
75
|
-
readonly resourceKey: string;
|
|
76
|
-
readonly message: string;
|
|
77
|
-
readonly cause: unknown;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Go ReconciliationResult, as a plain immutable value. */
|
|
81
|
-
export interface ReconciliationResult {
|
|
82
|
-
/** References in current but not previous — newly associated members. */
|
|
83
|
-
readonly added: ReadonlyArray<ApiResourceReference>;
|
|
84
|
-
/**
|
|
85
|
-
* Orphans successfully deleted (or, on a dry run, the orphans that WOULD
|
|
86
|
-
* be deleted — Go reports them in the same slot without deleting).
|
|
87
|
-
*/
|
|
88
|
-
readonly removed: ReadonlyArray<ApiResourceReference>;
|
|
89
|
-
readonly errors: ReadonlyArray<ReconciliationError>;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface ReconciliationService {
|
|
93
|
-
/**
|
|
94
|
-
* Compares memberships and prunes orphans per the options. NEVER throws —
|
|
95
|
-
* Go's implementation always returns a nil error; failures ride
|
|
96
|
-
* result.errors (and the apply handler's error-swallow arm upstream is
|
|
97
|
-
* therefore defensive dead code in both editions, ported anyway).
|
|
98
|
-
*/
|
|
99
|
-
reconcile(
|
|
100
|
-
previousMembers: ReadonlyArray<ApiResourceReference>,
|
|
101
|
-
currentMembers: ReadonlyArray<ApiResourceReference>,
|
|
102
|
-
options: ReconciliationOptions,
|
|
103
|
-
): Promise<ReconciliationResult>;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface ReconciliationServiceDeps {
|
|
107
|
-
readonly store: Store;
|
|
108
|
-
/**
|
|
109
|
-
* Lazy per the compose root's boot-ordering idiom: the in-process clients
|
|
110
|
-
* behind the deleter exist only after routes registration completes.
|
|
111
|
-
*/
|
|
112
|
-
readonly orphanDeleter: () => OrphanDeleter;
|
|
113
|
-
readonly logger: Logger;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function newReconciliationService(
|
|
117
|
-
deps: ReconciliationServiceDeps,
|
|
118
|
-
): ReconciliationService {
|
|
119
|
-
return {
|
|
120
|
-
async reconcile(previousMembers, currentMembers, options) {
|
|
121
|
-
const previousSet = buildReferenceSet(previousMembers);
|
|
122
|
-
const currentSet = buildReferenceSet(currentMembers);
|
|
123
|
-
|
|
124
|
-
const added = currentMembers.filter(
|
|
125
|
-
(ref) => !previousSet.has(referenceKey(ref)),
|
|
126
|
-
);
|
|
127
|
-
const orphans = previousMembers.filter(
|
|
128
|
-
(ref) => !currentSet.has(referenceKey(ref)),
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
if (added.length === 0 && orphans.length === 0) {
|
|
132
|
-
return { added: [], removed: [], errors: [] };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Dry run reports the orphans in `removed` WITHOUT deleting (Go
|
|
136
|
-
// service.go:61-63 passes them straight through).
|
|
137
|
-
if (options.dryRun) {
|
|
138
|
-
return { added, removed: orphans, errors: [] };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (!options.pruneEnabled || orphans.length === 0) {
|
|
142
|
-
return { added, removed: [], errors: [] };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const { removed, errors } = await deleteOrphans(deps, orphans);
|
|
146
|
-
return { added, removed, errors };
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Deletes each orphan, continuing on failure (Go deleteOrphans). Failures
|
|
153
|
-
* are warned here — Go collects them onto a result nothing in production
|
|
154
|
-
* reads, which leaves operators blind; the log is edition-local and the
|
|
155
|
-
* wire behavior is identical.
|
|
156
|
-
*/
|
|
157
|
-
async function deleteOrphans(
|
|
158
|
-
deps: ReconciliationServiceDeps,
|
|
159
|
-
orphans: ReadonlyArray<ApiResourceReference>,
|
|
160
|
-
): Promise<{
|
|
161
|
-
removed: ApiResourceReference[];
|
|
162
|
-
errors: ReconciliationError[];
|
|
163
|
-
}> {
|
|
164
|
-
const removed: ApiResourceReference[] = [];
|
|
165
|
-
const errors: ReconciliationError[] = [];
|
|
166
|
-
|
|
167
|
-
for (const ref of orphans) {
|
|
168
|
-
const refKey = referenceKey(ref);
|
|
169
|
-
|
|
170
|
-
let resourceId: string;
|
|
171
|
-
try {
|
|
172
|
-
resourceId = await resolveResourceId(deps.store, ref);
|
|
173
|
-
} catch (cause) {
|
|
174
|
-
errors.push({
|
|
175
|
-
resourceKey: refKey,
|
|
176
|
-
message: "failed to resolve resource for deletion",
|
|
177
|
-
cause,
|
|
178
|
-
});
|
|
179
|
-
deps.logger.warn("reconciliation: failed to resolve orphan for deletion", {
|
|
180
|
-
resource: refKey,
|
|
181
|
-
error: cause instanceof Error ? cause.message : String(cause),
|
|
182
|
-
});
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
await deps.orphanDeleter().delete(ref.kind, resourceId);
|
|
188
|
-
} catch (cause) {
|
|
189
|
-
errors.push({
|
|
190
|
-
resourceKey: refKey,
|
|
191
|
-
message: "failed to delete orphaned resource",
|
|
192
|
-
cause,
|
|
193
|
-
});
|
|
194
|
-
deps.logger.warn("reconciliation: failed to delete orphaned resource", {
|
|
195
|
-
resource: refKey,
|
|
196
|
-
resourceId,
|
|
197
|
-
error: cause instanceof Error ? cause.message : String(cause),
|
|
198
|
-
});
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
removed.push(ref);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return { removed, errors };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Resolves an orphan reference to its resource id, within the org the
|
|
210
|
-
* reference names. Slugs are org-scoped identifiers: a local backend holds
|
|
211
|
-
* the `stigmer` system org beside the user's, a self-host holds many, and
|
|
212
|
-
* the same slug may exist in several. The Go port matched by slug alone and
|
|
213
|
-
* could prune another org's resource (stigmer/stigmer#1151); the CLI has
|
|
214
|
-
* always stamped `org` on every member reference, so scoping is a change
|
|
215
|
-
* of lookup, not of contract. A reference without an org (a project written
|
|
216
|
-
* before members carried one) keeps the unscoped match, the helper's
|
|
217
|
-
* documented empty-org arm.
|
|
218
|
-
*/
|
|
219
|
-
async function resolveResourceId(
|
|
220
|
-
store: Store,
|
|
221
|
-
ref: ApiResourceReference,
|
|
222
|
-
): Promise<string> {
|
|
223
|
-
const schema = schemaForKind(ref.kind);
|
|
224
|
-
const resource = await findResourceBySlug(
|
|
225
|
-
store,
|
|
226
|
-
ref.kind,
|
|
227
|
-
schema,
|
|
228
|
-
ref.slug,
|
|
229
|
-
ref.org,
|
|
230
|
-
);
|
|
231
|
-
if (resource === undefined) {
|
|
232
|
-
const where = ref.org === "" ? "" : ` in org '${ref.org}'`;
|
|
233
|
-
throw new Error(
|
|
234
|
-
`resource ${kindName(ref.kind)}/${ref.slug} not found${where}`,
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const id = metadataOf(resource)?.id ?? "";
|
|
239
|
-
if (id === "") {
|
|
240
|
-
throw new Error(`resource ${kindName(ref.kind)}/${ref.slug} has no ID`);
|
|
241
|
-
}
|
|
242
|
-
return id;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* The four member kinds the reconciler supports (Go newProtoForKind);
|
|
247
|
-
* anything else is an unsupported-kind resolution error.
|
|
248
|
-
*/
|
|
249
|
-
function schemaForKind(kind: ApiResourceKind) {
|
|
250
|
-
switch (kind) {
|
|
251
|
-
case ApiResourceKind.agent:
|
|
252
|
-
return AgentSchema;
|
|
253
|
-
case ApiResourceKind.workflow:
|
|
254
|
-
return WorkflowSchema;
|
|
255
|
-
case ApiResourceKind.mcp_server:
|
|
256
|
-
return McpServerSchema;
|
|
257
|
-
case ApiResourceKind.skill:
|
|
258
|
-
return SkillSchema;
|
|
259
|
-
default:
|
|
260
|
-
throw new Error(`unsupported resource kind: ${kindName(kind)}`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* "{kind}:{slug}" with the kind's enum NAME — Go fmt %s of the enum (e.g.
|
|
266
|
-
* "agent:my-agent", "mcp_server:tools"). The numeric form would diverge
|
|
267
|
-
* from Go's log/error copy.
|
|
268
|
-
*/
|
|
269
|
-
export function referenceKey(ref: ApiResourceReference): string {
|
|
270
|
-
return `${kindName(ref.kind)}:${ref.slug}`;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function kindName(kind: ApiResourceKind): string {
|
|
274
|
-
return ApiResourceKind[kind] ?? String(kind);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function buildReferenceSet(
|
|
278
|
-
refs: ReadonlyArray<ApiResourceReference>,
|
|
279
|
-
): Set<string> {
|
|
280
|
-
return new Set(refs.map(referenceKey));
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Go ToProtoSummary: added → created, updated ALWAYS empty (not applicable
|
|
285
|
-
* in the reference model), removed → deleted. Every successful apply sets
|
|
286
|
-
* the summary — even an all-empty one is a PRESENT message on the wire.
|
|
287
|
-
*/
|
|
288
|
-
export function toProtoSummary(
|
|
289
|
-
result: ReconciliationResult,
|
|
290
|
-
): ReconciliationSummary {
|
|
291
|
-
return create(ReconciliationSummarySchema, {
|
|
292
|
-
created: [...result.added],
|
|
293
|
-
updated: [],
|
|
294
|
-
deleted: [...result.removed],
|
|
295
|
-
});
|
|
296
|
-
}
|