@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.
Files changed (128) hide show
  1. package/dist/domain/agent/controller.d.ts +2 -2
  2. package/dist/domain/agent/controller.d.ts.map +1 -1
  3. package/dist/domain/agent/controller.js +7 -3
  4. package/dist/domain/agent/controller.js.map +1 -1
  5. package/dist/domain/agentchannel/controller.d.ts +2 -2
  6. package/dist/domain/agentchannel/controller.d.ts.map +1 -1
  7. package/dist/domain/agentchannel/controller.js +10 -5
  8. package/dist/domain/agentchannel/controller.js.map +1 -1
  9. package/dist/domain/agentexecution/controller.js +7 -3
  10. package/dist/domain/agentexecution/controller.js.map +1 -1
  11. package/dist/domain/agentexecution/update-status.d.ts +9 -2
  12. package/dist/domain/agentexecution/update-status.d.ts.map +1 -1
  13. package/dist/domain/agentexecution/update-status.js +28 -5
  14. package/dist/domain/agentexecution/update-status.js.map +1 -1
  15. package/dist/domain/agentexecution/validate-thinking-mode.d.ts +31 -21
  16. package/dist/domain/agentexecution/validate-thinking-mode.d.ts.map +1 -1
  17. package/dist/domain/agentexecution/validate-thinking-mode.js +61 -18
  18. package/dist/domain/agentexecution/validate-thinking-mode.js.map +1 -1
  19. package/dist/domain/agentinstance/controller.d.ts +2 -2
  20. package/dist/domain/agentinstance/controller.d.ts.map +1 -1
  21. package/dist/domain/agentinstance/controller.js +7 -3
  22. package/dist/domain/agentinstance/controller.js.map +1 -1
  23. package/dist/domain/agentshare/controller.d.ts +2 -2
  24. package/dist/domain/agentshare/controller.d.ts.map +1 -1
  25. package/dist/domain/agentshare/controller.js +7 -3
  26. package/dist/domain/agentshare/controller.js.map +1 -1
  27. package/dist/domain/artifact/controller.d.ts.map +1 -1
  28. package/dist/domain/artifact/controller.js +9 -6
  29. package/dist/domain/artifact/controller.js.map +1 -1
  30. package/dist/domain/environment/controller.d.ts +2 -2
  31. package/dist/domain/environment/controller.js +7 -3
  32. package/dist/domain/environment/controller.js.map +1 -1
  33. package/dist/domain/environment/steps.d.ts +2 -1
  34. package/dist/domain/environment/steps.d.ts.map +1 -1
  35. package/dist/domain/environment/steps.js +10 -7
  36. package/dist/domain/environment/steps.js.map +1 -1
  37. package/dist/domain/organization/controller.d.ts.map +1 -1
  38. package/dist/domain/organization/controller.js +33 -14
  39. package/dist/domain/organization/controller.js.map +1 -1
  40. package/dist/domain/organization/slug-ledger.d.ts +86 -0
  41. package/dist/domain/organization/slug-ledger.d.ts.map +1 -0
  42. package/dist/domain/organization/slug-ledger.js +128 -0
  43. package/dist/domain/organization/slug-ledger.js.map +1 -0
  44. package/dist/domain/organization/steps.d.ts +12 -3
  45. package/dist/domain/organization/steps.d.ts.map +1 -1
  46. package/dist/domain/organization/steps.js +28 -6
  47. package/dist/domain/organization/steps.js.map +1 -1
  48. package/dist/domain/workflow/registry/data/model-registry.json +23 -14
  49. package/dist/domain/workflow/registry/model-registry-store.d.ts +14 -3
  50. package/dist/domain/workflow/registry/model-registry-store.d.ts.map +1 -1
  51. package/dist/domain/workflow/registry/model-registry-store.js +14 -3
  52. package/dist/domain/workflow/registry/model-registry-store.js.map +1 -1
  53. package/dist/extensions/gate-slots.d.ts +7 -5
  54. package/dist/extensions/gate-slots.d.ts.map +1 -1
  55. package/dist/extensions/gate-slots.js.map +1 -1
  56. package/dist/index.d.ts +3 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +2 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/pipeline/errors.d.ts +8 -0
  61. package/dist/pipeline/errors.d.ts.map +1 -1
  62. package/dist/pipeline/errors.js +19 -0
  63. package/dist/pipeline/errors.js.map +1 -1
  64. package/dist/store/interface.d.ts +67 -0
  65. package/dist/store/interface.d.ts.map +1 -1
  66. package/dist/store/interface.js.map +1 -1
  67. package/dist/store/organization-slug-history.d.ts +71 -0
  68. package/dist/store/organization-slug-history.d.ts.map +1 -0
  69. package/dist/store/organization-slug-history.js +88 -0
  70. package/dist/store/organization-slug-history.js.map +1 -0
  71. package/dist/store/postgres/migrations.d.ts +3 -1
  72. package/dist/store/postgres/migrations.d.ts.map +1 -1
  73. package/dist/store/postgres/migrations.js +69 -1
  74. package/dist/store/postgres/migrations.js.map +1 -1
  75. package/dist/store/postgres/store.d.ts +2 -1
  76. package/dist/store/postgres/store.d.ts.map +1 -1
  77. package/dist/store/postgres/store.js +49 -0
  78. package/dist/store/postgres/store.js.map +1 -1
  79. package/dist/store/sqlite/migrations.d.ts +3 -1
  80. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  81. package/dist/store/sqlite/migrations.js +54 -1
  82. package/dist/store/sqlite/migrations.js.map +1 -1
  83. package/dist/store/sqlite/store.d.ts +2 -1
  84. package/dist/store/sqlite/store.d.ts.map +1 -1
  85. package/dist/store/sqlite/store.js +55 -0
  86. package/dist/store/sqlite/store.js.map +1 -1
  87. package/package.json +6 -6
  88. package/src/domain/agent/__tests__/store-faults.test.ts +105 -0
  89. package/src/domain/agent/controller.ts +9 -5
  90. package/src/domain/agentchannel/__tests__/store-faults.test.ts +126 -0
  91. package/src/domain/agentchannel/controller.ts +12 -7
  92. package/src/domain/agentexecution/__tests__/run-gate.test.ts +42 -1
  93. package/src/domain/agentexecution/__tests__/update-status.test.ts +58 -2
  94. package/src/domain/agentexecution/__tests__/validate-thinking-mode.test.ts +209 -61
  95. package/src/domain/agentexecution/controller.ts +7 -3
  96. package/src/domain/agentexecution/update-status.ts +43 -3
  97. package/src/domain/agentexecution/validate-thinking-mode.ts +102 -47
  98. package/src/domain/agentinstance/__tests__/store-faults.test.ts +110 -0
  99. package/src/domain/agentinstance/controller.ts +9 -5
  100. package/src/domain/agentshare/__tests__/store-faults.test.ts +101 -0
  101. package/src/domain/agentshare/controller.ts +9 -5
  102. package/src/domain/artifact/__tests__/store-faults.test.ts +117 -0
  103. package/src/domain/artifact/controller.ts +12 -6
  104. package/src/domain/environment/__tests__/store-faults.test.ts +150 -0
  105. package/src/domain/environment/controller.ts +8 -5
  106. package/src/domain/environment/steps.ts +10 -7
  107. package/src/domain/organization/__tests__/organization-delete.test.ts +73 -18
  108. package/src/domain/organization/__tests__/organization-slugs.test.ts +386 -0
  109. package/src/domain/organization/controller.ts +36 -14
  110. package/src/domain/organization/slug-ledger.ts +209 -0
  111. package/src/domain/organization/steps.ts +28 -6
  112. package/src/domain/workflow/registry/data/model-registry.json +23 -14
  113. package/src/domain/workflow/registry/model-registry-store.ts +16 -3
  114. package/src/extensions/gate-slots.ts +7 -5
  115. package/src/index.ts +11 -0
  116. package/src/pipeline/__tests__/blind-not-found.test.ts +0 -7
  117. package/src/pipeline/errors.ts +23 -0
  118. package/src/store/__tests__/organization-slug-history.test.ts +109 -0
  119. package/src/store/__tests__/store-contract.ts +86 -1
  120. package/src/store/interface.ts +70 -0
  121. package/src/store/organization-slug-history.ts +159 -0
  122. package/src/store/postgres/__tests__/migrations.test.ts +138 -2
  123. package/src/store/postgres/__tests__/store-contract.test.ts +1 -0
  124. package/src/store/postgres/migrations.ts +84 -1
  125. package/src/store/postgres/store.ts +84 -0
  126. package/src/store/sqlite/__tests__/migrations.test.ts +139 -10
  127. package/src/store/sqlite/migrations.ts +69 -1
  128. 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__/agentinstance.test.ts.
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; ANY load failure → NotFound. */
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
- throw notFoundError("agent instance", input.resourceId);
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__/agentshare.test.ts.
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; ANY load failure → NotFound (Go). */
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
- throw notFoundError("AgentShare", ctx.input.resourceId);
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) and __tests__/artifact.test.ts.
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 with the byte-pinned Go NotFound copy. */
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
- // Go answers NotFound for ANY store failure here (status.Errorf on
665
- // err != nil), not only missing rows — mirrored.
666
- throw new ConnectError(artifactNotFoundMessage(artifactId), Code.NotFound);
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__/environment.test.ts.
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; ANY load failure → NotFound. */
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
- throw notFoundError("environment", input.resourceId);
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
  },