@stigmer/server 3.39.0 → 3.40.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 (150) hide show
  1. package/dist/artifactstorage/artifact-storage.d.ts +10 -2
  2. package/dist/artifactstorage/artifact-storage.d.ts.map +1 -1
  3. package/dist/artifactstorage/artifact-storage.js +6 -3
  4. package/dist/artifactstorage/artifact-storage.js.map +1 -1
  5. package/dist/boot/artifact-lane.d.ts +55 -0
  6. package/dist/boot/artifact-lane.d.ts.map +1 -0
  7. package/dist/boot/artifact-lane.js +74 -0
  8. package/dist/boot/artifact-lane.js.map +1 -0
  9. package/dist/boot/compose.d.ts.map +1 -1
  10. package/dist/boot/compose.js +42 -19
  11. package/dist/boot/compose.js.map +1 -1
  12. package/dist/boot/config.d.ts +11 -6
  13. package/dist/boot/config.d.ts.map +1 -1
  14. package/dist/boot/config.js +10 -5
  15. package/dist/boot/config.js.map +1 -1
  16. package/dist/domain/agent/controller.d.ts +5 -2
  17. package/dist/domain/agent/controller.d.ts.map +1 -1
  18. package/dist/domain/agent/controller.js.map +1 -1
  19. package/dist/domain/agentexecution/controller.d.ts +18 -4
  20. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  21. package/dist/domain/agentexecution/controller.js +2 -2
  22. package/dist/domain/agentexecution/controller.js.map +1 -1
  23. package/dist/domain/agentexecution/create-steps.d.ts +29 -15
  24. package/dist/domain/agentexecution/create-steps.d.ts.map +1 -1
  25. package/dist/domain/agentexecution/create-steps.js +118 -51
  26. package/dist/domain/agentexecution/create-steps.js.map +1 -1
  27. package/dist/domain/agentinstance/controller.d.ts +8 -2
  28. package/dist/domain/agentinstance/controller.d.ts.map +1 -1
  29. package/dist/domain/agentinstance/controller.js.map +1 -1
  30. package/dist/domain/agentinstance/steps.d.ts +3 -1
  31. package/dist/domain/agentinstance/steps.d.ts.map +1 -1
  32. package/dist/domain/agentinstance/steps.js +12 -6
  33. package/dist/domain/agentinstance/steps.js.map +1 -1
  34. package/dist/domain/agentshare/controller.d.ts +9 -4
  35. package/dist/domain/agentshare/controller.d.ts.map +1 -1
  36. package/dist/domain/agentshare/controller.js +3 -3
  37. package/dist/domain/agentshare/controller.js.map +1 -1
  38. package/dist/domain/artifact/file-server.d.ts +2 -1
  39. package/dist/domain/artifact/file-server.d.ts.map +1 -1
  40. package/dist/domain/artifact/file-server.js +6 -3
  41. package/dist/domain/artifact/file-server.js.map +1 -1
  42. package/dist/domain/channelapp/controller.d.ts +8 -3
  43. package/dist/domain/channelapp/controller.d.ts.map +1 -1
  44. package/dist/domain/channelapp/controller.js.map +1 -1
  45. package/dist/domain/environment/controller.d.ts +8 -5
  46. package/dist/domain/environment/controller.d.ts.map +1 -1
  47. package/dist/domain/environment/controller.js +3 -3
  48. package/dist/domain/environment/controller.js.map +1 -1
  49. package/dist/domain/executioncontext/controller.d.ts +7 -4
  50. package/dist/domain/executioncontext/controller.d.ts.map +1 -1
  51. package/dist/domain/executioncontext/controller.js.map +1 -1
  52. package/dist/domain/mcpserver/controller.d.ts.map +1 -1
  53. package/dist/domain/mcpserver/controller.js +6 -2
  54. package/dist/domain/mcpserver/controller.js.map +1 -1
  55. package/dist/domain/memory/constants.d.ts +9 -0
  56. package/dist/domain/memory/constants.d.ts.map +1 -1
  57. package/dist/domain/memory/constants.js +9 -0
  58. package/dist/domain/memory/constants.js.map +1 -1
  59. package/dist/domain/memory/controller.d.ts +18 -6
  60. package/dist/domain/memory/controller.d.ts.map +1 -1
  61. package/dist/domain/memory/controller.js +18 -21
  62. package/dist/domain/memory/controller.js.map +1 -1
  63. package/dist/domain/memory/steps.d.ts +38 -10
  64. package/dist/domain/memory/steps.d.ts.map +1 -1
  65. package/dist/domain/memory/steps.js +87 -17
  66. package/dist/domain/memory/steps.js.map +1 -1
  67. package/dist/domain/organization/controller.d.ts +7 -4
  68. package/dist/domain/organization/controller.d.ts.map +1 -1
  69. package/dist/domain/organization/controller.js.map +1 -1
  70. package/dist/domain/plugin/controller.d.ts.map +1 -1
  71. package/dist/domain/plugin/controller.js +10 -5
  72. package/dist/domain/plugin/controller.js.map +1 -1
  73. package/dist/domain/session/controller.d.ts +7 -3
  74. package/dist/domain/session/controller.d.ts.map +1 -1
  75. package/dist/domain/session/controller.js.map +1 -1
  76. package/dist/domain/skill/controller.d.ts.map +1 -1
  77. package/dist/domain/skill/controller.js +15 -5
  78. package/dist/domain/skill/controller.js.map +1 -1
  79. package/dist/domain/workflow/controller.d.ts +6 -2
  80. package/dist/domain/workflow/controller.d.ts.map +1 -1
  81. package/dist/domain/workflow/controller.js +6 -3
  82. package/dist/domain/workflow/controller.js.map +1 -1
  83. package/dist/domain/workflowexecution/controller.d.ts +8 -4
  84. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  85. package/dist/domain/workflowexecution/controller.js.map +1 -1
  86. package/dist/domain/workflowexecution/update-status.d.ts.map +1 -1
  87. package/dist/domain/workflowexecution/update-status.js +3 -2
  88. package/dist/domain/workflowexecution/update-status.js.map +1 -1
  89. package/dist/domain/workflowinstance/controller.d.ts +2 -2
  90. package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
  91. package/dist/domain/workflowinstance/controller.js +10 -5
  92. package/dist/domain/workflowinstance/controller.js.map +1 -1
  93. package/dist/domain/workflowinstance/steps.d.ts +5 -3
  94. package/dist/domain/workflowinstance/steps.d.ts.map +1 -1
  95. package/dist/domain/workflowinstance/steps.js +14 -9
  96. package/dist/domain/workflowinstance/steps.js.map +1 -1
  97. package/dist/extensions/identity.d.ts +28 -14
  98. package/dist/extensions/identity.d.ts.map +1 -1
  99. package/dist/extensions/identity.js +38 -0
  100. package/dist/extensions/identity.js.map +1 -1
  101. package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
  102. package/dist/pipeline/steps/guard-memory-capture.js +1 -13
  103. package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
  104. package/package.json +6 -6
  105. package/src/artifactstorage/__tests__/artifact-storage.test.ts +15 -1
  106. package/src/artifactstorage/artifact-storage.ts +12 -4
  107. package/src/boot/__tests__/artifact-lane.test.ts +71 -0
  108. package/src/boot/__tests__/compose.test.ts +73 -3
  109. package/src/boot/__tests__/config.test.ts +19 -0
  110. package/src/boot/artifact-lane.ts +96 -0
  111. package/src/boot/compose.ts +57 -21
  112. package/src/boot/config.ts +25 -15
  113. package/src/domain/agent/controller.ts +5 -2
  114. package/src/domain/agentexecution/__tests__/create-steps.test.ts +230 -2
  115. package/src/domain/agentexecution/controller.ts +32 -6
  116. package/src/domain/agentexecution/create-steps.ts +162 -62
  117. package/src/domain/agentinstance/__tests__/store-faults.test.ts +69 -18
  118. package/src/domain/agentinstance/controller.ts +8 -2
  119. package/src/domain/agentinstance/steps.ts +12 -6
  120. package/src/domain/agentshare/controller.ts +12 -7
  121. package/src/domain/artifact/file-server.ts +8 -4
  122. package/src/domain/channelapp/controller.ts +8 -3
  123. package/src/domain/environment/controller.ts +11 -8
  124. package/src/domain/executioncontext/controller.ts +7 -4
  125. package/src/domain/mcpserver/controller.ts +6 -2
  126. package/src/domain/memory/__tests__/capture-credential.test.ts +6 -6
  127. package/src/domain/memory/__tests__/person-consent-composed.test.ts +187 -0
  128. package/src/domain/memory/__tests__/person-consent.test.ts +274 -0
  129. package/src/domain/memory/constants.ts +11 -0
  130. package/src/domain/memory/controller.ts +36 -27
  131. package/src/domain/memory/steps.ts +112 -17
  132. package/src/domain/organization/controller.ts +7 -4
  133. package/src/domain/plugin/__tests__/plugin.test.ts +8 -0
  134. package/src/domain/plugin/__tests__/store-faults.test.ts +104 -0
  135. package/src/domain/plugin/controller.ts +10 -5
  136. package/src/domain/session/controller.ts +7 -3
  137. package/src/domain/skill/__tests__/skill.test.ts +4 -0
  138. package/src/domain/skill/__tests__/store-faults.test.ts +103 -0
  139. package/src/domain/skill/controller.ts +15 -5
  140. package/src/domain/workflow/__tests__/store-faults.test.ts +107 -0
  141. package/src/domain/workflow/controller.ts +12 -5
  142. package/src/domain/workflowexecution/controller.ts +8 -4
  143. package/src/domain/workflowexecution/update-status.ts +3 -2
  144. package/src/domain/workflowinstance/__tests__/store-faults.test.ts +172 -0
  145. package/src/domain/workflowinstance/controller.ts +12 -7
  146. package/src/domain/workflowinstance/steps.ts +14 -10
  147. package/src/extensions/__tests__/identity.test.ts +82 -0
  148. package/src/extensions/identity.ts +47 -0
  149. package/src/pipeline/__tests__/blind-not-found.test.ts +4 -20
  150. package/src/pipeline/steps/guard-memory-capture.ts +4 -19
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Pins the store-fault contract of the workflow's visibility-update load: a
3
+ * typed ResourceNotFoundError answers NotFound with the domain's pinned copy
4
+ * (`workflow not found: <id>`), and any other store failure is an
5
+ * infrastructure fault answered as a sanitized Internal, never a NotFound that
6
+ * tells a client the workflow does not exist (stigmer/stigmer#1345).
7
+ *
8
+ * The surface is updateVisibility, reached through the registered handler on
9
+ * an in-process router. The composed suites reach only a real store, which
10
+ * cannot fail selectively, so it runs here against a store whose read throws.
11
+ * The validator and the default-instance creator are untouchable: a load that
12
+ * fails must stop the call before anything past it runs.
13
+ *
14
+ * Out of scope: the NotFound copy itself (wire contract).
15
+ */
16
+ import type { Client, ConnectError } from "@connectrpc/connect";
17
+ import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
18
+ import { describe, expect, it } from "vitest";
19
+
20
+ import { WorkflowCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/command_pb";
21
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
22
+
23
+ import { createLogger } from "../../../boot/logger.js";
24
+ import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
25
+ import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
26
+ import {
27
+ errorOf,
28
+ failingStore,
29
+ untouchable,
30
+ } from "../../../pipeline/__tests__/support.js";
31
+ import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
32
+ import { ResourceNotFoundError } from "../../../store/interface.js";
33
+ import type { Store } from "../../../store/interface.js";
34
+
35
+ import { registerWorkflowServices } from "../controller.js";
36
+
37
+ const silentLogger = createLogger({
38
+ level: "error",
39
+ pretty: false,
40
+ write: () => {},
41
+ });
42
+
43
+ const WORKFLOW_ID = "wfl_storefault";
44
+
45
+ const NOT_FOUND_COPY = `workflow not found: ${WORKFLOW_ID}`;
46
+ const LOAD_FAULT_COPY = "failed to load workflow";
47
+
48
+ const MISSING = (): Error =>
49
+ new ResourceNotFoundError(`workflow/${WORKFLOW_ID}`);
50
+ const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
51
+
52
+ /**
53
+ * The registered handlers on an in-process router: the verifier chain stamps
54
+ * the trusted-local caller and the apiresource interceptor the kind, then
55
+ * Authorize and ValidateProto run and the load is the first step that reads
56
+ * the store.
57
+ */
58
+ function workflowCommand(
59
+ store: Store,
60
+ ): Client<typeof WorkflowCommandController> {
61
+ const transport = createRouterTransport(
62
+ (router) => {
63
+ registerWorkflowServices(router, {
64
+ store,
65
+ logger: silentLogger,
66
+ authorizer: newPermissiveSingleTeamAuthorizer(),
67
+ authorizationLifecycle: undefined,
68
+ validator: untouchable("validator"),
69
+ workflowInstanceCreator: untouchable("workflowInstanceCreator"),
70
+ });
71
+ },
72
+ {
73
+ router: {
74
+ interceptors: [
75
+ createVerifierChainInterceptor([], [], silentLogger),
76
+ createApiResourceInterceptor(),
77
+ ],
78
+ },
79
+ },
80
+ );
81
+ return createClient(WorkflowCommandController, transport);
82
+ }
83
+
84
+ describe("updateVisibility — LoadWorkflowForVisibilityUpdate", () => {
85
+ function surfaceError(store: Store): Promise<ConnectError> {
86
+ return errorOf(() =>
87
+ workflowCommand(store).updateVisibility({
88
+ resourceId: WORKFLOW_ID,
89
+ visibility: ApiResourceVisibility.visibility_private,
90
+ }),
91
+ );
92
+ }
93
+
94
+ it("a missing workflow answers NotFound with the domain's copy", async () => {
95
+ const error = await surfaceError(failingStore(MISSING()));
96
+
97
+ expect(error.code).toBe(Code.NotFound);
98
+ expect(error.rawMessage).toBe(NOT_FOUND_COPY);
99
+ });
100
+
101
+ it("any other store failure answers a sanitized Internal", async () => {
102
+ const error = await surfaceError(failingStore(LOCKED()));
103
+
104
+ expect(error.code).toBe(Code.Internal);
105
+ expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
106
+ });
107
+ });
@@ -13,8 +13,12 @@
13
13
  * Proven by workflow.conformance.test.ts (CONFORMANCE_TARGET=local) and
14
14
  * __tests__/.
15
15
  *
16
- * Versus Stigmer Cloud, OSS excludes the Authorize, CreateIamPolicies, and
17
- * Publish steps (no multi-tenant auth, IAM/FGA, or event publishing here).
16
+ * Every chain opens with Authorize; create, delete and updateVisibility run
17
+ * the shared tuple-lifecycle steps against the composed lifecycle;
18
+ * getByReference and listVersions authorize the resolved workflow as `get`
19
+ * would. getVersion evaluates its annotation through authorizeDirect;
20
+ * validateSpec deliberately does not, since it persists nothing. Per-RPC
21
+ * posture: docs/authorization-coverage.md §17.
18
22
  */
19
23
  import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
20
24
  import { create, enumToJson, fromBinary } from "@bufbuild/protobuf";
@@ -492,7 +496,7 @@ async function updateVisibility(
492
496
  return reqCtx.get(UPDATE_VISIBILITY_WORKFLOW_KEY) as Workflow;
493
497
  }
494
498
 
495
- /** Loads the workflow by resource_id; ANY load failure → NotFound. */
499
+ /** Loads the workflow by resource_id; a missing one answers NotFound, a store fault Internal. */
496
500
  function newLoadWorkflowForVisibilityUpdateStep(
497
501
  store: Store,
498
502
  ): PipelineStep<UpdateVisibilityDesc> {
@@ -507,8 +511,11 @@ function newLoadWorkflowForVisibilityUpdateStep(
507
511
  input.resourceId,
508
512
  WorkflowSchema,
509
513
  );
510
- } catch {
511
- throw notFoundError("workflow", input.resourceId);
514
+ } catch (error) {
515
+ if (error instanceof ResourceNotFoundError) {
516
+ throw notFoundError("workflow", input.resourceId);
517
+ }
518
+ throw internalError(error, "failed to load workflow");
512
519
  }
513
520
  ctx.set(UPDATE_VISIBILITY_WORKFLOW_KEY, workflow);
514
521
  },
@@ -12,10 +12,14 @@
12
12
  * Proven by workflowexecution.conformance.test.ts
13
13
  * (CONFORMANCE_TARGET=local) and __tests__/.
14
14
  *
15
- * Versus Stigmer Cloud, OSS excludes the Authorize, Publish,
16
- * PublishToRedis, and TransformResponse steps (no multi-tenant auth,
17
- * event publishing, or Redis here — subscribe streams ride in-memory
18
- * channels per ADR 011).
15
+ * Every chain opens with Authorize, and create also authorizes its run
16
+ * target (AuthorizeRunTarget); create and delete run the shared
17
+ * tuple-lifecycle steps against the composed lifecycle. The streams and
18
+ * the event log evaluate their annotation through authorizeDirect; the
19
+ * list, summary and pending-approval reads narrow through the composed
20
+ * list read scope. Subscribe streams ride the in-memory stream broker
21
+ * (ADR 011); there is no Redis. Per-RPC posture:
22
+ * docs/authorization-coverage.md §19.
19
23
  */
20
24
  import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
21
25
 
@@ -22,8 +22,9 @@
22
22
  * phase-transition-only statusAudit bump (heartbeats must not perpetually
23
23
  * re-sort long-running executions above new ones in the recents sidebar).
24
24
  *
25
- * Versus Stigmer Cloud, OSS excludes the Authorize, PublishToRedis, and
26
- * Publish steps (broadcast rides in-memory channels per ADR 011).
25
+ * The chain opens with Authorize (can_edit on the execution); the
26
+ * broadcast rides the in-memory stream broker (ADR 011) — there is no
27
+ * Redis.
27
28
  */
28
29
  import { create, toBinary } from "@bufbuild/protobuf";
29
30
  import { timestampNow } from "@bufbuild/protobuf/wkt";
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Pins the store-fault contract of the workflow instance's loads. The shared
3
+ * targeted-update load (updateVisibility and updateExecutionVisibility): a
4
+ * typed ResourceNotFoundError answers NotFound with the domain's pinned copy
5
+ * (`workflow instance not found: <id>`), and any other store failure is an
6
+ * infrastructure fault answered as a sanitized Internal, never a NotFound that
7
+ * tells a client the instance does not exist (stigmer/stigmer#1345). Create's
8
+ * parent-workflow load reads the workflow through the in-process query
9
+ * client, so the store's typed miss arrives as a Connect NotFound: that
10
+ * answers the pinned `Workflow not found: <id>`, and any other failure an
11
+ * Internal, never a missing parent (stigmer/stigmer#1351).
12
+ *
13
+ * Every surface is reached through the registered handler on an in-process
14
+ * router. The composed suites reach only a real store, which cannot fail
15
+ * selectively, so the updates run against a store whose read throws and
16
+ * create against a parent loader whose read throws. Whatever the surface does
17
+ * not need is untouchable: a load that fails must stop the call before
18
+ * anything past it runs.
19
+ *
20
+ * Out of scope: the NotFound copies themselves (wire contract).
21
+ */
22
+ import type { Client } from "@connectrpc/connect";
23
+ import {
24
+ Code,
25
+ ConnectError,
26
+ createClient,
27
+ createRouterTransport,
28
+ } from "@connectrpc/connect";
29
+ import { describe, expect, it } from "vitest";
30
+
31
+ import { WorkflowInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/command_pb";
32
+ import { WorkflowExecutionVisibility } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/spec_pb";
33
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
34
+
35
+ import { createLogger } from "../../../boot/logger.js";
36
+ import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
37
+ import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
38
+ import {
39
+ errorOf,
40
+ failingStore,
41
+ untouchable,
42
+ } from "../../../pipeline/__tests__/support.js";
43
+ import { INTERNAL_FALLBACK_MESSAGE } from "../../../pipeline/pipeline.js";
44
+ import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
45
+ import { ResourceNotFoundError } from "../../../store/interface.js";
46
+ import type { Store } from "../../../store/interface.js";
47
+
48
+ import { registerWorkflowInstanceServices } from "../controller.js";
49
+ import type { ParentWorkflowLoaderProvider } from "../steps.js";
50
+
51
+ const silentLogger = createLogger({
52
+ level: "error",
53
+ pretty: false,
54
+ write: () => {},
55
+ });
56
+
57
+ const INSTANCE_ID = "win_storefault";
58
+ const WORKFLOW_ID = "wfl_storefault";
59
+
60
+ const NOT_FOUND_COPY = `workflow instance not found: ${INSTANCE_ID}`;
61
+ const LOAD_FAULT_COPY = "failed to load workflow instance";
62
+ const PARENT_NOT_FOUND_COPY = `Workflow not found: ${WORKFLOW_ID}`;
63
+ const PARENT_LOAD_FAULT_COPY = "failed to load parent workflow";
64
+
65
+ const MISSING = (): Error =>
66
+ new ResourceNotFoundError(`workflow_instance/${INSTANCE_ID}`);
67
+ const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
68
+
69
+ /** What the workflow query's `get` answers for a missing and a faulted workflow. */
70
+ const PARENT_MISSING = (): Error =>
71
+ new ConnectError(`Workflow not found: ${WORKFLOW_ID}`, Code.NotFound);
72
+ const PARENT_FAULTED = (): Error =>
73
+ new ConnectError(INTERNAL_FALLBACK_MESSAGE, Code.Internal);
74
+
75
+ /**
76
+ * The registered handlers on an in-process router: the verifier chain stamps
77
+ * the trusted-local caller and the apiresource interceptor the kind, then
78
+ * Authorize and ValidateProto run and the load is the first step that reads
79
+ * the store or the parent.
80
+ */
81
+ function instanceCommand(
82
+ store: Store,
83
+ parentWorkflowLoader: ParentWorkflowLoaderProvider = untouchable(
84
+ "parentWorkflowLoader",
85
+ ),
86
+ ): Client<typeof WorkflowInstanceCommandController> {
87
+ const transport = createRouterTransport(
88
+ (router) => {
89
+ registerWorkflowInstanceServices(router, {
90
+ store,
91
+ logger: silentLogger,
92
+ authorizer: newPermissiveSingleTeamAuthorizer(),
93
+ authorizationLifecycle: undefined,
94
+ parentWorkflowLoader,
95
+ listReadScope: undefined,
96
+ });
97
+ },
98
+ {
99
+ router: {
100
+ interceptors: [
101
+ createVerifierChainInterceptor([], [], silentLogger),
102
+ createApiResourceInterceptor(),
103
+ ],
104
+ },
105
+ },
106
+ );
107
+ return createClient(WorkflowInstanceCommandController, transport);
108
+ }
109
+
110
+ describe.each([
111
+ {
112
+ surface: "updateVisibility — LoadInstanceForVisibilityUpdate",
113
+ call: (store: Store) =>
114
+ instanceCommand(store).updateVisibility({
115
+ resourceId: INSTANCE_ID,
116
+ visibility: ApiResourceVisibility.visibility_private,
117
+ }),
118
+ },
119
+ {
120
+ surface:
121
+ "updateExecutionVisibility — LoadInstanceForExecutionVisibilityUpdate",
122
+ call: (store: Store) =>
123
+ instanceCommand(store).updateExecutionVisibility({
124
+ resourceId: INSTANCE_ID,
125
+ executionVisibility: WorkflowExecutionVisibility.private,
126
+ }),
127
+ },
128
+ ])("$surface", ({ call }) => {
129
+ it("a missing workflow instance answers NotFound with the domain's copy", async () => {
130
+ const error = await errorOf(() => call(failingStore(MISSING())));
131
+
132
+ expect(error.code).toBe(Code.NotFound);
133
+ expect(error.rawMessage).toBe(NOT_FOUND_COPY);
134
+ });
135
+
136
+ it("any other store failure answers a sanitized Internal", async () => {
137
+ const error = await errorOf(() => call(failingStore(LOCKED())));
138
+
139
+ expect(error.code).toBe(Code.Internal);
140
+ expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
141
+ });
142
+ });
143
+
144
+ describe("create — LoadParentWorkflow", () => {
145
+ function surfaceError(parentError: Error): Promise<ConnectError> {
146
+ const failingParent: ParentWorkflowLoaderProvider = () => ({
147
+ get: () => Promise.reject(parentError),
148
+ });
149
+ return errorOf(() =>
150
+ instanceCommand(untouchable("store"), failingParent).create({
151
+ apiVersion: "agentic.stigmer.ai/v1",
152
+ kind: "WorkflowInstance",
153
+ metadata: { name: "Store Fault Instance", org: "org_storefault" },
154
+ spec: { workflowId: WORKFLOW_ID },
155
+ }),
156
+ );
157
+ }
158
+
159
+ it("a missing parent workflow answers NotFound with the pinned copy", async () => {
160
+ const error = await surfaceError(PARENT_MISSING());
161
+
162
+ expect(error.code).toBe(Code.NotFound);
163
+ expect(error.rawMessage).toBe(PARENT_NOT_FOUND_COPY);
164
+ });
165
+
166
+ it("any other parent-load failure answers a sanitized Internal", async () => {
167
+ const error = await surfaceError(PARENT_FAULTED());
168
+
169
+ expect(error.code).toBe(Code.Internal);
170
+ expect(error.rawMessage).toBe(PARENT_LOAD_FAULT_COPY);
171
+ });
172
+ });
@@ -13,8 +13,8 @@
13
13
  *
14
14
  * Pipeline per RPC mirrors the Go step chains character-for-character.
15
15
  * Proven by workflowinstance.conformance.test.ts
16
- * (CONFORMANCE_TARGET=local) and the family test
17
- * ../workflow/__tests__/workflow.test.ts (the mutual edge makes the two
16
+ * (CONFORMANCE_TARGET=local), __tests__/store-faults.test.ts and the family
17
+ * test ../workflow/__tests__/workflow.test.ts (the mutual edge makes the two
18
18
  * domains one testable unit).
19
19
  */
20
20
  import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
@@ -104,6 +104,7 @@ import {
104
104
  newValidateVisibilityStep,
105
105
  newValidateVisibilityUpdateStep,
106
106
  } from "../../pipeline/steps/validate-visibility.js";
107
+ import { ResourceNotFoundError } from "../../store/interface.js";
107
108
  import type { Store } from "../../store/interface.js";
108
109
  import { workflowInstanceSearchExtractor } from "./search-extractor.js";
109
110
  import {
@@ -573,9 +574,10 @@ function newSetInstanceExecutionVisibilityStep(): PipelineStep<UpdateExecutionVi
573
574
  // ---------------------------------------------------------------------------
574
575
 
575
576
  /**
576
- * Loads the instance by resource_id; ANY load failure → NotFound. The
577
- * accessor keeps the input shape compiler-checked (both targeted-update
578
- * inputs carry resource_id, but the compiler should prove it, not a cast).
577
+ * Loads the instance by resource_id; a missing one answers NotFound, a store
578
+ * fault Internal. The accessor keeps the input shape compiler-checked (both
579
+ * targeted-update inputs carry resource_id, but the compiler should prove it,
580
+ * not a cast).
579
581
  */
580
582
  function newLoadInstanceStep<Desc extends DescMessage>(
581
583
  store: Store,
@@ -594,8 +596,11 @@ function newLoadInstanceStep<Desc extends DescMessage>(
594
596
  resourceId,
595
597
  WorkflowInstanceSchema,
596
598
  );
597
- } catch {
598
- throw notFoundError("workflow instance", resourceId);
599
+ } catch (error) {
600
+ if (error instanceof ResourceNotFoundError) {
601
+ throw notFoundError("workflow instance", resourceId);
602
+ }
603
+ throw internalError(error, "failed to load workflow instance");
599
604
  }
600
605
  ctx.set(instanceKey, instance);
601
606
  },
@@ -8,7 +8,7 @@
8
8
  * the family test ../workflow/__tests__/workflow.test.ts (the mutual edge
9
9
  * makes the two domains one testable unit).
10
10
  */
11
- import { ConnectError } from "@connectrpc/connect";
11
+ import { Code, ConnectError } from "@connectrpc/connect";
12
12
  import type { DescMessage } from "@bufbuild/protobuf";
13
13
 
14
14
  import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
@@ -55,9 +55,11 @@ export type ParentWorkflowLoaderProvider = () => ParentWorkflowLoader;
55
55
 
56
56
  /**
57
57
  * LoadParentWorkflow — create.go: loads the workflow the instance
58
- * materializes and stashes it for the same-org rule. ANY load failure maps
59
- * to NotFound("Workflow", id) — Go folds every client error into that arm
60
- * (capital-W copy is the wire contract).
58
+ * materializes and stashes it for the same-org rule. The workflow is read
59
+ * through the in-process query client, so a missing one arrives as a Connect
60
+ * NotFound and answers NotFound("Workflow", id) (the capital-W copy is the
61
+ * wire contract); any other failure is a fault answered as Internal, never a
62
+ * missing parent.
61
63
  */
62
64
  export function newLoadParentWorkflowStep(
63
65
  loaderProvider: ParentWorkflowLoaderProvider,
@@ -74,12 +76,14 @@ export function newLoadParentWorkflowStep(
74
76
  try {
75
77
  parent = await loaderProvider().get(workflowId);
76
78
  } catch (error) {
77
- logger.warn("Parent workflow not found", {
78
- error:
79
- error instanceof ConnectError ? error.rawMessage : String(error),
80
- workflowId,
81
- });
82
- throw notFoundError("Workflow", workflowId);
79
+ if (error instanceof ConnectError && error.code === Code.NotFound) {
80
+ logger.warn("Parent workflow not found", {
81
+ error: error.rawMessage,
82
+ workflowId,
83
+ });
84
+ throw notFoundError("Workflow", workflowId);
85
+ }
86
+ throw internalError(error, "failed to load parent workflow");
83
87
  }
84
88
 
85
89
  ctx.set(PARENT_WORKFLOW_KEY, parent);
@@ -13,10 +13,18 @@
13
13
  * in-process origin, and never a wire `machine` account — the one row
14
14
  * where the two predicates part, pinned so a step trusting server-composed
15
15
  * state can never be widened to the wire by picking the wrong one.
16
+ *
17
+ * And pins `isFirstPartyHumanOperator` (stigmer#1387), the allow-list
18
+ * memory recall and the person's declared preferences read: a wire `user`
19
+ * whose bearer is not a PlatformClient user token. Every other class, an
20
+ * in-process `user` (the open-source schedule fire's shape) and a
21
+ * platform token naming a PlatformClient are excluded; another issuer's
22
+ * token carrying the same claim is not a platform token (stigmer#1312).
16
23
  */
17
24
  import { describe, expect, it } from "vitest";
18
25
 
19
26
  import {
27
+ isFirstPartyHumanOperator,
20
28
  isPlatformPipelineCaller,
21
29
  isServerComposedRequest,
22
30
  } from "../identity.js";
@@ -103,3 +111,77 @@ describe("isServerComposedRequest", () => {
103
111
  );
104
112
  });
105
113
  });
114
+
115
+ /** A JWT carrying `payload`, unsigned: the predicate reads, never verifies. */
116
+ function jwtWith(payload: Record<string, unknown>): string {
117
+ const segment = (value: object) =>
118
+ Buffer.from(JSON.stringify(value)).toString("base64url");
119
+ return `${segment({ alg: "RS256", typ: "JWT" })}.${segment(payload)}.signature`;
120
+ }
121
+
122
+ describe("isFirstPartyHumanOperator", () => {
123
+ it("is true for a wire user — an OIDC login, an API key, the laptop's operator", () => {
124
+ expect(isFirstPartyHumanOperator(caller({}))).toBe(true);
125
+ expect(isFirstPartyHumanOperator(caller({ origin: "wire" }))).toBe(true);
126
+ expect(
127
+ isFirstPartyHumanOperator(
128
+ caller({
129
+ issuer: "https://issuer.example",
130
+ rawToken: jwtWith({ iss: "https://issuer.example", sub: "carol" }),
131
+ }),
132
+ ),
133
+ ).toBe(true);
134
+ });
135
+
136
+ it("is false for every other class, a composition's own included", () => {
137
+ for (const callerClass of [
138
+ "machine",
139
+ "runner",
140
+ "internal",
141
+ "guest",
142
+ "channel",
143
+ "schedule",
144
+ "sandbox",
145
+ ]) {
146
+ expect(
147
+ isFirstPartyHumanOperator(caller({ callerClass })),
148
+ callerClass,
149
+ ).toBe(false);
150
+ }
151
+ });
152
+
153
+ it("is false for a user the server composed a request for (the schedule fire)", () => {
154
+ expect(
155
+ isFirstPartyHumanOperator(
156
+ caller({ callerClass: "user", origin: "in-process" }),
157
+ ),
158
+ ).toBe(false);
159
+ });
160
+
161
+ it("is false for a PlatformClient user token, and true when another issuer stamps the claim", () => {
162
+ expect(
163
+ isFirstPartyHumanOperator(
164
+ caller({
165
+ issuer: "stigmer",
166
+ rawToken: jwtWith({
167
+ iss: "stigmer",
168
+ sub: "ida_x",
169
+ platform_client_id: "pc_1",
170
+ }),
171
+ }),
172
+ ),
173
+ ).toBe(false);
174
+ expect(
175
+ isFirstPartyHumanOperator(
176
+ caller({
177
+ issuer: "https://issuer.example",
178
+ rawToken: jwtWith({
179
+ iss: "https://issuer.example",
180
+ sub: "ida_x",
181
+ platform_client_id: "pc_1",
182
+ }),
183
+ }),
184
+ ),
185
+ ).toBe(true);
186
+ });
187
+ });
@@ -12,6 +12,11 @@
12
12
  * HandlerContext.values (never ambient state — the every-dependency-
13
13
  * explicit doctrine).
14
14
  */
15
+ import { USER_TOKEN_CLAIMS } from "../domain/platformclient/constants.js";
16
+ import {
17
+ decodeVerifiedPlatformTokenPayload,
18
+ stringClaim,
19
+ } from "../platformtoken/envelope.js";
15
20
 
16
21
  /**
17
22
  * The caller-class discriminant. OSS knows user / machine / runner plus
@@ -152,3 +157,45 @@ export function isPlatformPipelineCaller(caller: CallerIdentity): boolean {
152
157
  export function isServerComposedRequest(caller: CallerIdentity): boolean {
153
158
  return caller.callerClass === "internal" || caller.origin === "in-process";
154
159
  }
160
+
161
+ /**
162
+ * Whether `caller` is a FIRST-PARTY HUMAN OPERATOR: a person at the
163
+ * console, the CLI or an SDK, speaking for themselves on the wire — the
164
+ * TS rendering of the Java RequestCallerIdentity.isFirstPartyHumanOperator
165
+ * (not machine, not impersonated, no token_type, not a PlatformClient user
166
+ * token). Stated once for every consumer that must tell the person from a
167
+ * lane acting near them: memory recall and the person's declared
168
+ * preferences on execution create (domain/agentexecution/create-steps.ts).
169
+ *
170
+ * An ALLOW-list, so a lane this module has never heard of is excluded by
171
+ * construction: the class must be `user` (every human verifier stamps it —
172
+ * OIDC, API key, the cloud's direct and federated logins; runners, the
173
+ * cloud's token lanes and the platform's own pipelines each carry their
174
+ * own class), the request must come from the wire (the open-source
175
+ * schedule fire acts as its creator with class `user` but rides the
176
+ * in-process transport, which is exactly what `isServerComposedRequest`
177
+ * reads), and the bearer must not be a PlatformClient user token (a
178
+ * person, but spoken for by a third-party client — DD-002 D4 as amended).
179
+ */
180
+ export function isFirstPartyHumanOperator(caller: CallerIdentity): boolean {
181
+ return (
182
+ caller.callerClass === "user" &&
183
+ !isServerComposedRequest(caller) &&
184
+ !carriesPlatformClientClaim(caller.rawToken)
185
+ );
186
+ }
187
+
188
+ /**
189
+ * Whether the bearer is a platform token naming a PlatformClient. Anything
190
+ * that is not a platform token (no token, an API key, another issuer's
191
+ * JWT) is not one, whatever claims it carries (stigmer#1312). Read from
192
+ * the raw token, never from `platformClientId`, which is audit provenance
193
+ * set by one verifier and not a guard vocabulary (see its field doc).
194
+ */
195
+ export function carriesPlatformClientClaim(rawToken: string): boolean {
196
+ const payload = decodeVerifiedPlatformTokenPayload(rawToken);
197
+ return (
198
+ payload !== undefined &&
199
+ stringClaim(payload, USER_TOKEN_CLAIMS.platformClientId) !== undefined
200
+ );
201
+ }
@@ -20,9 +20,8 @@
20
20
  *
21
21
  * The scan parses each module with the TypeScript compiler API rather than a
22
22
  * regular expression, because a catch block's extent (nested braces, a log
23
- * line before the throw) is a syntax question. `PENDING` is the list of known
24
- * blind sites still to be fixed; the scan must equal it exactly, so a new
25
- * blind site fails here, and so does a fixed site still listed.
23
+ * line before the throw) is a syntax question. The scan must find no blind
24
+ * site at all, so a new one fails here with its module and line.
26
25
  */
27
26
  import { readFileSync, readdirSync } from "node:fs";
28
27
  import path from "node:path";
@@ -34,19 +33,6 @@ import { describe, expect, it } from "vitest";
34
33
  const HERE = path.dirname(fileURLToPath(import.meta.url));
35
34
  const SRC = path.resolve(HERE, "../..");
36
35
 
37
- /** Known blind sites by module (relative to `src`), with their count. */
38
- const PENDING: ReadonlyMap<string, number> = new Map([
39
- // Store loads, stigmer/stigmer#1345 (fixed domain by domain).
40
- ["domain/plugin/controller.ts", 1],
41
- ["domain/skill/controller.ts", 1],
42
- ["domain/workflow/controller.ts", 1],
43
- ["domain/workflowinstance/controller.ts", 1],
44
- // Parent loads through the in-process chain that fold every RPC error,
45
- // validation included, stigmer/stigmer#1351.
46
- ["domain/agentinstance/steps.ts", 1],
47
- ["domain/workflowinstance/steps.ts", 1],
48
- ]);
49
-
50
36
  interface BlindSite {
51
37
  readonly module: string;
52
38
  readonly line: number;
@@ -258,7 +244,7 @@ describe("the blind-NotFound rule", () => {
258
244
  });
259
245
 
260
246
  describe("the server source", () => {
261
- it("has no blind NotFound beyond the pending list, and the list names only live sites", () => {
247
+ it("has no blind NotFound", () => {
262
248
  const sources = serverSources();
263
249
  const constructors = notFoundConstructors(sources);
264
250
  const found = new Map<string, number[]>();
@@ -278,8 +264,6 @@ describe("the server source", () => {
278
264
  const where = [...found]
279
265
  .flatMap(([module, lines]) => lines.map((line) => `${module}:${line}`))
280
266
  .join("\n");
281
- expect(counts, `blind NotFound sites found:\n${where}`).toEqual(
282
- Object.fromEntries(PENDING),
283
- );
267
+ expect(counts, `blind NotFound sites found:\n${where}`).toEqual({});
284
268
  });
285
269
  });