@stigmer/server 3.38.3 → 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 (90) 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/agentinstance/controller.d.ts +2 -2
  10. package/dist/domain/agentinstance/controller.d.ts.map +1 -1
  11. package/dist/domain/agentinstance/controller.js +7 -3
  12. package/dist/domain/agentinstance/controller.js.map +1 -1
  13. package/dist/domain/agentshare/controller.d.ts +2 -2
  14. package/dist/domain/agentshare/controller.d.ts.map +1 -1
  15. package/dist/domain/agentshare/controller.js +7 -3
  16. package/dist/domain/agentshare/controller.js.map +1 -1
  17. package/dist/domain/organization/controller.d.ts.map +1 -1
  18. package/dist/domain/organization/controller.js +33 -14
  19. package/dist/domain/organization/controller.js.map +1 -1
  20. package/dist/domain/organization/slug-ledger.d.ts +86 -0
  21. package/dist/domain/organization/slug-ledger.d.ts.map +1 -0
  22. package/dist/domain/organization/slug-ledger.js +128 -0
  23. package/dist/domain/organization/slug-ledger.js.map +1 -0
  24. package/dist/domain/organization/steps.d.ts +12 -3
  25. package/dist/domain/organization/steps.d.ts.map +1 -1
  26. package/dist/domain/organization/steps.js +28 -6
  27. package/dist/domain/organization/steps.js.map +1 -1
  28. package/dist/extensions/gate-slots.d.ts +7 -5
  29. package/dist/extensions/gate-slots.d.ts.map +1 -1
  30. package/dist/extensions/gate-slots.js.map +1 -1
  31. package/dist/index.d.ts +3 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/pipeline/errors.d.ts +8 -0
  36. package/dist/pipeline/errors.d.ts.map +1 -1
  37. package/dist/pipeline/errors.js +19 -0
  38. package/dist/pipeline/errors.js.map +1 -1
  39. package/dist/store/interface.d.ts +67 -0
  40. package/dist/store/interface.d.ts.map +1 -1
  41. package/dist/store/interface.js.map +1 -1
  42. package/dist/store/organization-slug-history.d.ts +71 -0
  43. package/dist/store/organization-slug-history.d.ts.map +1 -0
  44. package/dist/store/organization-slug-history.js +88 -0
  45. package/dist/store/organization-slug-history.js.map +1 -0
  46. package/dist/store/postgres/migrations.d.ts +3 -1
  47. package/dist/store/postgres/migrations.d.ts.map +1 -1
  48. package/dist/store/postgres/migrations.js +69 -1
  49. package/dist/store/postgres/migrations.js.map +1 -1
  50. package/dist/store/postgres/store.d.ts +2 -1
  51. package/dist/store/postgres/store.d.ts.map +1 -1
  52. package/dist/store/postgres/store.js +49 -0
  53. package/dist/store/postgres/store.js.map +1 -1
  54. package/dist/store/sqlite/migrations.d.ts +3 -1
  55. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  56. package/dist/store/sqlite/migrations.js +54 -1
  57. package/dist/store/sqlite/migrations.js.map +1 -1
  58. package/dist/store/sqlite/store.d.ts +2 -1
  59. package/dist/store/sqlite/store.d.ts.map +1 -1
  60. package/dist/store/sqlite/store.js +55 -0
  61. package/dist/store/sqlite/store.js.map +1 -1
  62. package/package.json +6 -6
  63. package/src/domain/agent/__tests__/store-faults.test.ts +105 -0
  64. package/src/domain/agent/controller.ts +9 -5
  65. package/src/domain/agentchannel/__tests__/store-faults.test.ts +126 -0
  66. package/src/domain/agentchannel/controller.ts +12 -7
  67. package/src/domain/agentinstance/__tests__/store-faults.test.ts +110 -0
  68. package/src/domain/agentinstance/controller.ts +9 -5
  69. package/src/domain/agentshare/__tests__/store-faults.test.ts +101 -0
  70. package/src/domain/agentshare/controller.ts +9 -5
  71. package/src/domain/organization/__tests__/organization-delete.test.ts +73 -18
  72. package/src/domain/organization/__tests__/organization-slugs.test.ts +386 -0
  73. package/src/domain/organization/controller.ts +36 -14
  74. package/src/domain/organization/slug-ledger.ts +209 -0
  75. package/src/domain/organization/steps.ts +28 -6
  76. package/src/extensions/gate-slots.ts +7 -5
  77. package/src/index.ts +11 -0
  78. package/src/pipeline/__tests__/blind-not-found.test.ts +0 -4
  79. package/src/pipeline/errors.ts +23 -0
  80. package/src/store/__tests__/organization-slug-history.test.ts +109 -0
  81. package/src/store/__tests__/store-contract.ts +86 -1
  82. package/src/store/interface.ts +70 -0
  83. package/src/store/organization-slug-history.ts +159 -0
  84. package/src/store/postgres/__tests__/migrations.test.ts +138 -2
  85. package/src/store/postgres/__tests__/store-contract.test.ts +1 -0
  86. package/src/store/postgres/migrations.ts +84 -1
  87. package/src/store/postgres/store.ts +84 -0
  88. package/src/store/sqlite/__tests__/migrations.test.ts +139 -10
  89. package/src/store/sqlite/migrations.ts +69 -1
  90. package/src/store/sqlite/store.ts +84 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/server",
