@stigmer/server 3.38.3 → 3.39.1-dev.20260928194002

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) 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/agentinstance/steps.d.ts +3 -1
  14. package/dist/domain/agentinstance/steps.d.ts.map +1 -1
  15. package/dist/domain/agentinstance/steps.js +12 -6
  16. package/dist/domain/agentinstance/steps.js.map +1 -1
  17. package/dist/domain/agentshare/controller.d.ts +2 -2
  18. package/dist/domain/agentshare/controller.d.ts.map +1 -1
  19. package/dist/domain/agentshare/controller.js +7 -3
  20. package/dist/domain/agentshare/controller.js.map +1 -1
  21. package/dist/domain/organization/controller.d.ts.map +1 -1
  22. package/dist/domain/organization/controller.js +33 -14
  23. package/dist/domain/organization/controller.js.map +1 -1
  24. package/dist/domain/organization/slug-ledger.d.ts +86 -0
  25. package/dist/domain/organization/slug-ledger.d.ts.map +1 -0
  26. package/dist/domain/organization/slug-ledger.js +128 -0
  27. package/dist/domain/organization/slug-ledger.js.map +1 -0
  28. package/dist/domain/organization/steps.d.ts +12 -3
  29. package/dist/domain/organization/steps.d.ts.map +1 -1
  30. package/dist/domain/organization/steps.js +28 -6
  31. package/dist/domain/organization/steps.js.map +1 -1
  32. package/dist/domain/plugin/controller.d.ts.map +1 -1
  33. package/dist/domain/plugin/controller.js +10 -5
  34. package/dist/domain/plugin/controller.js.map +1 -1
  35. package/dist/domain/skill/controller.d.ts.map +1 -1
  36. package/dist/domain/skill/controller.js +7 -3
  37. package/dist/domain/skill/controller.js.map +1 -1
  38. package/dist/domain/workflow/controller.js +6 -3
  39. package/dist/domain/workflow/controller.js.map +1 -1
  40. package/dist/domain/workflowinstance/controller.d.ts +2 -2
  41. package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
  42. package/dist/domain/workflowinstance/controller.js +10 -5
  43. package/dist/domain/workflowinstance/controller.js.map +1 -1
  44. package/dist/domain/workflowinstance/steps.d.ts +5 -3
  45. package/dist/domain/workflowinstance/steps.d.ts.map +1 -1
  46. package/dist/domain/workflowinstance/steps.js +14 -9
  47. package/dist/domain/workflowinstance/steps.js.map +1 -1
  48. package/dist/extensions/gate-slots.d.ts +7 -5
  49. package/dist/extensions/gate-slots.d.ts.map +1 -1
  50. package/dist/extensions/gate-slots.js.map +1 -1
  51. package/dist/index.d.ts +3 -1
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +2 -1
  54. package/dist/index.js.map +1 -1
  55. package/dist/pipeline/errors.d.ts +8 -0
  56. package/dist/pipeline/errors.d.ts.map +1 -1
  57. package/dist/pipeline/errors.js +19 -0
  58. package/dist/pipeline/errors.js.map +1 -1
  59. package/dist/store/interface.d.ts +67 -0
  60. package/dist/store/interface.d.ts.map +1 -1
  61. package/dist/store/interface.js.map +1 -1
  62. package/dist/store/organization-slug-history.d.ts +71 -0
  63. package/dist/store/organization-slug-history.d.ts.map +1 -0
  64. package/dist/store/organization-slug-history.js +88 -0
  65. package/dist/store/organization-slug-history.js.map +1 -0
  66. package/dist/store/postgres/migrations.d.ts +3 -1
  67. package/dist/store/postgres/migrations.d.ts.map +1 -1
  68. package/dist/store/postgres/migrations.js +69 -1
  69. package/dist/store/postgres/migrations.js.map +1 -1
  70. package/dist/store/postgres/store.d.ts +2 -1
  71. package/dist/store/postgres/store.d.ts.map +1 -1
  72. package/dist/store/postgres/store.js +49 -0
  73. package/dist/store/postgres/store.js.map +1 -1
  74. package/dist/store/sqlite/migrations.d.ts +3 -1
  75. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  76. package/dist/store/sqlite/migrations.js +54 -1
  77. package/dist/store/sqlite/migrations.js.map +1 -1
  78. package/dist/store/sqlite/store.d.ts +2 -1
  79. package/dist/store/sqlite/store.d.ts.map +1 -1
  80. package/dist/store/sqlite/store.js +55 -0
  81. package/dist/store/sqlite/store.js.map +1 -1
  82. package/package.json +6 -6
  83. package/src/domain/agent/__tests__/store-faults.test.ts +105 -0
  84. package/src/domain/agent/controller.ts +9 -5
  85. package/src/domain/agentchannel/__tests__/store-faults.test.ts +126 -0
  86. package/src/domain/agentchannel/controller.ts +12 -7
  87. package/src/domain/agentinstance/__tests__/store-faults.test.ts +161 -0
  88. package/src/domain/agentinstance/controller.ts +9 -5
  89. package/src/domain/agentinstance/steps.ts +12 -6
  90. package/src/domain/agentshare/__tests__/store-faults.test.ts +101 -0
  91. package/src/domain/agentshare/controller.ts +9 -5
  92. package/src/domain/organization/__tests__/organization-delete.test.ts +73 -18
  93. package/src/domain/organization/__tests__/organization-slugs.test.ts +386 -0
  94. package/src/domain/organization/controller.ts +36 -14
  95. package/src/domain/organization/slug-ledger.ts +209 -0
  96. package/src/domain/organization/steps.ts +28 -6
  97. package/src/domain/plugin/__tests__/store-faults.test.ts +104 -0
  98. package/src/domain/plugin/controller.ts +10 -5
  99. package/src/domain/skill/__tests__/store-faults.test.ts +103 -0
  100. package/src/domain/skill/controller.ts +7 -3
  101. package/src/domain/workflow/__tests__/store-faults.test.ts +107 -0
  102. package/src/domain/workflow/controller.ts +6 -3
  103. package/src/domain/workflowinstance/__tests__/store-faults.test.ts +172 -0
  104. package/src/domain/workflowinstance/controller.ts +12 -7
  105. package/src/domain/workflowinstance/steps.ts +14 -10
  106. package/src/extensions/gate-slots.ts +7 -5
  107. package/src/index.ts +11 -0
  108. package/src/pipeline/__tests__/blind-not-found.test.ts +4 -24
  109. package/src/pipeline/errors.ts +23 -0
  110. package/src/store/__tests__/organization-slug-history.test.ts +109 -0
  111. package/src/store/__tests__/store-contract.ts +86 -1
  112. package/src/store/interface.ts +70 -0
  113. package/src/store/organization-slug-history.ts +159 -0
  114. package/src/store/postgres/__tests__/migrations.test.ts +138 -2
  115. package/src/store/postgres/__tests__/store-contract.test.ts +1 -0
  116. package/src/store/postgres/migrations.ts +84 -1
  117. package/src/store/postgres/store.ts +84 -0
  118. package/src/store/sqlite/__tests__/migrations.test.ts +139 -10
  119. package/src/store/sqlite/migrations.ts +69 -1
  120. package/src/store/sqlite/store.ts +84 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Pins the store-fault contract of the agent instance's two loads. The
