@stigmer/server 3.38.2 → 3.39.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/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/agentexecution/controller.js +7 -3
- package/dist/domain/agentexecution/controller.js.map +1 -1
- package/dist/domain/agentexecution/update-status.d.ts +9 -2
- package/dist/domain/agentexecution/update-status.d.ts.map +1 -1
- package/dist/domain/agentexecution/update-status.js +28 -5
- package/dist/domain/agentexecution/update-status.js.map +1 -1
- package/dist/domain/agentexecution/validate-thinking-mode.d.ts +31 -21
- package/dist/domain/agentexecution/validate-thinking-mode.d.ts.map +1 -1
- package/dist/domain/agentexecution/validate-thinking-mode.js +61 -18
- package/dist/domain/agentexecution/validate-thinking-mode.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/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/artifact/controller.d.ts.map +1 -1
- package/dist/domain/artifact/controller.js +9 -6
- package/dist/domain/artifact/controller.js.map +1 -1
- package/dist/domain/environment/controller.d.ts +2 -2
- package/dist/domain/environment/controller.js +7 -3
- package/dist/domain/environment/controller.js.map +1 -1
- package/dist/domain/environment/steps.d.ts +2 -1
- package/dist/domain/environment/steps.d.ts.map +1 -1
- package/dist/domain/environment/steps.js +10 -7
- package/dist/domain/environment/steps.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/workflow/registry/data/model-registry.json +23 -14
- package/dist/domain/workflow/registry/model-registry-store.d.ts +14 -3
- package/dist/domain/workflow/registry/model-registry-store.d.ts.map +1 -1
- package/dist/domain/workflow/registry/model-registry-store.js +14 -3
- package/dist/domain/workflow/registry/model-registry-store.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/agentexecution/__tests__/run-gate.test.ts +42 -1
- package/src/domain/agentexecution/__tests__/update-status.test.ts +58 -2
- package/src/domain/agentexecution/__tests__/validate-thinking-mode.test.ts +209 -61
- package/src/domain/agentexecution/controller.ts +7 -3
- package/src/domain/agentexecution/update-status.ts +43 -3
- package/src/domain/agentexecution/validate-thinking-mode.ts +102 -47
- package/src/domain/agentinstance/__tests__/store-faults.test.ts +110 -0
- package/src/domain/agentinstance/controller.ts +9 -5
- package/src/domain/agentshare/__tests__/store-faults.test.ts +101 -0
- package/src/domain/agentshare/controller.ts +9 -5
- package/src/domain/artifact/__tests__/store-faults.test.ts +117 -0
- package/src/domain/artifact/controller.ts +12 -6
- package/src/domain/environment/__tests__/store-faults.test.ts +150 -0
- package/src/domain/environment/controller.ts +8 -5
- package/src/domain/environment/steps.ts +10 -7
- 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/workflow/registry/data/model-registry.json +23 -14
- package/src/domain/workflow/registry/model-registry-store.ts +16 -3
- package/src/extensions/gate-slots.ts +7 -5
- package/src/index.ts +11 -0
- package/src/pipeline/__tests__/blind-not-found.test.ts +0 -7
- 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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the agent instance's visibility-update
|
|
3
|
+
* load: a typed ResourceNotFoundError answers NotFound with the domain's
|
|
4
|
+
* pinned copy (`agent instance not found: <id>`), and any other store failure
|
|
5
|
+
* is an infrastructure fault answered as a sanitized Internal, never a
|
|
6
|
+
* NotFound that tells a client the instance does not exist
|
|
7
|
+
* (stigmer/stigmer#1345).
|
|
8
|
+
*
|
|
9
|
+
* The surface is updateVisibility, reached through the registered handler on
|
|
10
|
+
* an in-process router. The composed suites reach only a real store, which
|
|
11
|
+
* cannot fail selectively, so it runs here against a store whose read throws.
|
|
12
|
+
* The parent-agent loader is untouchable: a load that fails must stop the
|
|
13
|
+
* call before anything past it runs.
|
|
14
|
+
*
|
|
15
|
+
* Out of scope: the NotFound copy itself (wire contract) and create's
|
|
16
|
+
* parent-agent load, which folds every RPC error into NotFound
|
|
17
|
+
* (stigmer/stigmer#1351).
|
|
18
|
+
*/
|
|
19
|
+
import type { Client, ConnectError } from "@connectrpc/connect";
|
|
20
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
21
|
+
import { describe, expect, it } from "vitest";
|
|
22
|
+
|
|
23
|
+
import { AgentInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/command_pb";
|
|
24
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
25
|
+
|
|
26
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
27
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
28
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
29
|
+
import {
|
|
30
|
+
errorOf,
|
|
31
|
+
failingStore,
|
|
32
|
+
untouchable,
|
|
33
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
34
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
35
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
36
|
+
import type { Store } from "../../../store/interface.js";
|
|
37
|
+
|
|
38
|
+
import { registerAgentInstanceServices } from "../controller.js";
|
|
39
|
+
|
|
40
|
+
const silentLogger = createLogger({
|
|
41
|
+
level: "error",
|
|
42
|
+
pretty: false,
|
|
43
|
+
write: () => {},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const INSTANCE_ID = "ain_storefault";
|
|
47
|
+
|
|
48
|
+
const NOT_FOUND_COPY = `agent instance not found: ${INSTANCE_ID}`;
|
|
49
|
+
const LOAD_FAULT_COPY = "failed to load agent instance";
|
|
50
|
+
|
|
51
|
+
const MISSING = (): Error =>
|
|
52
|
+
new ResourceNotFoundError(`agent_instance/${INSTANCE_ID}`);
|
|
53
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
57
|
+
* the trusted-local caller and the apiresource interceptor the kind, then
|
|
58
|
+
* Authorize and ValidateProto run and the load is the first step that reads
|
|
59
|
+
* the store.
|
|
60
|
+
*/
|
|
61
|
+
function instanceCommand(
|
|
62
|
+
store: Store,
|
|
63
|
+
): Client<typeof AgentInstanceCommandController> {
|
|
64
|
+
const transport = createRouterTransport(
|
|
65
|
+
(router) => {
|
|
66
|
+
registerAgentInstanceServices(router, {
|
|
67
|
+
store,
|
|
68
|
+
logger: silentLogger,
|
|
69
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
70
|
+
authorizationLifecycle: undefined,
|
|
71
|
+
parentAgentLoader: untouchable("parentAgentLoader"),
|
|
72
|
+
listReadScope: undefined,
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
router: {
|
|
77
|
+
interceptors: [
|
|
78
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
79
|
+
createApiResourceInterceptor(),
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
return createClient(AgentInstanceCommandController, transport);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("updateVisibility — LoadInstanceForVisibilityUpdate", () => {
|
|
88
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
89
|
+
return errorOf(() =>
|
|
90
|
+
instanceCommand(store).updateVisibility({
|
|
91
|
+
resourceId: INSTANCE_ID,
|
|
92
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
it("a missing agent instance answers NotFound with the domain's copy", async () => {
|
|
98
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
99
|
+
|
|
100
|
+
expect(error.code).toBe(Code.NotFound);
|
|
101
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
105
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
106
|
+
|
|
107
|
+
expect(error.code).toBe(Code.Internal);
|
|
108
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* refuses default instances outright (stigmer/stigmer#556).
|
|
8
8
|
*
|
|
9
9
|
* Pipeline per RPC mirrors the Go step chains character-for-character.
|
|
10
|
-
* Proven by agentinstance.conformance.test.ts (CONFORMANCE_TARGET=local)
|
|
11
|
-
* and __tests__/
|
|
10
|
+
* Proven by agentinstance.conformance.test.ts (CONFORMANCE_TARGET=local),
|
|
11
|
+
* __tests__/agentinstance.test.ts and __tests__/store-faults.test.ts.
|
|
12
12
|
*
|
|
13
13
|
* Versus Stigmer Cloud, OSS excludes the CreateIamPolicies and Publish
|
|
14
14
|
* steps. Deliberately NO same-org rule on create, unlike WorkflowInstance:
|
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
newValidateVisibilityStep,
|
|
99
99
|
newValidateVisibilityUpdateStep,
|
|
100
100
|
} from "../../pipeline/steps/validate-visibility.js";
|
|
101
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
101
102
|
import type { Store } from "../../store/interface.js";
|
|
102
103
|
import { agentInstanceSearchExtractor } from "./search-extractor.js";
|
|
103
104
|
import {
|
|
@@ -398,7 +399,7 @@ async function updateVisibility(
|
|
|
398
399
|
return reqCtx.get(UPDATE_VISIBILITY_INSTANCE_KEY) as AgentInstance;
|
|
399
400
|
}
|
|
400
401
|
|
|
401
|
-
/** Loads the agent instance by resource_id;
|
|
402
|
+
/** Loads the agent instance by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
402
403
|
function newLoadInstanceForVisibilityUpdateStep(
|
|
403
404
|
store: Store,
|
|
404
405
|
): PipelineStep<UpdateVisibilityDesc> {
|
|
@@ -413,8 +414,11 @@ function newLoadInstanceForVisibilityUpdateStep(
|
|
|
413
414
|
input.resourceId,
|
|
414
415
|
AgentInstanceSchema,
|
|
415
416
|
);
|
|
416
|
-
} catch {
|
|
417
|
-
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (error instanceof ResourceNotFoundError) {
|
|
419
|
+
throw notFoundError("agent instance", input.resourceId);
|
|
420
|
+
}
|
|
421
|
+
throw internalError(error, "failed to load agent instance");
|
|
418
422
|
}
|
|
419
423
|
ctx.set(UPDATE_VISIBILITY_INSTANCE_KEY, instance);
|
|
420
424
|
},
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the share's link-rotation load: a typed
|
|
3
|
+
* ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
4
|
+
* (`AgentShare not found: <id>`), and any other store failure is an
|
|
5
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound
|
|
6
|
+
* that tells a client the share does not exist (stigmer/stigmer#1345).
|
|
7
|
+
* Rotation is how an owner kills a leaked link, so a fault that reads as a
|
|
8
|
+
* missing share would leave the link live behind a false answer.
|
|
9
|
+
*
|
|
10
|
+
* The surface is rotateShareLink, reached through the registered handler on
|
|
11
|
+
* an in-process router. The composed suites reach only a real store, which
|
|
12
|
+
* cannot fail selectively, so it runs here against a store whose read throws.
|
|
13
|
+
*
|
|
14
|
+
* Out of scope: the NotFound copy itself (wire contract, pinned by the
|
|
15
|
+
* agentshare conformance suite), the anonymous profile lanes' deliberate
|
|
16
|
+
* uniform NotFound, and getByAgent's empty-list answer on an agent-read
|
|
17
|
+
* fault (stigmer/stigmer#1375).
|
|
18
|
+
*/
|
|
19
|
+
import type { Client, ConnectError } from "@connectrpc/connect";
|
|
20
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
21
|
+
import { describe, expect, it } from "vitest";
|
|
22
|
+
|
|
23
|
+
import { AgentShareCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/command_pb";
|
|
24
|
+
|
|
25
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
26
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
27
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
28
|
+
import { errorOf, failingStore } from "../../../pipeline/__tests__/support.js";
|
|
29
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
30
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
31
|
+
import type { Store } from "../../../store/interface.js";
|
|
32
|
+
|
|
33
|
+
import { registerAgentShareServices } from "../controller.js";
|
|
34
|
+
|
|
35
|
+
const silentLogger = createLogger({
|
|
36
|
+
level: "error",
|
|
37
|
+
pretty: false,
|
|
38
|
+
write: () => {},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const SHARE_ID = "ash_storefault";
|
|
42
|
+
|
|
43
|
+
const NOT_FOUND_COPY = `AgentShare not found: ${SHARE_ID}`;
|
|
44
|
+
const LOAD_FAULT_COPY = "failed to load agent share";
|
|
45
|
+
|
|
46
|
+
const MISSING = (): Error =>
|
|
47
|
+
new ResourceNotFoundError(`agent_share/${SHARE_ID}`);
|
|
48
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
52
|
+
* the trusted-local caller and the apiresource interceptor the kind the load
|
|
53
|
+
* reads, then Authorize and ValidateProto run and the load is the first step
|
|
54
|
+
* that reads the store.
|
|
55
|
+
*/
|
|
56
|
+
function shareCommand(
|
|
57
|
+
store: Store,
|
|
58
|
+
): Client<typeof AgentShareCommandController> {
|
|
59
|
+
const transport = createRouterTransport(
|
|
60
|
+
(router) => {
|
|
61
|
+
registerAgentShareServices(router, {
|
|
62
|
+
store,
|
|
63
|
+
logger: silentLogger,
|
|
64
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
65
|
+
authorizationLifecycle: undefined,
|
|
66
|
+
listReadScope: undefined,
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
router: {
|
|
71
|
+
interceptors: [
|
|
72
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
73
|
+
createApiResourceInterceptor(),
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
return createClient(AgentShareCommandController, transport);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("rotateShareLink — LoadShareForLinkRotation", () => {
|
|
82
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
83
|
+
return errorOf(() =>
|
|
84
|
+
shareCommand(store).rotateShareLink({ resourceId: SHARE_ID }),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
it("a missing share answers NotFound with the domain's copy", async () => {
|
|
89
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
90
|
+
|
|
91
|
+
expect(error.code).toBe(Code.NotFound);
|
|
92
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
96
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
97
|
+
|
|
98
|
+
expect(error.code).toBe(Code.Internal);
|
|
99
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* cloud edition enforces the same contracts via FGA plus app-level gates
|
|
19
19
|
* for the anonymous resolution paths.
|
|
20
20
|
*
|
|
21
|
-
* Proven by agentshare.conformance.test.ts (CONFORMANCE_TARGET=local)
|
|
22
|
-
* and __tests__/
|
|
21
|
+
* Proven by agentshare.conformance.test.ts (CONFORMANCE_TARGET=local),
|
|
22
|
+
* __tests__/agentshare.test.ts and __tests__/store-faults.test.ts.
|
|
23
23
|
*/
|
|
24
24
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
25
25
|
import { create, fromBinary } from "@bufbuild/protobuf";
|
|
@@ -105,6 +105,7 @@ import { newPersistStep } from "../../pipeline/steps/persist.js";
|
|
|
105
105
|
import { newResolveSlugStep } from "../../pipeline/steps/slug.js";
|
|
106
106
|
import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
|
|
107
107
|
import { newValidateVisibilityStep } from "../../pipeline/steps/validate-visibility.js";
|
|
108
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
108
109
|
import type { Store } from "../../store/interface.js";
|
|
109
110
|
import { ORG_REQUIRED_FOR_LOOKUP_MESSAGE } from "./constants.js";
|
|
110
111
|
import {
|
|
@@ -335,7 +336,7 @@ async function rotateShareLink(
|
|
|
335
336
|
return reqCtx.get(ROTATE_SHARE_KEY) as AgentShare;
|
|
336
337
|
}
|
|
337
338
|
|
|
338
|
-
/** Loads the share by resource_id;
|
|
339
|
+
/** Loads the share by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
339
340
|
function newLoadShareForLinkRotationStep(
|
|
340
341
|
store: Store,
|
|
341
342
|
): PipelineStep<RotateDesc> {
|
|
@@ -349,8 +350,11 @@ function newLoadShareForLinkRotationStep(
|
|
|
349
350
|
ctx.input.resourceId,
|
|
350
351
|
AgentShareSchema,
|
|
351
352
|
);
|
|
352
|
-
} catch {
|
|
353
|
-
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (error instanceof ResourceNotFoundError) {
|
|
355
|
+
throw notFoundError("AgentShare", ctx.input.resourceId);
|
|
356
|
+
}
|
|
357
|
+
throw internalError(error, "failed to load agent share");
|
|
354
358
|
}
|
|
355
359
|
ctx.set(ROTATE_SHARE_KEY, share);
|
|
356
360
|
},
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract the artifact load by id shares: a typed
|
|
3
|
+
* ResourceNotFoundError answers NotFound with the domain's pinned Go copy
|
|
4
|
+
* (`Artifact not found: <id>`), and any other store failure is an
|
|
5
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound
|
|
6
|
+
* that tells a client a run's output does not exist (stigmer/stigmer#1345).
|
|
7
|
+
*
|
|
8
|
+
* The surfaces: the three direct handlers that load through
|
|
9
|
+
* loadArtifactOrNotFound, delete, getDownloadUrl and getContent, each
|
|
10
|
+
* reached through the registered handler on an in-process router. All three
|
|
11
|
+
* load before they authorize (stigmer#224), and a fault answered as Internal
|
|
12
|
+
* says nothing about whether the artifact exists. The composed suites reach
|
|
13
|
+
* only a real store, which cannot fail selectively, so each surface runs here
|
|
14
|
+
* against a store whose read throws. Artifact storage is untouchable: a load
|
|
15
|
+
* that fails must stop the call before any blob read or URL mint.
|
|
16
|
+
*
|
|
17
|
+
* Out of scope: the NotFound copy itself (wire contract, pinned by the
|
|
18
|
+
* artifact conformance suite) and create's best-effort org derivation
|
|
19
|
+
* (stigmer/stigmer#1369).
|
|
20
|
+
*/
|
|
21
|
+
import type { Client, ConnectError } from "@connectrpc/connect";
|
|
22
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
23
|
+
import { describe, expect, it } from "vitest";
|
|
24
|
+
|
|
25
|
+
import { ArtifactCommandController } from "@stigmer/protos/ai/stigmer/agentic/artifact/v1/command_pb";
|
|
26
|
+
import { ArtifactQueryController } from "@stigmer/protos/ai/stigmer/agentic/artifact/v1/query_pb";
|
|
27
|
+
|
|
28
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
29
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
30
|
+
import {
|
|
31
|
+
errorOf,
|
|
32
|
+
failingStore,
|
|
33
|
+
untouchable,
|
|
34
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
35
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
36
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
37
|
+
import type { Store } from "../../../store/interface.js";
|
|
38
|
+
|
|
39
|
+
import { registerArtifactServices } from "../controller.js";
|
|
40
|
+
|
|
41
|
+
const silentLogger = createLogger({
|
|
42
|
+
level: "error",
|
|
43
|
+
pretty: false,
|
|
44
|
+
write: () => {},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const ARTIFACT_ID = "art_storefault";
|
|
48
|
+
|
|
49
|
+
const NOT_FOUND_COPY = `Artifact not found: ${ARTIFACT_ID}`;
|
|
50
|
+
const LOAD_FAULT_COPY = "failed to load artifact";
|
|
51
|
+
|
|
52
|
+
const MISSING = (): Error =>
|
|
53
|
+
new ResourceNotFoundError(`artifact/${ARTIFACT_ID}`);
|
|
54
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
55
|
+
|
|
56
|
+
interface ArtifactClients {
|
|
57
|
+
readonly command: Client<typeof ArtifactCommandController>;
|
|
58
|
+
readonly query: Client<typeof ArtifactQueryController>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The registered handlers on an in-process router (`artifact.test.ts`'s
|
|
63
|
+
* harness): the verifier chain stamps the trusted-local caller, each handler
|
|
64
|
+
* checks its id, and the load is the first read of the store.
|
|
65
|
+
*/
|
|
66
|
+
function artifactClients(store: Store): ArtifactClients {
|
|
67
|
+
const transport = createRouterTransport(
|
|
68
|
+
(router) => {
|
|
69
|
+
registerArtifactServices(router, {
|
|
70
|
+
store,
|
|
71
|
+
artifactStorage: untouchable("artifactStorage"),
|
|
72
|
+
logger: silentLogger,
|
|
73
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
router: {
|
|
78
|
+
interceptors: [createVerifierChainInterceptor([], [], silentLogger)],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
return {
|
|
83
|
+
command: createClient(ArtifactCommandController, transport),
|
|
84
|
+
query: createClient(ArtifactQueryController, transport),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const SURFACES: ReadonlyArray<
|
|
89
|
+
readonly [string, (clients: ArtifactClients) => Promise<unknown>]
|
|
90
|
+
> = [
|
|
91
|
+
["delete", ({ command }) => command.delete({ value: ARTIFACT_ID })],
|
|
92
|
+
[
|
|
93
|
+
"getDownloadUrl",
|
|
94
|
+
({ query }) => query.getDownloadUrl({ value: ARTIFACT_ID }),
|
|
95
|
+
],
|
|
96
|
+
["getContent", ({ query }) => query.getContent({ artifactId: ARTIFACT_ID })],
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
describe.each(SURFACES)("%s — loadArtifactOrNotFound", (_name, call) => {
|
|
100
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
101
|
+
return errorOf(() => call(artifactClients(store)));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
it("a missing artifact answers NotFound with the domain's copy", async () => {
|
|
105
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
106
|
+
|
|
107
|
+
expect(error.code).toBe(Code.NotFound);
|
|
108
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
112
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
113
|
+
|
|
114
|
+
expect(error.code).toBe(Code.Internal);
|
|
115
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* storage_state transition rather than the hard-delete pipeline; only
|
|
10
10
|
* get/listByExecution ride pipelines. Proven by
|
|
11
11
|
* artifact.conformance.test.ts (CONFORMANCE_TARGET=local, incl. the
|
|
12
|
-
* file-server lane)
|
|
12
|
+
* file-server lane), __tests__/artifact.test.ts and
|
|
13
|
+
* __tests__/store-faults.test.ts.
|
|
13
14
|
*
|
|
14
15
|
* Go tolerates a nil ArtifactStorage (two-phase wiring) and answers
|
|
15
16
|
* Internal "artifact storage not configured"; the TS composition root
|
|
@@ -79,6 +80,7 @@ import {
|
|
|
79
80
|
newLoadTargetStep,
|
|
80
81
|
} from "../../pipeline/steps/load-target.js";
|
|
81
82
|
import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
|
|
83
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
82
84
|
import type { Store } from "../../store/interface.js";
|
|
83
85
|
import type { ListIndexRow } from "../../store/list-index.js";
|
|
84
86
|
import { artifactListIndex } from "./list-index.js";
|
|
@@ -649,7 +651,7 @@ async function getContent(
|
|
|
649
651
|
});
|
|
650
652
|
}
|
|
651
653
|
|
|
652
|
-
/** Load by id
|
|
654
|
+
/** Load by id: a missing artifact answers the byte-pinned Go NotFound copy, a store fault Internal. */
|
|
653
655
|
async function loadArtifactOrNotFound(
|
|
654
656
|
deps: ArtifactControllerDeps,
|
|
655
657
|
artifactId: string,
|
|
@@ -660,10 +662,14 @@ async function loadArtifactOrNotFound(
|
|
|
660
662
|
artifactId,
|
|
661
663
|
ArtifactSchema,
|
|
662
664
|
);
|
|
663
|
-
} catch {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
665
|
+
} catch (error) {
|
|
666
|
+
if (error instanceof ResourceNotFoundError) {
|
|
667
|
+
throw new ConnectError(
|
|
668
|
+
artifactNotFoundMessage(artifactId),
|
|
669
|
+
Code.NotFound,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
throw internalError(error, "failed to load artifact");
|
|
667
673
|
}
|
|
668
674
|
}
|
|
669
675
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract every environment load by id shares: a typed
|
|
3
|
+
* ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
4
|
+
* (`environment not found: <id>`), and any other store failure is an
|
|
5
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound
|
|
6
|
+
* that tells a client the environment does not exist
|
|
7
|
+
* (stigmer/stigmer#1345).
|
|
8
|
+
*
|
|
9
|
+
* The surfaces: updateVisibility (its own load step) and the three RPCs that
|
|
10
|
+
* share LoadEnvironmentByID, updateVariables, removeVariables and
|
|
11
|
+
* getSecretValue, each reached through the registered handler on an
|
|
12
|
+
* in-process router. The composed suites reach only a real store, which
|
|
13
|
+
* cannot fail selectively, so each surface runs here against a store whose
|
|
14
|
+
* read throws. The secret service is untouchable: a load that fails must stop
|
|
15
|
+
* the call before any decrypt or encrypt.
|
|
16
|
+
*
|
|
17
|
+
* Out of scope: the NotFound copy itself (wire contract, pinned by
|
|
18
|
+
* environment.test.ts and the environment conformance suite) and the
|
|
19
|
+
* personal-environment resolver's fold of a failed secret read into a
|
|
20
|
+
* missing key (stigmer/stigmer#1360).
|
|
21
|
+
*/
|
|
22
|
+
import type { Client, ConnectError } from "@connectrpc/connect";
|
|
23
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
24
|
+
import { describe, expect, it } from "vitest";
|
|
25
|
+
|
|
26
|
+
import { EnvironmentCommandController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/command_pb";
|
|
27
|
+
import { EnvironmentQueryController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/query_pb";
|
|
28
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
29
|
+
|
|
30
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
31
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
32
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
33
|
+
import {
|
|
34
|
+
errorOf,
|
|
35
|
+
failingStore,
|
|
36
|
+
untouchable,
|
|
37
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
38
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
39
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
40
|
+
import type { Store } from "../../../store/interface.js";
|
|
41
|
+
|
|
42
|
+
import { registerEnvironmentServices } from "../controller.js";
|
|
43
|
+
|
|
44
|
+
const silentLogger = createLogger({
|
|
45
|
+
level: "error",
|
|
46
|
+
pretty: false,
|
|
47
|
+
write: () => {},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const ENVIRONMENT_ID = "env_storefault";
|
|
51
|
+
|
|
52
|
+
const NOT_FOUND_COPY = `environment not found: ${ENVIRONMENT_ID}`;
|
|
53
|
+
const LOAD_FAULT_COPY = "failed to load environment";
|
|
54
|
+
|
|
55
|
+
const MISSING = (): Error =>
|
|
56
|
+
new ResourceNotFoundError(`environment/${ENVIRONMENT_ID}`);
|
|
57
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
58
|
+
|
|
59
|
+
interface EnvironmentClients {
|
|
60
|
+
readonly command: Client<typeof EnvironmentCommandController>;
|
|
61
|
+
readonly query: Client<typeof EnvironmentQueryController>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
66
|
+
* the trusted-local caller and the apiresource interceptor the kind both
|
|
67
|
+
* loads read, then Authorize and ValidateProto run and the load is the first
|
|
68
|
+
* step that reads the store.
|
|
69
|
+
*/
|
|
70
|
+
function environmentClients(store: Store): EnvironmentClients {
|
|
71
|
+
const transport = createRouterTransport(
|
|
72
|
+
(router) => {
|
|
73
|
+
registerEnvironmentServices(router, {
|
|
74
|
+
store,
|
|
75
|
+
logger: silentLogger,
|
|
76
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
77
|
+
authorizationLifecycle: undefined,
|
|
78
|
+
secretService: untouchable("secretService"),
|
|
79
|
+
listReadScope: undefined,
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
router: {
|
|
84
|
+
interceptors: [
|
|
85
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
86
|
+
createApiResourceInterceptor(),
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
return {
|
|
92
|
+
command: createClient(EnvironmentCommandController, transport),
|
|
93
|
+
query: createClient(EnvironmentQueryController, transport),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Each surface with an input valid enough to pass ValidateProto. */
|
|
98
|
+
const SURFACES: ReadonlyArray<
|
|
99
|
+
readonly [string, (clients: EnvironmentClients) => Promise<unknown>]
|
|
100
|
+
> = [
|
|
101
|
+
[
|
|
102
|
+
"updateVisibility — LoadEnvironmentForVisibilityUpdate",
|
|
103
|
+
({ command }) =>
|
|
104
|
+
command.updateVisibility({
|
|
105
|
+
resourceId: ENVIRONMENT_ID,
|
|
106
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
107
|
+
}),
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
"updateVariables — LoadEnvironmentByID",
|
|
111
|
+
({ command }) =>
|
|
112
|
+
command.updateVariables({
|
|
113
|
+
environmentId: ENVIRONMENT_ID,
|
|
114
|
+
variables: { API_KEY: { value: "v", isSecret: false } },
|
|
115
|
+
}),
|
|
116
|
+
],
|
|
117
|
+
[
|
|
118
|
+
"removeVariables — LoadEnvironmentByID",
|
|
119
|
+
({ command }) =>
|
|
120
|
+
command.removeVariables({
|
|
121
|
+
environmentId: ENVIRONMENT_ID,
|
|
122
|
+
keys: ["API_KEY"],
|
|
123
|
+
}),
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
"getSecretValue — LoadEnvironmentByID",
|
|
127
|
+
({ query }) =>
|
|
128
|
+
query.getSecretValue({ environmentId: ENVIRONMENT_ID, key: "API_KEY" }),
|
|
129
|
+
],
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
describe.each(SURFACES)("%s", (_name, call) => {
|
|
133
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
134
|
+
return errorOf(() => call(environmentClients(store)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
it("a missing environment answers NotFound with the domain's copy", async () => {
|
|
138
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
139
|
+
|
|
140
|
+
expect(error.code).toBe(Code.NotFound);
|
|
141
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
145
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
146
|
+
|
|
147
|
+
expect(error.code).toBe(Code.Internal);
|
|
148
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* design: limited blast radius, per-key audit, the industry "reveal" UX).
|
|
9
9
|
*
|
|
10
10
|
* Pipeline per RPC mirrors the Go step chains character-for-character.
|
|
11
|
-
* Proven by environment.conformance.test.ts (CONFORMANCE_TARGET=local)
|
|
12
|
-
* and __tests__/
|
|
11
|
+
* Proven by environment.conformance.test.ts (CONFORMANCE_TARGET=local),
|
|
12
|
+
* __tests__/environment.test.ts and __tests__/store-faults.test.ts.
|
|
13
13
|
*
|
|
14
14
|
* Versus Stigmer Cloud, OSS excludes the Authorize, CreateIamPolicies,
|
|
15
15
|
* FGA-tuple, and Publish steps (no multi-tenant auth, IAM/FGA, or event
|
|
@@ -421,7 +421,7 @@ async function updateVisibility(
|
|
|
421
421
|
return env;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
/** Loads the environment by resource_id;
|
|
424
|
+
/** Loads the environment by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
425
425
|
function newLoadEnvironmentForVisibilityUpdateStep(
|
|
426
426
|
store: Store,
|
|
427
427
|
): PipelineStep<UpdateVisibilityDesc> {
|
|
@@ -436,8 +436,11 @@ function newLoadEnvironmentForVisibilityUpdateStep(
|
|
|
436
436
|
input.resourceId,
|
|
437
437
|
EnvironmentSchema,
|
|
438
438
|
);
|
|
439
|
-
} catch {
|
|
440
|
-
|
|
439
|
+
} catch (error) {
|
|
440
|
+
if (error instanceof ResourceNotFoundError) {
|
|
441
|
+
throw notFoundError("environment", input.resourceId);
|
|
442
|
+
}
|
|
443
|
+
throw internalError(error, "failed to load environment");
|
|
441
444
|
}
|
|
442
445
|
ctx.set(UPDATE_VISIBILITY_ENVIRONMENT_KEY, env);
|
|
443
446
|
},
|