@stigmer/server 3.38.3 → 3.39.1-dev.20260928194002
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/domain/agent/controller.d.ts +2 -2
- package/dist/domain/agent/controller.d.ts.map +1 -1
- package/dist/domain/agent/controller.js +7 -3
- package/dist/domain/agent/controller.js.map +1 -1
- package/dist/domain/agentchannel/controller.d.ts +2 -2
- package/dist/domain/agentchannel/controller.d.ts.map +1 -1
- package/dist/domain/agentchannel/controller.js +10 -5
- package/dist/domain/agentchannel/controller.js.map +1 -1
- package/dist/domain/agentinstance/controller.d.ts +2 -2
- package/dist/domain/agentinstance/controller.d.ts.map +1 -1
- package/dist/domain/agentinstance/controller.js +7 -3
- package/dist/domain/agentinstance/controller.js.map +1 -1
- package/dist/domain/agentinstance/steps.d.ts +3 -1
- package/dist/domain/agentinstance/steps.d.ts.map +1 -1
- package/dist/domain/agentinstance/steps.js +12 -6
- package/dist/domain/agentinstance/steps.js.map +1 -1
- package/dist/domain/agentshare/controller.d.ts +2 -2
- package/dist/domain/agentshare/controller.d.ts.map +1 -1
- package/dist/domain/agentshare/controller.js +7 -3
- package/dist/domain/agentshare/controller.js.map +1 -1
- package/dist/domain/organization/controller.d.ts.map +1 -1
- package/dist/domain/organization/controller.js +33 -14
- package/dist/domain/organization/controller.js.map +1 -1
- package/dist/domain/organization/slug-ledger.d.ts +86 -0
- package/dist/domain/organization/slug-ledger.d.ts.map +1 -0
- package/dist/domain/organization/slug-ledger.js +128 -0
- package/dist/domain/organization/slug-ledger.js.map +1 -0
- package/dist/domain/organization/steps.d.ts +12 -3
- package/dist/domain/organization/steps.d.ts.map +1 -1
- package/dist/domain/organization/steps.js +28 -6
- package/dist/domain/organization/steps.js.map +1 -1
- package/dist/domain/plugin/controller.d.ts.map +1 -1
- package/dist/domain/plugin/controller.js +10 -5
- package/dist/domain/plugin/controller.js.map +1 -1
- package/dist/domain/skill/controller.d.ts.map +1 -1
- package/dist/domain/skill/controller.js +7 -3
- package/dist/domain/skill/controller.js.map +1 -1
- package/dist/domain/workflow/controller.js +6 -3
- package/dist/domain/workflow/controller.js.map +1 -1
- package/dist/domain/workflowinstance/controller.d.ts +2 -2
- package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
- package/dist/domain/workflowinstance/controller.js +10 -5
- package/dist/domain/workflowinstance/controller.js.map +1 -1
- package/dist/domain/workflowinstance/steps.d.ts +5 -3
- package/dist/domain/workflowinstance/steps.d.ts.map +1 -1
- package/dist/domain/workflowinstance/steps.js +14 -9
- package/dist/domain/workflowinstance/steps.js.map +1 -1
- package/dist/extensions/gate-slots.d.ts +7 -5
- package/dist/extensions/gate-slots.d.ts.map +1 -1
- package/dist/extensions/gate-slots.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/errors.d.ts +8 -0
- package/dist/pipeline/errors.d.ts.map +1 -1
- package/dist/pipeline/errors.js +19 -0
- package/dist/pipeline/errors.js.map +1 -1
- package/dist/store/interface.d.ts +67 -0
- package/dist/store/interface.d.ts.map +1 -1
- package/dist/store/interface.js.map +1 -1
- package/dist/store/organization-slug-history.d.ts +71 -0
- package/dist/store/organization-slug-history.d.ts.map +1 -0
- package/dist/store/organization-slug-history.js +88 -0
- package/dist/store/organization-slug-history.js.map +1 -0
- 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 +69 -1
- package/dist/store/postgres/migrations.js.map +1 -1
- package/dist/store/postgres/store.d.ts +2 -1
- package/dist/store/postgres/store.d.ts.map +1 -1
- package/dist/store/postgres/store.js +49 -0
- package/dist/store/postgres/store.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 +54 -1
- package/dist/store/sqlite/migrations.js.map +1 -1
- package/dist/store/sqlite/store.d.ts +2 -1
- package/dist/store/sqlite/store.d.ts.map +1 -1
- package/dist/store/sqlite/store.js +55 -0
- package/dist/store/sqlite/store.js.map +1 -1
- package/package.json +6 -6
- package/src/domain/agent/__tests__/store-faults.test.ts +105 -0
- package/src/domain/agent/controller.ts +9 -5
- package/src/domain/agentchannel/__tests__/store-faults.test.ts +126 -0
- package/src/domain/agentchannel/controller.ts +12 -7
- package/src/domain/agentinstance/__tests__/store-faults.test.ts +161 -0
- package/src/domain/agentinstance/controller.ts +9 -5
- package/src/domain/agentinstance/steps.ts +12 -6
- package/src/domain/agentshare/__tests__/store-faults.test.ts +101 -0
- package/src/domain/agentshare/controller.ts +9 -5
- package/src/domain/organization/__tests__/organization-delete.test.ts +73 -18
- package/src/domain/organization/__tests__/organization-slugs.test.ts +386 -0
- package/src/domain/organization/controller.ts +36 -14
- package/src/domain/organization/slug-ledger.ts +209 -0
- package/src/domain/organization/steps.ts +28 -6
- package/src/domain/plugin/__tests__/store-faults.test.ts +104 -0
- package/src/domain/plugin/controller.ts +10 -5
- package/src/domain/skill/__tests__/store-faults.test.ts +103 -0
- package/src/domain/skill/controller.ts +7 -3
- package/src/domain/workflow/__tests__/store-faults.test.ts +107 -0
- package/src/domain/workflow/controller.ts +6 -3
- package/src/domain/workflowinstance/__tests__/store-faults.test.ts +172 -0
- package/src/domain/workflowinstance/controller.ts +12 -7
- package/src/domain/workflowinstance/steps.ts +14 -10
- package/src/extensions/gate-slots.ts +7 -5
- package/src/index.ts +11 -0
- package/src/pipeline/__tests__/blind-not-found.test.ts +4 -24
- package/src/pipeline/errors.ts +23 -0
- package/src/store/__tests__/organization-slug-history.test.ts +109 -0
- package/src/store/__tests__/store-contract.ts +86 -1
- package/src/store/interface.ts +70 -0
- package/src/store/organization-slug-history.ts +159 -0
- package/src/store/postgres/__tests__/migrations.test.ts +138 -2
- package/src/store/postgres/__tests__/store-contract.test.ts +1 -0
- package/src/store/postgres/migrations.ts +84 -1
- package/src/store/postgres/store.ts +84 -0
- package/src/store/sqlite/__tests__/migrations.test.ts +139 -10
- package/src/store/sqlite/migrations.ts +69 -1
- package/src/store/sqlite/store.ts +84 -0
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* uniqueness guarantee, mirroring cloud's OrganizationCreateHandler
|
|
11
11
|
* (CheckDuplicate + CopySlugToId) step-for-step.
|
|
12
12
|
*
|
|
13
|
-
* The same deviation shapes the delete
|
|
14
|
-
*
|
|
15
|
-
* rows BEFORE its row is deleted, and fails the
|
|
13
|
+
* The same deviation shapes the delete. A slug is never taken twice
|
|
14
|
+
* (slug-ledger.ts), and newRevokeOrganizationPoliciesStep still revokes
|
|
15
|
+
* the organization's policy rows BEFORE its row is deleted, and fails the
|
|
16
|
+
* delete when it cannot, so nothing that grants on the organization
|
|
17
|
+
* outlives it.
|
|
16
18
|
*
|
|
17
19
|
* Proven by organization.conformance.test.ts (CONFORMANCE_TARGET=local),
|
|
18
20
|
* __tests__/organization.test.ts and __tests__/organization-delete.test.ts.
|
|
@@ -28,6 +30,7 @@ import type { RequestContext } from "../../pipeline/request-context.js";
|
|
|
28
30
|
import { EXISTING_RESOURCE_KEY } from "../../pipeline/steps/load-existing.js";
|
|
29
31
|
import { metadataOf } from "../../pipeline/steps/shapes.js";
|
|
30
32
|
import type { IamPolicyGrantPath } from "../iampolicy/grant-path.js";
|
|
33
|
+
import { refusalForHeldSlug } from "./slug-ledger.js";
|
|
31
34
|
|
|
32
35
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
33
36
|
import { ApiResourceRefSchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
|
|
@@ -48,6 +51,13 @@ import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organizat
|
|
|
48
51
|
* organization. Checking existence by id (== the resolved slug) closes
|
|
49
52
|
* that hole and mirrors cloud's OrganizationCreateHandler.CheckDuplicate.
|
|
50
53
|
*
|
|
54
|
+
* A slug is taken for good (slug-ledger.ts), so the slug ledger is read
|
|
55
|
+
* first: a retired slug is refused with the reserved reason, a held one
|
|
56
|
+
* with the duplicate copy. The row is read after it for an organization no
|
|
57
|
+
* ledger entry records yet, one an older binary created during a rolling
|
|
58
|
+
* upgrade. This read is the early refusal, before any gate; the atomic
|
|
59
|
+
* guarantee is ClaimOrganizationSlug's, immediately before Persist.
|
|
60
|
+
*
|
|
51
61
|
* Runs after ResolveSlug (slug is set) and before BuildNewState/
|
|
52
62
|
* CopySlugToId (the id is not yet minted), so it keys on the slug value
|
|
53
63
|
* that will become the id.
|
|
@@ -70,6 +80,16 @@ export function newCheckOrgDuplicateStep(
|
|
|
70
80
|
throw internalError(new Error("organization slug is empty"), "duplicate check");
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
let entry;
|
|
84
|
+
try {
|
|
85
|
+
entry = await store.organizationSlugs.find(metadata.slug);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw internalError(error, "failed to check for duplicate organization");
|
|
88
|
+
}
|
|
89
|
+
if (entry !== undefined) {
|
|
90
|
+
throw refusalForHeldSlug(entry);
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
try {
|
|
74
94
|
await store.getResource(
|
|
75
95
|
ctx.apiResourceKind,
|
|
@@ -120,9 +140,11 @@ export function newCopySlugToIdStep(): PipelineStep<typeof OrganizationSchema> {
|
|
|
120
140
|
*
|
|
121
141
|
* The generic delete chains clean up after the row and log a fault,
|
|
122
142
|
* because a deleted resource's rows grant nothing once it is gone. An
|
|
123
|
-
* organization is the exception: its
|
|
124
|
-
*
|
|
125
|
-
*
|
|
143
|
+
* organization is the exception: its rows are the grants on the tenancy
|
|
144
|
+
* root itself, and the scope link of every resource under it. The slug is
|
|
145
|
+
* never taken again (slug-ledger.ts), so they cannot pass to a new holder,
|
|
146
|
+
* but a row left behind would still be a grant nobody administers. So this
|
|
147
|
+
* step does not catch. A fault fails the delete with the
|
|
126
148
|
* organization in place, and a retry resumes where the revocation stopped
|
|
127
149
|
* (the grant path revokes the organization's owners last, so the owner
|
|
128
150
|
* who retries still can). The grant path fires the composed driver's
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the plugin's visibility-update load: a
|
|
3
|
+
* typed ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
4
|
+
* (`plugin not found: <id>`), and any other store failure is an
|
|
5
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound that
|
|
6
|
+
* tells a client the plugin does not exist (stigmer/stigmer#1345).
|
|
7
|
+
*
|
|
8
|
+
* The surface is updateVisibility, reached through the registered handler on
|
|
9
|
+
* an in-process router. The composed suites reach only a real store, which
|
|
10
|
+
* cannot fail selectively, so it runs here against a store whose read throws.
|
|
11
|
+
* The archive store and the member materializer are untouchable: a load that
|
|
12
|
+
* fails must stop the call before the plugin's level fans out to its members.
|
|
13
|
+
*
|
|
14
|
+
* Out of scope: the NotFound copy itself (wire contract).
|
|
15
|
+
*/
|
|
16
|
+
import type { Client, ConnectError } from "@connectrpc/connect";
|
|
17
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
18
|
+
import { describe, expect, it } from "vitest";
|
|
19
|
+
|
|
20
|
+
import { PluginCommandController } from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/command_pb";
|
|
21
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
22
|
+
|
|
23
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
24
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
25
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
26
|
+
import {
|
|
27
|
+
errorOf,
|
|
28
|
+
failingStore,
|
|
29
|
+
untouchable,
|
|
30
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
31
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
32
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
33
|
+
import type { Store } from "../../../store/interface.js";
|
|
34
|
+
|
|
35
|
+
import { registerPluginServices } from "../controller.js";
|
|
36
|
+
|
|
37
|
+
const silentLogger = createLogger({
|
|
38
|
+
level: "error",
|
|
39
|
+
pretty: false,
|
|
40
|
+
write: () => {},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const PLUGIN_ID = "plg_storefault";
|
|
44
|
+
|
|
45
|
+
const NOT_FOUND_COPY = `plugin not found: ${PLUGIN_ID}`;
|
|
46
|
+
const LOAD_FAULT_COPY = "failed to load plugin";
|
|
47
|
+
|
|
48
|
+
const MISSING = (): Error => new ResourceNotFoundError(`plugin/${PLUGIN_ID}`);
|
|
49
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
53
|
+
* the trusted-local caller and the apiresource interceptor the kind, then
|
|
54
|
+
* Authorize and ValidateProto run and the load is the first step that reads
|
|
55
|
+
* the store.
|
|
56
|
+
*/
|
|
57
|
+
function pluginCommand(store: Store): Client<typeof PluginCommandController> {
|
|
58
|
+
const transport = createRouterTransport(
|
|
59
|
+
(router) => {
|
|
60
|
+
registerPluginServices(router, {
|
|
61
|
+
store,
|
|
62
|
+
logger: silentLogger,
|
|
63
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
64
|
+
authorizationLifecycle: undefined,
|
|
65
|
+
artifactStorage: untouchable("artifactStorage"),
|
|
66
|
+
materializerProvider: untouchable("materializerProvider"),
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
router: {
|
|
71
|
+
interceptors: [
|
|
72
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
73
|
+
createApiResourceInterceptor(),
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
return createClient(PluginCommandController, transport);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("updateVisibility — LoadPluginForVisibilityUpdate", () => {
|
|
82
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
83
|
+
return errorOf(() =>
|
|
84
|
+
pluginCommand(store).updateVisibility({
|
|
85
|
+
resourceId: PLUGIN_ID,
|
|
86
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
it("a missing plugin answers NotFound with the domain's copy", async () => {
|
|
92
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
93
|
+
|
|
94
|
+
expect(error.code).toBe(Code.NotFound);
|
|
95
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
99
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
100
|
+
|
|
101
|
+
expect(error.code).toBe(Code.Internal);
|
|
102
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* in-process clients it rides exist only after the routes this controller
|
|
17
17
|
* is registered in.
|
|
18
18
|
*
|
|
19
|
-
* Proven by __tests__/plugin.test.ts (composed-server round-trips)
|
|
20
|
-
* plugin.conformance.test.ts
|
|
19
|
+
* Proven by __tests__/plugin.test.ts (composed-server round-trips),
|
|
20
|
+
* __tests__/store-faults.test.ts and plugin.conformance.test.ts
|
|
21
|
+
* (CONFORMANCE_TARGET=local, local-postgres).
|
|
21
22
|
*/
|
|
22
23
|
import { create, fromBinary } from "@bufbuild/protobuf";
|
|
23
24
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
@@ -104,6 +105,7 @@ import {
|
|
|
104
105
|
} from "../../pipeline/steps/authorize-resolved-target.js";
|
|
105
106
|
import type { VersionHistoryBinding } from "../../pipeline/steps/version-history.js";
|
|
106
107
|
import { metadataOf } from "../../pipeline/steps/shapes.js";
|
|
108
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
107
109
|
import type { Store } from "../../store/interface.js";
|
|
108
110
|
import type {
|
|
109
111
|
ArchiveStaging,
|
|
@@ -386,7 +388,7 @@ async function updateVisibility(
|
|
|
386
388
|
return reqCtx.get(UPDATE_VISIBILITY_PLUGIN_KEY) as Plugin;
|
|
387
389
|
}
|
|
388
390
|
|
|
389
|
-
/** Loads the plugin by resource_id;
|
|
391
|
+
/** Loads the plugin by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
390
392
|
function newLoadPluginByIdStep(
|
|
391
393
|
store: Store,
|
|
392
394
|
key: string,
|
|
@@ -401,8 +403,11 @@ function newLoadPluginByIdStep(
|
|
|
401
403
|
ctx.input.resourceId,
|
|
402
404
|
PluginSchema,
|
|
403
405
|
);
|
|
404
|
-
} catch {
|
|
405
|
-
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (error instanceof ResourceNotFoundError) {
|
|
408
|
+
throw notFoundError("plugin", ctx.input.resourceId);
|
|
409
|
+
}
|
|
410
|
+
throw internalError(error, "failed to load plugin");
|
|
406
411
|
}
|
|
407
412
|
ctx.set(key, plugin);
|
|
408
413
|
},
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the skill's visibility-update load: a
|
|
3
|
+
* typed ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
4
|
+
* (`skill not found: <id>`), and any other store failure is an
|
|
5
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound that
|
|
6
|
+
* tells a client the skill does not exist (stigmer/stigmer#1345).
|
|
7
|
+
*
|
|
8
|
+
* The surface is updateVisibility, reached through the registered handler on
|
|
9
|
+
* an in-process router. The composed suites reach only a real store, which
|
|
10
|
+
* cannot fail selectively, so it runs here against a store whose read throws.
|
|
11
|
+
* The artifact storage is untouchable: a load that fails must stop the call
|
|
12
|
+
* before anything past it runs.
|
|
13
|
+
*
|
|
14
|
+
* Out of scope: the NotFound copy itself (wire contract).
|
|
15
|
+
*/
|
|
16
|
+
import type { Client, ConnectError } from "@connectrpc/connect";
|
|
17
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
18
|
+
import { describe, expect, it } from "vitest";
|
|
19
|
+
|
|
20
|
+
import { SkillCommandController } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/command_pb";
|
|
21
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
22
|
+
|
|
23
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
24
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
25
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
26
|
+
import {
|
|
27
|
+
errorOf,
|
|
28
|
+
failingStore,
|
|
29
|
+
untouchable,
|
|
30
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
31
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
32
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
33
|
+
import type { Store } from "../../../store/interface.js";
|
|
34
|
+
|
|
35
|
+
import { registerSkillServices } from "../controller.js";
|
|
36
|
+
|
|
37
|
+
const silentLogger = createLogger({
|
|
38
|
+
level: "error",
|
|
39
|
+
pretty: false,
|
|
40
|
+
write: () => {},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const SKILL_ID = "skl_storefault";
|
|
44
|
+
|
|
45
|
+
const NOT_FOUND_COPY = `skill not found: ${SKILL_ID}`;
|
|
46
|
+
const LOAD_FAULT_COPY = "failed to load skill";
|
|
47
|
+
|
|
48
|
+
const MISSING = (): Error => new ResourceNotFoundError(`skill/${SKILL_ID}`);
|
|
49
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
53
|
+
* the trusted-local caller and the apiresource interceptor the kind, then
|
|
54
|
+
* Authorize and ValidateProto run and the load is the first step that reads
|
|
55
|
+
* the store.
|
|
56
|
+
*/
|
|
57
|
+
function skillCommand(store: Store): Client<typeof SkillCommandController> {
|
|
58
|
+
const transport = createRouterTransport(
|
|
59
|
+
(router) => {
|
|
60
|
+
registerSkillServices(router, {
|
|
61
|
+
store,
|
|
62
|
+
logger: silentLogger,
|
|
63
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
64
|
+
authorizationLifecycle: undefined,
|
|
65
|
+
artifactStorage: untouchable("artifactStorage"),
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
router: {
|
|
70
|
+
interceptors: [
|
|
71
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
72
|
+
createApiResourceInterceptor(),
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
return createClient(SkillCommandController, transport);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("updateVisibility — LoadSkillForVisibilityUpdate", () => {
|
|
81
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
82
|
+
return errorOf(() =>
|
|
83
|
+
skillCommand(store).updateVisibility({
|
|
84
|
+
resourceId: SKILL_ID,
|
|
85
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
it("a missing skill answers NotFound with the domain's copy", async () => {
|
|
91
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
92
|
+
|
|
93
|
+
expect(error.code).toBe(Code.NotFound);
|
|
94
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
98
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
99
|
+
|
|
100
|
+
expect(error.code).toBe(Code.Internal);
|
|
101
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -99,6 +99,7 @@ import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-
|
|
|
99
99
|
import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
|
|
100
100
|
import { newValidateVisibilityUpdateStep } from "../../pipeline/steps/validate-visibility.js";
|
|
101
101
|
import { newDeleteVersionArchivesStep } from "../../pipeline/steps/version-archive.js";
|
|
102
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
102
103
|
import type { Store } from "../../store/interface.js";
|
|
103
104
|
import { MAX_ZIP_SIZE, TRANSFER_LANE_NOT_CONFIGURED } from "./constants.js";
|
|
104
105
|
import {
|
|
@@ -463,7 +464,7 @@ async function updateVisibility(
|
|
|
463
464
|
return reqCtx.get(UPDATE_VISIBILITY_SKILL_KEY) as Skill;
|
|
464
465
|
}
|
|
465
466
|
|
|
466
|
-
/** Loads the skill by resource_id;
|
|
467
|
+
/** Loads the skill by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
467
468
|
function newLoadSkillForVisibilityUpdateStep(
|
|
468
469
|
store: Store,
|
|
469
470
|
): PipelineStep<UpdateVisibilityDesc> {
|
|
@@ -478,8 +479,11 @@ function newLoadSkillForVisibilityUpdateStep(
|
|
|
478
479
|
input.resourceId,
|
|
479
480
|
SkillSchema,
|
|
480
481
|
);
|
|
481
|
-
} catch {
|
|
482
|
-
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (error instanceof ResourceNotFoundError) {
|
|
484
|
+
throw notFoundError("skill", input.resourceId);
|
|
485
|
+
}
|
|
486
|
+
throw internalError(error, "failed to load skill");
|
|
483
487
|
}
|
|
484
488
|
ctx.set(UPDATE_VISIBILITY_SKILL_KEY, skill);
|
|
485
489
|
},
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the workflow's visibility-update load: a
|
|
3
|
+
* typed ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
4
|
+
* (`workflow not found: <id>`), and any other store failure is an
|
|
5
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound that
|
|
6
|
+
* tells a client the workflow does not exist (stigmer/stigmer#1345).
|
|
7
|
+
*
|
|
8
|
+
* The surface is updateVisibility, reached through the registered handler on
|
|
9
|
+
* an in-process router. The composed suites reach only a real store, which
|
|
10
|
+
* cannot fail selectively, so it runs here against a store whose read throws.
|
|
11
|
+
* The validator and the default-instance creator are untouchable: a load that
|
|
12
|
+
* fails must stop the call before anything past it runs.
|
|
13
|
+
*
|
|
14
|
+
* Out of scope: the NotFound copy itself (wire contract).
|
|
15
|
+
*/
|
|
16
|
+
import type { Client, ConnectError } from "@connectrpc/connect";
|
|
17
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
18
|
+
import { describe, expect, it } from "vitest";
|
|
19
|
+
|
|
20
|
+
import { WorkflowCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/command_pb";
|
|
21
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
22
|
+
|
|
23
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
24
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
25
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
26
|
+
import {
|
|
27
|
+
errorOf,
|
|
28
|
+
failingStore,
|
|
29
|
+
untouchable,
|
|
30
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
31
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
32
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
33
|
+
import type { Store } from "../../../store/interface.js";
|
|
34
|
+
|
|
35
|
+
import { registerWorkflowServices } from "../controller.js";
|
|
36
|
+
|
|
37
|
+
const silentLogger = createLogger({
|
|
38
|
+
level: "error",
|
|
39
|
+
pretty: false,
|
|
40
|
+
write: () => {},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const WORKFLOW_ID = "wfl_storefault";
|
|
44
|
+
|
|
45
|
+
const NOT_FOUND_COPY = `workflow not found: ${WORKFLOW_ID}`;
|
|
46
|
+
const LOAD_FAULT_COPY = "failed to load workflow";
|
|
47
|
+
|
|
48
|
+
const MISSING = (): Error =>
|
|
49
|
+
new ResourceNotFoundError(`workflow/${WORKFLOW_ID}`);
|
|
50
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
54
|
+
* the trusted-local caller and the apiresource interceptor the kind, then
|
|
55
|
+
* Authorize and ValidateProto run and the load is the first step that reads
|
|
56
|
+
* the store.
|
|
57
|
+
*/
|
|
58
|
+
function workflowCommand(
|
|
59
|
+
store: Store,
|
|
60
|
+
): Client<typeof WorkflowCommandController> {
|
|
61
|
+
const transport = createRouterTransport(
|
|
62
|
+
(router) => {
|
|
63
|
+
registerWorkflowServices(router, {
|
|
64
|
+
store,
|
|
65
|
+
logger: silentLogger,
|
|
66
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
67
|
+
authorizationLifecycle: undefined,
|
|
68
|
+
validator: untouchable("validator"),
|
|
69
|
+
workflowInstanceCreator: untouchable("workflowInstanceCreator"),
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
router: {
|
|
74
|
+
interceptors: [
|
|
75
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
76
|
+
createApiResourceInterceptor(),
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
return createClient(WorkflowCommandController, transport);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("updateVisibility — LoadWorkflowForVisibilityUpdate", () => {
|
|
85
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
86
|
+
return errorOf(() =>
|
|
87
|
+
workflowCommand(store).updateVisibility({
|
|
88
|
+
resourceId: WORKFLOW_ID,
|
|
89
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
it("a missing workflow answers NotFound with the domain's copy", async () => {
|
|
95
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
96
|
+
|
|
97
|
+
expect(error.code).toBe(Code.NotFound);
|
|
98
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
102
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
103
|
+
|
|
104
|
+
expect(error.code).toBe(Code.Internal);
|
|
105
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -492,7 +492,7 @@ async function updateVisibility(
|
|
|
492
492
|
return reqCtx.get(UPDATE_VISIBILITY_WORKFLOW_KEY) as Workflow;
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
-
/** Loads the workflow by resource_id;
|
|
495
|
+
/** Loads the workflow by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
496
496
|
function newLoadWorkflowForVisibilityUpdateStep(
|
|
497
497
|
store: Store,
|
|
498
498
|
): PipelineStep<UpdateVisibilityDesc> {
|
|
@@ -507,8 +507,11 @@ function newLoadWorkflowForVisibilityUpdateStep(
|
|
|
507
507
|
input.resourceId,
|
|
508
508
|
WorkflowSchema,
|
|
509
509
|
);
|
|
510
|
-
} catch {
|
|
511
|
-
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (error instanceof ResourceNotFoundError) {
|
|
512
|
+
throw notFoundError("workflow", input.resourceId);
|
|
513
|
+
}
|
|
514
|
+
throw internalError(error, "failed to load workflow");
|
|
512
515
|
}
|
|
513
516
|
ctx.set(UPDATE_VISIBILITY_WORKFLOW_KEY, workflow);
|
|
514
517
|
},
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the workflow instance's loads. The shared
|
|
3
|
+
* targeted-update load (updateVisibility and updateExecutionVisibility): a
|
|
4
|
+
* typed ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
5
|
+
* (`workflow instance not found: <id>`), and any other store failure is an
|
|
6
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound that
|
|
7
|
+
* tells a client the instance does not exist (stigmer/stigmer#1345). Create's
|
|
8
|
+
* parent-workflow load reads the workflow through the in-process query
|
|
9
|
+
* client, so the store's typed miss arrives as a Connect NotFound: that
|
|
10
|
+
* answers the pinned `Workflow not found: <id>`, and any other failure an
|
|
11
|
+
* Internal, never a missing parent (stigmer/stigmer#1351).
|
|
12
|
+
*
|
|
13
|
+
* Every surface is reached through the registered handler on an in-process
|
|
14
|
+
* router. The composed suites reach only a real store, which cannot fail
|
|
15
|
+
* selectively, so the updates run against a store whose read throws and
|
|
16
|
+
* create against a parent loader whose read throws. Whatever the surface does
|
|
17
|
+
* not need is untouchable: a load that fails must stop the call before
|
|
18
|
+
* anything past it runs.
|
|
19
|
+
*
|
|
20
|
+
* Out of scope: the NotFound copies themselves (wire contract).
|
|
21
|
+
*/
|
|
22
|
+
import type { Client } from "@connectrpc/connect";
|
|
23
|
+
import {
|
|
24
|
+
Code,
|
|
25
|
+
ConnectError,
|
|
26
|
+
createClient,
|
|
27
|
+
createRouterTransport,
|
|
28
|
+
} from "@connectrpc/connect";
|
|
29
|
+
import { describe, expect, it } from "vitest";
|
|
30
|
+
|
|
31
|
+
import { WorkflowInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/command_pb";
|
|
32
|
+
import { WorkflowExecutionVisibility } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/spec_pb";
|
|
33
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
34
|
+
|
|
35
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
36
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
37
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
38
|
+
import {
|
|
39
|
+
errorOf,
|
|
40
|
+
failingStore,
|
|
41
|
+
untouchable,
|
|
42
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
43
|
+
import { INTERNAL_FALLBACK_MESSAGE } from "../../../pipeline/pipeline.js";
|
|
44
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
45
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
46
|
+
import type { Store } from "../../../store/interface.js";
|
|
47
|
+
|
|
48
|
+
import { registerWorkflowInstanceServices } from "../controller.js";
|
|
49
|
+
import type { ParentWorkflowLoaderProvider } from "../steps.js";
|
|
50
|
+
|
|
51
|
+
const silentLogger = createLogger({
|
|
52
|
+
level: "error",
|
|
53
|
+
pretty: false,
|
|
54
|
+
write: () => {},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const INSTANCE_ID = "win_storefault";
|
|
58
|
+
const WORKFLOW_ID = "wfl_storefault";
|
|
59
|
+
|
|
60
|
+
const NOT_FOUND_COPY = `workflow instance not found: ${INSTANCE_ID}`;
|
|
61
|
+
const LOAD_FAULT_COPY = "failed to load workflow instance";
|
|
62
|
+
const PARENT_NOT_FOUND_COPY = `Workflow not found: ${WORKFLOW_ID}`;
|
|
63
|
+
const PARENT_LOAD_FAULT_COPY = "failed to load parent workflow";
|
|
64
|
+
|
|
65
|
+
const MISSING = (): Error =>
|
|
66
|
+
new ResourceNotFoundError(`workflow_instance/${INSTANCE_ID}`);
|
|
67
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
68
|
+
|
|
69
|
+
/** What the workflow query's `get` answers for a missing and a faulted workflow. */
|
|
70
|
+
const PARENT_MISSING = (): Error =>
|
|
71
|
+
new ConnectError(`Workflow not found: ${WORKFLOW_ID}`, Code.NotFound);
|
|
72
|
+
const PARENT_FAULTED = (): Error =>
|
|
73
|
+
new ConnectError(INTERNAL_FALLBACK_MESSAGE, Code.Internal);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
77
|
+
* the trusted-local caller and the apiresource interceptor the kind, then
|
|
78
|
+
* Authorize and ValidateProto run and the load is the first step that reads
|
|
79
|
+
* the store or the parent.
|
|
80
|
+
*/
|
|
81
|
+
function instanceCommand(
|
|
82
|
+
store: Store,
|
|
83
|
+
parentWorkflowLoader: ParentWorkflowLoaderProvider = untouchable(
|
|
84
|
+
"parentWorkflowLoader",
|
|
85
|
+
),
|
|
86
|
+
): Client<typeof WorkflowInstanceCommandController> {
|
|
87
|
+
const transport = createRouterTransport(
|
|
88
|
+
(router) => {
|
|
89
|
+
registerWorkflowInstanceServices(router, {
|
|
90
|
+
store,
|
|
91
|
+
logger: silentLogger,
|
|
92
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
93
|
+
authorizationLifecycle: undefined,
|
|
94
|
+
parentWorkflowLoader,
|
|
95
|
+
listReadScope: undefined,
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
router: {
|
|
100
|
+
interceptors: [
|
|
101
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
102
|
+
createApiResourceInterceptor(),
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
return createClient(WorkflowInstanceCommandController, transport);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe.each([
|
|
111
|
+
{
|
|
112
|
+
surface: "updateVisibility — LoadInstanceForVisibilityUpdate",
|
|
113
|
+
call: (store: Store) =>
|
|
114
|
+
instanceCommand(store).updateVisibility({
|
|
115
|
+
resourceId: INSTANCE_ID,
|
|
116
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
surface:
|
|
121
|
+
"updateExecutionVisibility — LoadInstanceForExecutionVisibilityUpdate",
|
|
122
|
+
call: (store: Store) =>
|
|
123
|
+
instanceCommand(store).updateExecutionVisibility({
|
|
124
|
+
resourceId: INSTANCE_ID,
|
|
125
|
+
executionVisibility: WorkflowExecutionVisibility.private,
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
])("$surface", ({ call }) => {
|
|
129
|
+
it("a missing workflow instance answers NotFound with the domain's copy", async () => {
|
|
130
|
+
const error = await errorOf(() => call(failingStore(MISSING())));
|
|
131
|
+
|
|
132
|
+
expect(error.code).toBe(Code.NotFound);
|
|
133
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
137
|
+
const error = await errorOf(() => call(failingStore(LOCKED())));
|
|
138
|
+
|
|
139
|
+
expect(error.code).toBe(Code.Internal);
|
|
140
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("create — LoadParentWorkflow", () => {
|
|
145
|
+
function surfaceError(parentError: Error): Promise<ConnectError> {
|
|
146
|
+
const failingParent: ParentWorkflowLoaderProvider = () => ({
|
|
147
|
+
get: () => Promise.reject(parentError),
|
|
148
|
+
});
|
|
149
|
+
return errorOf(() =>
|
|
150
|
+
instanceCommand(untouchable("store"), failingParent).create({
|
|
151
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
152
|
+
kind: "WorkflowInstance",
|
|
153
|
+
metadata: { name: "Store Fault Instance", org: "org_storefault" },
|
|
154
|
+
spec: { workflowId: WORKFLOW_ID },
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
it("a missing parent workflow answers NotFound with the pinned copy", async () => {
|
|
160
|
+
const error = await surfaceError(PARENT_MISSING());
|
|
161
|
+
|
|
162
|
+
expect(error.code).toBe(Code.NotFound);
|
|
163
|
+
expect(error.rawMessage).toBe(PARENT_NOT_FOUND_COPY);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("any other parent-load failure answers a sanitized Internal", async () => {
|
|
167
|
+
const error = await surfaceError(PARENT_FAULTED());
|
|
168
|
+
|
|
169
|
+
expect(error.code).toBe(Code.Internal);
|
|
170
|
+
expect(error.rawMessage).toBe(PARENT_LOAD_FAULT_COPY);
|
|
171
|
+
});
|
|
172
|
+
});
|