3
+ * visibility-update load: a typed ResourceNotFoundError answers NotFound with
4
+ * the domain's pinned copy (`agent instance not found: <id>`), and any other
5
+ * store failure is an infrastructure fault answered as a sanitized Internal,
6
+ * never a NotFound that tells a client the instance does not exist
7
+ * (stigmer/stigmer#1345). Create's parent-agent load reads the agent through
8
+ * the in-process query client, so the store's typed miss arrives as a Connect
9
+ * NotFound: that answers the pinned `Agent not found: <id>`, and any other
10
+ * failure an Internal, never a missing parent (stigmer/stigmer#1351).
11
+ *
12
+ * Both surfaces are reached through the registered handler on an in-process
13
+ * router. The composed suites reach only a real store, which cannot fail
14
+ * selectively, so the visibility update runs against a store whose read
15
+ * throws and create against a parent loader whose read throws. Whatever the
16
+ * surface does not need is untouchable: a load that fails must stop the call
17
+ * before anything past it runs.
18
+ *
19
+ * Out of scope: the NotFound copies themselves (wire contract).
20
+ */
21
+ import type { Client } from "@connectrpc/connect";
22
+ import {
23
+ Code,
24
+ ConnectError,
25
+ createClient,
26
+ createRouterTransport,
27
+ } from "@connectrpc/connect";
28
+ import { describe, expect, it } from "vitest";
29
+
30
+ import { AgentInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/command_pb";
31
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
32
+
33
+ import { createLogger } from "../../../boot/logger.js";
34
+ import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
35
+ import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
36
+ import {
37
+ errorOf,
38
+ failingStore,
39
+ untouchable,
40
+ } from "../../../pipeline/__tests__/support.js";
41
+ import { INTERNAL_FALLBACK_MESSAGE } from "../../../pipeline/pipeline.js";
42
+ import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
43
+ import { ResourceNotFoundError } from "../../../store/interface.js";
44
+ import type { Store } from "../../../store/interface.js";
45
+
46
+ import { registerAgentInstanceServices } from "../controller.js";
47
+ import type { ParentAgentLoaderProvider } from "../steps.js";
48
+
49
+ const silentLogger = createLogger({
50
+ level: "error",
51
+ pretty: false,
52
+ write: () => {},
53
+ });
54
+
55
+ const INSTANCE_ID = "ain_storefault";
56
+ const AGENT_ID = "agt_storefault";
57
+
58
+ const NOT_FOUND_COPY = `agent instance not found: ${INSTANCE_ID}`;
59
+ const LOAD_FAULT_COPY = "failed to load agent instance";
60
+ const PARENT_NOT_FOUND_COPY = `Agent not found: ${AGENT_ID}`;
61
+ const PARENT_LOAD_FAULT_COPY = "failed to load parent agent";
62
+
63
+ const MISSING = (): Error =>
64
+ new ResourceNotFoundError(`agent_instance/${INSTANCE_ID}`);
65
+ const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
66
+
67
+ /** What the agent query's `get` answers for a missing and a faulted agent. */
68
+ const PARENT_MISSING = (): Error =>
69
+ new ConnectError(`Agent not found: ${AGENT_ID}`, Code.NotFound);
70
+ const PARENT_FAULTED = (): Error =>
71
+ new ConnectError(INTERNAL_FALLBACK_MESSAGE, Code.Internal);
72
+
73
+ /**
74
+ * The registered handlers on an in-process router: the verifier chain stamps
75
+ * the trusted-local caller and the apiresource interceptor the kind, then
76
+ * Authorize and ValidateProto run and the load is the first step that reads
77
+ * the store or the parent.
78
+ */
79
+ function instanceCommand(
80
+ store: Store,
81
+ parentAgentLoader: ParentAgentLoaderProvider = untouchable(
82
+ "parentAgentLoader",
83
+ ),
84
+ ): Client<typeof AgentInstanceCommandController> {
85
+ const transport = createRouterTransport(
86
+ (router) => {
87
+ registerAgentInstanceServices(router, {
88
+ store,
89
+ logger: silentLogger,
90
+ authorizer: newPermissiveSingleTeamAuthorizer(),
91
+ authorizationLifecycle: undefined,
92
+ parentAgentLoader,
93
+ listReadScope: undefined,
94
+ });
95
+ },
96
+ {
97
+ router: {
98
+ interceptors: [
99
+ createVerifierChainInterceptor([], [], silentLogger),
100
+ createApiResourceInterceptor(),
101
+ ],
102
+ },
103
+ },
104
+ );
105
+ return createClient(AgentInstanceCommandController, transport);
106
+ }
107
+
108
+ describe("updateVisibility — LoadInstanceForVisibilityUpdate", () => {
109
+ function surfaceError(store: Store): Promise<ConnectError> {
110
+ return errorOf(() =>
111
+ instanceCommand(store).updateVisibility({
112
+ resourceId: INSTANCE_ID,
113
+ visibility: ApiResourceVisibility.visibility_private,
114
+ }),
115
+ );
116
+ }
117
+
118
+ it("a missing agent instance answers NotFound with the domain's copy", async () => {
119
+ const error = await surfaceError(failingStore(MISSING()));
120
+
121
+ expect(error.code).toBe(Code.NotFound);
122
+ expect(error.rawMessage).toBe(NOT_FOUND_COPY);
123
+ });
124
+
125
+ it("any other store failure answers a sanitized Internal", async () => {
126
+ const error = await surfaceError(failingStore(LOCKED()));
127
+
128
+ expect(error.code).toBe(Code.Internal);
129
+ expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
130
+ });
131
+ });
132
+
133
+ describe("create — LoadParentAgent", () => {
134
+ function surfaceError(parentError: Error): Promise<ConnectError> {
135
+ const failingParent: ParentAgentLoaderProvider = () => ({
136
+ get: () => Promise.reject(parentError),
137
+ });
138
+ return errorOf(() =>
139
+ instanceCommand(untouchable("store"), failingParent).create({
140
+ apiVersion: "agentic.stigmer.ai/v1",
141
+ kind: "AgentInstance",
142
+ metadata: { name: "Store Fault Instance", org: "org_storefault" },
143
+ spec: { agentId: AGENT_ID },
144
+ }),
145
+ );
146
+ }
147
+
148
+ it("a missing parent agent answers NotFound with the pinned copy", async () => {
149
+ const error = await surfaceError(PARENT_MISSING());
150
+
151
+ expect(error.code).toBe(Code.NotFound);
152
+ expect(error.rawMessage).toBe(PARENT_NOT_FOUND_COPY);
153
+ });
154
+
155
+ it("any other parent-load failure answers a sanitized Internal", async () => {
156
+ const error = await surfaceError(PARENT_FAULTED());
157
+
158
+ expect(error.code).toBe(Code.Internal);
159
+ expect(error.rawMessage).toBe(PARENT_LOAD_FAULT_COPY);
160
+ });
161
+ });
@@ -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
  },
@@ -6,6 +6,7 @@
6
6
  * contracts: the parent-agent edge, the immutable agent_id, the
7
7
  * default-instance visibility guard, and the org/label list filters.
8
8
  */
9
+ import { Code, ConnectError } from "@connectrpc/connect";
9
10
  import { create, fromBinary } from "@bufbuild/protobuf";
10
11
  import type { DescMessage } from "@bufbuild/protobuf";
11
12
 
@@ -69,7 +70,9 @@ export const PARENT_AGENT_KEY = "parent_agent";
69
70
  * an agent is a shareable blueprint and one agent legitimately has
70
71
  * instances in several orgs (the marketplace case) — the instance's
71
72
  * organization is the caller's, and the check below asks the caller's
72
- * standing there.
73
+ * standing there. The agent is read through the in-process query client, so
74
+ * a missing one arrives as a Connect NotFound; any other failure is a fault
75
+ * answered as Internal, never a missing parent.
73
76
  */
74
77
  export function newLoadParentAgentStep(
75
78
  agentLoader: ParentAgentLoaderProvider,
@@ -88,11 +91,14 @@ export function newLoadParentAgentStep(
88
91
  try {
89
92
  parentAgent = await agentLoader().get(agentId);
90
93
  } catch (error) {
91
- logger.warn("Parent agent not found", {
92
- agentId,
93
- error: error instanceof Error ? error.message : String(error),
94
- });
95
- throw notFoundError("Agent", agentId);
94
+ if (error instanceof ConnectError && error.code === Code.NotFound) {
95
+ logger.warn("Parent agent not found", {
96
+ agentId,
97
+ error: error.rawMessage,
98
+ });
99
+ throw notFoundError("Agent", agentId);
100
+ }
101
+ throw internalError(error, "failed to load parent agent");
96
102
  }
97
103
 
98
104
  logger.debug("Loaded parent agent", {
@@ -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
  },
@@ -1,20 +1,24 @@
1
1
  /**
2
2
  * Pins the organization delete's order through a composed server in the
3
- * trusted-local posture: everything that names the organization goes
4
- * before its row, and a fault stops the delete with the organization in
5
- * place. An organization's id is its slug and the delete frees the slug for
6
- * anyone, so a row left behind would belong to the slug's next holder.
3
+ * trusted-local posture: the slug is retired first, everything that grants
4
+ * on the organization goes before its row, and a fault stops the delete
5
+ * with the organization in place. An organization's id is its slug, and a
6
+ * slug is never taken again, so nothing a deleted organization left behind
7
+ * can pass to a new holder.
7
8
  *
8
9
  * - the `org-delete:pre-delete` slot runs with the organization loaded
9
10
  * and still stored, and its rows still in place;
10
11
  * - a slot step's refusal answers its own code and copy, and leaves the
11
- * organization and its rows;
12
+ * organization and its rows, its slug already retired;
13
+ * - a fault retiring the slug fails the delete before anything else, with
14
+ * the organization and its rows in place;
12
15
  * - the organization's policy rows are revoked before its row, as
13
16
  * principal and as resource: a revocation fault answers Internal with
14
17
  * fixed copy, leaves the organization and its owner row, and a retry
15
18
  * completes the delete;
16
- * - a slug freed by the delete and created again carries none of the old
17
- * organization's rows.
19
+ * - a create of a deleted organization's slug is refused with the
20
+ * ORGANIZATION_SLUG_RESERVED reason, and no row of the old organization
21
+ * gains an owner.
18
22
  *
19
23
  * The policy store is the library's in-memory double, composed as the
20
24
  * IamPolicy store driver so a test can make one row's delete fault; the
@@ -28,13 +32,22 @@ import type { DescMessage } from "@bufbuild/protobuf";
28
32
  import { Code, ConnectError, createClient } from "@connectrpc/connect";
29
33
  import type { Client, Transport } from "@connectrpc/connect";
30
34
  import { createGrpcTransport } from "@connectrpc/connect-node";
31
- import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
35
+ import {
36
+ afterAll,
37
+ beforeAll,
38
+ beforeEach,
39
+ describe,
40
+ expect,
41
+ it,
42
+ vi,
43
+ } from "vitest";
32
44
 
33
45
  import { IamPolicyCommandController } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/command_pb";
34
46
  import type { IamPolicy } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
35
47
  import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
36
48
  import { OrganizationCommandController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/command_pb";
37
49
  import { OrganizationQueryController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/query_pb";
50
+ import { ErrorInfoSchema } from "@stigmer/protos/google/rpc/error_details_pb";
38
51
 
39
52
  import { loadConfig } from "../../../boot/config.js";
40
53
  import { composeServer } from "../../../boot/compose.js";
@@ -215,6 +228,36 @@ describe("organization delete (composed server, trusted-local posture)", () => {
215
228
  (await organizationQuery.get({ value: REFUSED_SLUG })).metadata?.id,
216
229
  ).toBe(REFUSED_SLUG);
217
230
  expect(rowsNaming(REFUSED_SLUG)).toHaveLength(3);
231
+ // The slug was retired before the slot refused; the organization lives
232
+ // on, and its slug is refused to a create either way.
233
+ expect(
234
+ (await server.store.organizationSlugs.find(REFUSED_SLUG))?.retiredAt,
235
+ ).not.toBe("");
236
+ });
237
+
238
+ it("a fault retiring the slug fails the delete before anything else, with the organization and its rows in place", async () => {
239
+ await organizationWithRows("delete-retire-faults");
240
+ const retire = vi
241
+ .spyOn(server.store.organizationSlugs, "retire")
242
+ .mockRejectedValueOnce(new Error("ledger unavailable"));
243
+ try {
244
+ const failed = await grpcError(() =>
245
+ organizations.delete({ value: "delete-retire-faults" }),
246
+ );
247
+ expect(failed.code).toBe(Code.Internal);
248
+ expect(failed.rawMessage).toBe("failed to retire the organization slug");
249
+ } finally {
250
+ retire.mockRestore();
251
+ }
252
+ expect(seenBySlot.has("delete-retire-faults")).toBe(false);
253
+ expect(
254
+ (await organizationQuery.get({ value: "delete-retire-faults" })).metadata
255
+ ?.id,
256
+ ).toBe("delete-retire-faults");
257
+ expect(rowsNaming("delete-retire-faults")).toHaveLength(3);
258
+
259
+ await organizations.delete({ value: "delete-retire-faults" });
260
+ expect(rowsNaming("delete-retire-faults")).toEqual([]);
218
261
  });
219
262
 
220
263
  it("a revocation fault fails the delete with the organization and its owner in place, and the retry completes it", async () => {
@@ -248,19 +291,31 @@ describe("organization delete (composed server, trusted-local posture)", () => {
248
291
  expect(rowsNaming("delete-faults")).toEqual([]);
249
292
  });
250
293
 
251
- it("a slug freed by the delete and created again carries none of the old organization's rows", async () => {
294
+ it("a deleted organization's slug is never taken again: the create is refused with the reserved reason", async () => {
252
295
  await organizationWithRows("delete-reborn");
253
296
  await organizations.delete({ value: "delete-reborn" });
254
297
 
255
- await organizations.create({
256
- apiVersion: "tenancy.stigmer.ai/v1",
257
- kind: "Organization",
258
- metadata: { name: "delete-reborn", slug: "delete-reborn", org: "" },
259
- spec: { description: "the slug, taken again" },
260
- });
298
+ const refused = await grpcError(() =>
299
+ organizations.create({
300
+ apiVersion: "tenancy.stigmer.ai/v1",
301
+ kind: "Organization",
302
+ metadata: { name: "delete-reborn", slug: "delete-reborn", org: "" },
303
+ spec: { description: "the slug, taken again" },
304
+ }),
305
+ );
261
306
 
262
- expect(
263
- rowsNaming("delete-reborn").map((row) => row.spec?.relation),
264
- ).toEqual(["owner"]);
307
+ expect(refused.code).toBe(Code.AlreadyExists);
308
+ expect(refused.rawMessage).toBe(
309
+ "Organization slug 'delete-reborn' belonged to an organization that was deleted, and a slug is never reused",
310
+ );
311
+ const [reason] = refused.findDetails(ErrorInfoSchema);
312
+ expect(reason?.reason).toBe("ORGANIZATION_SLUG_RESERVED");
313
+ expect(reason?.domain).toBe("stigmer.ai");
314
+ expect(reason?.metadata).toEqual({ slug: "delete-reborn" });
315
+ expect(rowsNaming("delete-reborn")).toEqual([]);
316
+ const gone = await grpcError(() =>
317
+ organizationQuery.get({ value: "delete-reborn" }),
318
+ );
319
+ expect(gone.code).toBe(Code.NotFound);
265
320
  });
266
321
  });