3
- "version": "3.38.3",
3
+ "version": "3.39.0",
4
4
  "description": "The Stigmer OSS control plane as a composable library: composeServer, the extension registry and the driver seams a composition builds on. The self-contained deployable of the same server is @stigmer/server-slim.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -63,11 +63,11 @@
63
63
  "@connectrpc/connect-node": "^2.0.0",
64
64
  "@kubernetes/client-node": "2.0.0",
65
65
  "@opentelemetry/api": "^1.9.0",
66
- "@stigmer/outbound": "3.38.3",
67
- "@stigmer/plugin-package": "3.38.3",
68
- "@stigmer/protos": "3.38.3",
69
- "@stigmer/temporal-codecs": "3.38.3",
70
- "@stigmer/zip-structure": "3.38.3",
66
+ "@stigmer/outbound": "3.39.0",
67
+ "@stigmer/plugin-package": "3.39.0",
68
+ "@stigmer/protos": "3.39.0",
69
+ "@stigmer/temporal-codecs": "3.39.0",
70
+ "@stigmer/zip-structure": "3.39.0",
71
71
  "@temporalio/activity": "1.16.2",
72
72
  "@temporalio/client": "1.16.2",
73
73
  "@temporalio/common": "1.16.2",
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Pins the store-fault contract of the agent's visibility-update load: a
3
+ * typed ResourceNotFoundError answers NotFound with the domain's pinned copy
4
+ * (`agent 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 agent 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 default-instance applier is untouchable: a load that fails must stop
12
+ * the call before anything past it runs.
13
+ *
14
+ * Out of scope: the NotFound copy itself (wire contract) and getByAgent's
15
+ * empty-list answer on an agent-read fault in the channel and share domains
16
+ * (stigmer/stigmer#1375).
17
+ */
18
+ import type { Client, ConnectError } from "@connectrpc/connect";
19
+ import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
20
+ import { describe, expect, it } from "vitest";
21
+
22
+ import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
23
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_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 {
29
+ errorOf,
30
+ failingStore,
31
+ untouchable,
32
+ } from "../../../pipeline/__tests__/support.js";
33
+ import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
34
+ import { ResourceNotFoundError } from "../../../store/interface.js";
35
+ import type { Store } from "../../../store/interface.js";
36
+
37
+ import { registerAgentServices } from "../controller.js";
38
+
39
+ const silentLogger = createLogger({
40
+ level: "error",
41
+ pretty: false,
42
+ write: () => {},
43
+ });
44
+
45
+ const AGENT_ID = "agt_storefault";
46
+
47
+ const NOT_FOUND_COPY = `agent not found: ${AGENT_ID}`;
48
+ const LOAD_FAULT_COPY = "failed to load agent";
49
+
50
+ const MISSING = (): Error => new ResourceNotFoundError(`agent/${AGENT_ID}`);
51
+ const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
52
+
53
+ /**
54
+ * The registered handlers on an in-process router: the verifier chain stamps
55
+ * the trusted-local caller and the apiresource interceptor the kind, then
56
+ * Authorize and ValidateProto run and the load is the first step that reads
57
+ * the store.
58
+ */
59
+ function agentCommand(store: Store): Client<typeof AgentCommandController> {
60
+ const transport = createRouterTransport(
61
+ (router) => {
62
+ registerAgentServices(router, {
63
+ store,
64
+ logger: silentLogger,
65
+ authorizer: newPermissiveSingleTeamAuthorizer(),
66
+ authorizationLifecycle: undefined,
67
+ agentInstanceApplier: untouchable("agentInstanceApplier"),
68
+ });
69
+ },
70
+ {
71
+ router: {
72
+ interceptors: [
73
+ createVerifierChainInterceptor([], [], silentLogger),
74
+ createApiResourceInterceptor(),
75
+ ],
76
+ },
77
+ },
78
+ );
79
+ return createClient(AgentCommandController, transport);
80
+ }
81
+
82
+ describe("updateVisibility — LoadAgentForVisibilityUpdate", () => {
83
+ function surfaceError(store: Store): Promise<ConnectError> {
84
+ return errorOf(() =>
85
+ agentCommand(store).updateVisibility({
86
+ resourceId: AGENT_ID,
87
+ visibility: ApiResourceVisibility.visibility_private,
88
+ }),
89
+ );
90
+ }
91
+
92
+ it("a missing agent answers NotFound with the domain's copy", async () => {
93
+ const error = await surfaceError(failingStore(MISSING()));
94
+
95
+ expect(error.code).toBe(Code.NotFound);
96
+ expect(error.rawMessage).toBe(NOT_FOUND_COPY);
97
+ });
98
+
99
+ it("any other store failure answers a sanitized Internal", async () => {
100
+ const error = await surfaceError(failingStore(LOCKED()));
101
+
102
+ expect(error.code).toBe(Code.Internal);
103
+ expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
104
+ });
105
+ });
@@ -8,8 +8,8 @@
8
8
  * agent to serve: a session with no agent runs the built-in assistant.
9
9
  *
10
10
  * Pipeline per RPC mirrors the Go step chains character-for-character.
11
- * Proven by agent.conformance.test.ts (CONFORMANCE_TARGET=local) and
12
- * __tests__/agent.test.ts.
11
+ * Proven by agent.conformance.test.ts (CONFORMANCE_TARGET=local),
12
+ * __tests__/agent.test.ts and __tests__/store-faults.test.ts.
13
13
  *
14
14
  * Versus Stigmer Cloud, OSS excludes the Authorize, CreateIamPolicies, and
15
15
  * Publish steps (no multi-tenant auth, IAM/FGA, or event publishing here).
@@ -91,6 +91,7 @@ import {
91
91
  newValidateVisibilityStep,
92
92
  newValidateVisibilityUpdateStep,
93
93
  } from "../../pipeline/steps/validate-visibility.js";
94
+ import { ResourceNotFoundError } from "../../store/interface.js";
94
95
  import type { Store } from "../../store/interface.js";
95
96
  import { agentSearchExtractor } from "./search-extractor.js";
96
97
  import {
@@ -382,7 +383,7 @@ async function updateVisibility(
382
383
  return reqCtx.get(UPDATE_VISIBILITY_AGENT_KEY) as Agent;
383
384
  }
384
385
 
385
- /** Loads the agent by resource_id; ANY load failure → NotFound. */
386
+ /** Loads the agent by resource_id; a missing one answers NotFound, a store fault Internal. */
386
387
  function newLoadAgentForVisibilityUpdateStep(
387
388
  store: Store,
388
389
  ): PipelineStep<UpdateVisibilityDesc> {
@@ -397,8 +398,11 @@ function newLoadAgentForVisibilityUpdateStep(
397
398
  input.resourceId,
398
399
  AgentSchema,
399
400
  );
400
- } catch {
401
- throw notFoundError("agent", input.resourceId);
401
+ } catch (error) {
402
+ if (error instanceof ResourceNotFoundError) {
403
+ throw notFoundError("agent", input.resourceId);
404
+ }
405
+ throw internalError(error, "failed to load agent");
402
406
  }
403
407
  ctx.set(UPDATE_VISIBILITY_AGENT_KEY, agent);
404
408
  },
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Pins the store-fault contract of the install lane's channel load, which
3
+ * initiateInstall and completeInstall share: a typed ResourceNotFoundError
4
+ * answers NotFound with the domain's pinned copy
5
+ * (`AgentChannel not found: <id>`), and any other store failure is an
6
+ * infrastructure fault answered as a sanitized Internal, never a NotFound
7
+ * that tells a client the channel does not exist (stigmer/stigmer#1345).
8
+ *
9
+ * Both surfaces are reached through the registered handler on an in-process
10
+ * router. The composed suites reach only a real store, which cannot fail
11
+ * selectively, so each runs here against a store whose read throws. The
12
+ * channel runtime and the model registry are untouchable: the load comes
13
+ * before authorization and before the runtime, so a load that fails must
14
+ * stop the call before either.
15
+ *
16
+ * Out of scope: the NotFound copy itself (wire contract, pinned by
17
+ * agentchannel.test.ts and the agentchannel conformance suite) and
18
+ * getByAgent's empty-list answer on an agent-read fault
19
+ * (stigmer/stigmer#1375).
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 { AgentChannelCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/command_pb";
26
+
27
+ import { createLogger } from "../../../boot/logger.js";
28
+ import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.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 type { ChannelRuntime } from "../channel-runtime.js";
40
+ import { registerAgentChannelServices } from "../controller.js";
41
+
42
+ const silentLogger = createLogger({
43
+ level: "error",
44
+ pretty: false,
45
+ write: () => {},
46
+ });
47
+
48
+ const CHANNEL_ID = "ach_storefault";
49
+
50
+ const NOT_FOUND_COPY = `AgentChannel not found: ${CHANNEL_ID}`;
51
+ const LOAD_FAULT_COPY = "failed to load agent channel";
52
+
53
+ const MISSING = (): Error =>
54
+ new ResourceNotFoundError(`agent_channel/${CHANNEL_ID}`);
55
+ const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
56
+
57
+ type ChannelCommand = Client<typeof AgentChannelCommandController>;
58
+
59
+ /**
60
+ * The registered handlers on an in-process router: the verifier chain stamps
61
+ * the trusted-local caller and the apiresource interceptor the kind the
62
+ * install load reads; the load is the first thing either handler does.
63
+ */
64
+ function channelCommand(store: Store): ChannelCommand {
65
+ const transport = createRouterTransport(
66
+ (router) => {
67
+ registerAgentChannelServices(router, {
68
+ store,
69
+ logger: silentLogger,
70
+ authorizer: newPermissiveSingleTeamAuthorizer(),
71
+ authorizationLifecycle: undefined,
72
+ modelRegistry: untouchable("modelRegistry"),
73
+ channelRuntime: untouchable<ChannelRuntime>("channelRuntime"),
74
+ listReadScope: undefined,
75
+ });
76
+ },
77
+ {
78
+ router: {
79
+ interceptors: [
80
+ createVerifierChainInterceptor([], [], silentLogger),
81
+ createApiResourceInterceptor(),
82
+ ],
83
+ },
84
+ },
85
+ );
86
+ return createClient(AgentChannelCommandController, transport);
87
+ }
88
+
89
+ /** Each surface with the input its proto requires. */
90
+ const SURFACES: ReadonlyArray<
91
+ readonly [string, (command: ChannelCommand) => Promise<unknown>]
92
+ > = [
93
+ [
94
+ "initiateInstall — the install load",
95
+ (command) => command.initiateInstall({ resourceId: CHANNEL_ID }),
96
+ ],
97
+ [
98
+ "completeInstall — the install load",
99
+ (command) =>
100
+ command.completeInstall({
101
+ resourceId: CHANNEL_ID,
102
+ state: "install-state",
103
+ code: "provider-code",
104
+ }),
105
+ ],
106
+ ];
107
+
108
+ describe.each(SURFACES)("%s", (_name, call) => {
109
+ function surfaceError(store: Store): Promise<ConnectError> {
110
+ return errorOf(() => call(channelCommand(store)));
111
+ }
112
+
113
+ it("a missing channel answers NotFound with the domain's copy", async () => {
114
+ const error = await surfaceError(failingStore(MISSING()));
115
+
116
+ expect(error.code).toBe(Code.NotFound);
117
+ expect(error.rawMessage).toBe(NOT_FOUND_COPY);
118
+ });
119
+
120
+ it("any other store failure answers a sanitized Internal", async () => {
121
+ const error = await surfaceError(failingStore(LOCKED()));
122
+
123
+ expect(error.code).toBe(Code.Internal);
124
+ expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
125
+ });
126
+ });
@@ -23,8 +23,8 @@
23
23
  * (only same-org shares are) — a dangling channel is tolerated because
24
24
  * OSS has no serving runtime and cloud fails closed elsewhere.
25
25
  *
26
- * Proven by agentchannel.conformance.test.ts (CONFORMANCE_TARGET=local)
27
- * and __tests__/agentchannel.test.ts.
26
+ * Proven by agentchannel.conformance.test.ts (CONFORMANCE_TARGET=local),
27
+ * __tests__/agentchannel.test.ts and __tests__/store-faults.test.ts.
28
28
  */
29
29
  import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
30
30
  import { create, fromBinary } from "@bufbuild/protobuf";
@@ -108,6 +108,7 @@ import { newPersistStep } from "../../pipeline/steps/persist.js";
108
108
  import { newResolveSlugStep } from "../../pipeline/steps/slug.js";
109
109
  import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
110
110
  import { newValidateVisibilityStep } from "../../pipeline/steps/validate-visibility.js";
111
+ import { ResourceNotFoundError } from "../../store/interface.js";
111
112
  import type { Store } from "../../store/interface.js";
112
113
  import type { ModelCatalogProvider } from "../workflow/registry/model-catalog-provider.js";
113
114
  import type { ChannelRuntime } from "./channel-runtime.js";
@@ -395,9 +396,10 @@ async function completeInstall(
395
396
  }
396
397
 
397
398
  /**
398
- * Loads the install target — cloud's LoadChannel NOT_FOUND, verbatim.
399
- * Returns the loaded channel so a composed runtime receives it without a
400
- * second load (the load-then-X contract stays OSS-owned).
399
+ * Loads the install target — cloud's LoadChannel NOT_FOUND, verbatim, for
400
+ * a missing channel; a store fault answers Internal. Returns the loaded
401
+ * channel so a composed runtime receives it without a second load (the
402
+ * load-then-X contract stays OSS-owned).
401
403
  */
402
404
  async function loadChannelForInstall(
403
405
  deps: AgentChannelControllerDeps,
@@ -410,8 +412,11 @@ async function loadChannelForInstall(
410
412
  resourceId,
411
413
  AgentChannelSchema,
412
414
  );
413
- } catch {
414
- throw notFoundError("AgentChannel", resourceId);
415
+ } catch (error) {
416
+ if (error instanceof ResourceNotFoundError) {
417
+ throw notFoundError("AgentChannel", resourceId);
418
+ }
419
+ throw internalError(error, "failed to load agent channel");
415
420
  }
416
421
  }
417
422
 
@@ -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
  },