@stigmer/runner 3.15.3-dev.20260916211208 → 3.16.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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/activities/generate-session-subject.d.ts +25 -4
  4. package/dist/activities/generate-session-subject.js +17 -7
  5. package/dist/activities/generate-session-subject.js.map +1 -1
  6. package/dist/client/stigmer-client.d.ts +17 -6
  7. package/dist/client/stigmer-client.js +47 -14
  8. package/dist/client/stigmer-client.js.map +1 -1
  9. package/dist/config.d.ts +4 -1
  10. package/dist/config.js +4 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/interceptors/run-credential-activity.d.ts +28 -0
  13. package/dist/interceptors/run-credential-activity.js +56 -0
  14. package/dist/interceptors/run-credential-activity.js.map +1 -0
  15. package/dist/runner-manager.js +9 -3
  16. package/dist/runner-manager.js.map +1 -1
  17. package/dist/shared/activity-input.d.ts +9 -0
  18. package/dist/shared/activity-input.js +17 -0
  19. package/dist/shared/activity-input.js.map +1 -1
  20. package/dist/shared/channel-attachment.d.ts +8 -5
  21. package/dist/shared/channel-attachment.js +10 -6
  22. package/dist/shared/channel-attachment.js.map +1 -1
  23. package/dist/shared/filereview/progress.d.ts +13 -0
  24. package/dist/shared/filereview/progress.js +13 -0
  25. package/dist/shared/filereview/progress.js.map +1 -1
  26. package/dist/shared/memory-attachment.d.ts +16 -9
  27. package/dist/shared/memory-attachment.js +18 -10
  28. package/dist/shared/memory-attachment.js.map +1 -1
  29. package/dist/shared/run-credential-store.d.ts +32 -0
  30. package/dist/shared/run-credential-store.js +39 -0
  31. package/dist/shared/run-credential-store.js.map +1 -0
  32. package/dist/shared/run-credential.d.ts +61 -0
  33. package/dist/shared/run-credential.js +68 -0
  34. package/dist/shared/run-credential.js.map +1 -0
  35. package/dist/shared/synthesized-attachment.d.ts +30 -3
  36. package/dist/shared/synthesized-attachment.js +26 -0
  37. package/dist/shared/synthesized-attachment.js.map +1 -1
  38. package/dist/worker.js +10 -3
  39. package/dist/worker.js.map +1 -1
  40. package/dist/workflow-source.d.ts +10 -0
  41. package/dist/workflow-source.js +12 -0
  42. package/dist/workflow-source.js.map +1 -1
  43. package/dist/workflows/execute-from-execution.d.ts +15 -0
  44. package/dist/workflows/execute-from-execution.js +11 -0
  45. package/dist/workflows/execute-from-execution.js.map +1 -1
  46. package/dist/workflows/index.d.ts +5 -3
  47. package/dist/workflows/index.js +5 -3
  48. package/dist/workflows/index.js.map +1 -1
  49. package/dist/workflows/interceptors/run-credential.d.ts +34 -0
  50. package/dist/workflows/interceptors/run-credential.js +71 -0
  51. package/dist/workflows/interceptors/run-credential.js.map +1 -0
  52. package/package.json +4 -4
  53. package/src/__tests__/run-credential-e2e.test.ts +233 -0
  54. package/src/__tests__/run-credential-registration.test.ts +80 -0
  55. package/src/activities/__tests__/generate-session-subject-wire.test.ts +119 -0
  56. package/src/activities/generate-session-subject.ts +29 -4
  57. package/src/client/__tests__/stigmer-client.test.ts +92 -0
  58. package/src/client/stigmer-client.ts +48 -14
  59. package/src/config.ts +4 -1
  60. package/src/interceptors/__tests__/run-credential-activity.test.ts +133 -0
  61. package/src/interceptors/run-credential-activity.ts +67 -0
  62. package/src/runner-manager.ts +13 -3
  63. package/src/shared/__tests__/channel-attachment.test.ts +12 -0
  64. package/src/shared/__tests__/memory-attachment.test.ts +17 -0
  65. package/src/shared/__tests__/run-credential.test.ts +111 -0
  66. package/src/shared/activity-input.ts +21 -0
  67. package/src/shared/channel-attachment.ts +14 -6
  68. package/src/shared/filereview/progress.ts +13 -0
  69. package/src/shared/memory-attachment.ts +22 -10
  70. package/src/shared/run-credential-store.ts +45 -0
  71. package/src/shared/run-credential.ts +70 -0
  72. package/src/shared/synthesized-attachment.ts +38 -3
  73. package/src/worker.ts +15 -3
  74. package/src/workflow-source.ts +15 -0
  75. package/src/workflows/__tests__/run-credential-interceptor.test.ts +178 -0
  76. package/src/workflows/execute-from-execution.ts +17 -0
  77. package/src/workflows/index.ts +5 -3
  78. package/src/workflows/interceptors/run-credential.ts +101 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The workflow side of the run credential (`interceptors/run-credential.ts`):
3
+ * the stamp on both activity kinds, the gate by workflow type, the
4
+ * credential-less pass-through, and the fence that every gated type is a
5
+ * real export alias of the workflow barrel.
6
+ *
7
+ * `workflowInfo()` is the only sandbox API the interceptor reads; it is
8
+ * replaced here so the factory can run outside an isolate. Everything else
9
+ * from `@temporalio/workflow` is the real module, imported once for the
10
+ * barrel fence.
11
+ */
12
+
13
+ import { beforeEach, describe, expect, it, vi } from "vitest";
14
+ import { defaultPayloadConverter } from "@temporalio/common";
15
+ import type {
16
+ ActivityInput,
17
+ LocalActivityInput,
18
+ WorkflowExecuteInput,
19
+ } from "@temporalio/workflow";
20
+
21
+ let workflowType = "";
22
+
23
+ vi.mock("@temporalio/workflow", async (importOriginal) => {
24
+ const original =
25
+ await importOriginal<typeof import("@temporalio/workflow")>();
26
+ return { ...original, workflowInfo: () => ({ workflowType }) };
27
+ });
28
+
29
+ import {
30
+ RUN_WORKFLOW_TYPES,
31
+ interceptors,
32
+ } from "../interceptors/run-credential.js";
33
+ import { EXECUTE_FROM_EXECUTION_WORKFLOW_TYPE } from "../execute-from-execution.js";
34
+ import {
35
+ RUN_CREDENTIAL_HEADER,
36
+ RUN_CREDENTIAL_INPUT_KEY,
37
+ } from "../../shared/run-credential.js";
38
+ import * as barrel from "../index.js";
39
+
40
+ /** Drive one workflow run through the factory: its inbound execute, then one schedule of each kind. */
41
+ async function run(type: string, workflowArg: unknown) {
42
+ workflowType = type;
43
+ const { inbound, outbound } = interceptors();
44
+ const executeInput: WorkflowExecuteInput = {
45
+ args: [workflowArg],
46
+ headers: {},
47
+ };
48
+ await inbound![0]!.execute!(executeInput, async () => undefined);
49
+
50
+ const seen: { activity?: ActivityInput; local?: LocalActivityInput } = {};
51
+ const activityInput = {
52
+ activityType: "EmitWorkflowEvents",
53
+ args: ["wex_1"],
54
+ headers: {},
55
+ options: {},
56
+ seq: 1,
57
+ } as unknown as ActivityInput;
58
+ const localInput = {
59
+ activityType: "PromoteTaskOutput",
60
+ args: ["out", "wex_1"],
61
+ headers: {},
62
+ options: {},
63
+ seq: 2,
64
+ } as unknown as LocalActivityInput;
65
+ await outbound![0]!.scheduleActivity!(activityInput, async (input) => {
66
+ seen.activity = input;
67
+ return undefined;
68
+ });
69
+ await outbound![0]!.scheduleLocalActivity!(localInput, async (input) => {
70
+ seen.local = input;
71
+ return undefined;
72
+ });
73
+ return seen;
74
+ }
75
+
76
+ function credentialIn(headers: ActivityInput["headers"] | undefined): unknown {
77
+ const payload = headers?.[RUN_CREDENTIAL_HEADER];
78
+ return payload === undefined
79
+ ? undefined
80
+ : defaultPayloadConverter.fromPayload(payload);
81
+ }
82
+
83
+ describe("run-credential workflow interceptors", () => {
84
+ beforeEach(() => {
85
+ workflowType = "";
86
+ });
87
+
88
+ it("stamps the run workflow's credential onto a remote activity and a local activity alike", async () => {
89
+ const seen = await run(EXECUTE_FROM_EXECUTION_WORKFLOW_TYPE, {
90
+ execution_id: "wex_1",
91
+ [RUN_CREDENTIAL_INPUT_KEY]: "cred-run",
92
+ });
93
+ expect(credentialIn(seen.activity?.headers)).toBe("cred-run");
94
+ expect(credentialIn(seen.local?.headers)).toBe("cred-run");
95
+ });
96
+
97
+ it("keeps the other headers a schedule already carries", async () => {
98
+ workflowType = EXECUTE_FROM_EXECUTION_WORKFLOW_TYPE;
99
+ const { inbound, outbound } = interceptors();
100
+ await inbound![0]!.execute!(
101
+ { args: [{ [RUN_CREDENTIAL_INPUT_KEY]: "cred-run" }], headers: {} },
102
+ async () => undefined,
103
+ );
104
+ const other = defaultPayloadConverter.toPayload("trace");
105
+ let stamped: ActivityInput | undefined;
106
+ await outbound![0]!.scheduleActivity!(
107
+ {
108
+ activityType: "X",
109
+ args: [],
110
+ headers: { "_tracer-data": other },
111
+ options: {},
112
+ seq: 1,
113
+ } as unknown as ActivityInput,
114
+ async (input) => {
115
+ stamped = input;
116
+ return undefined;
117
+ },
118
+ );
119
+ expect(stamped?.headers["_tracer-data"]).toBe(other);
120
+ expect(credentialIn(stamped?.headers)).toBe("cred-run");
121
+ });
122
+
123
+ it("stamps nothing when the run workflow's input carries no credential — an older server's dispatch replays byte for byte", async () => {
124
+ const seen = await run(EXECUTE_FROM_EXECUTION_WORKFLOW_TYPE, {
125
+ execution_id: "wex_1",
126
+ });
127
+ expect(seen.activity?.headers).toEqual({});
128
+ expect(seen.local?.headers).toEqual({});
129
+ });
130
+
131
+ it("stamps nothing for the connect workflow even though its input carries the same key — that token is not a run credential", async () => {
132
+ const seen = await run("stigmer/mcp-server/connect", {
133
+ mcp_server_id: "mcp_1",
134
+ [RUN_CREDENTIAL_INPUT_KEY]: "connect-token",
135
+ });
136
+ expect(seen.activity?.headers).toEqual({});
137
+ expect(seen.local?.headers).toEqual({});
138
+ });
139
+
140
+ it("keeps each workflow run's credential on its own factory instance", async () => {
141
+ workflowType = EXECUTE_FROM_EXECUTION_WORKFLOW_TYPE;
142
+ const first = interceptors();
143
+ const second = interceptors();
144
+ await first.inbound![0]!.execute!(
145
+ { args: [{ [RUN_CREDENTIAL_INPUT_KEY]: "cred-1" }], headers: {} },
146
+ async () => undefined,
147
+ );
148
+ await second.inbound![0]!.execute!(
149
+ { args: [{ execution_id: "wex_2" }], headers: {} },
150
+ async () => undefined,
151
+ );
152
+ let fromSecond: ActivityInput | undefined;
153
+ await second.outbound![0]!.scheduleActivity!(
154
+ {
155
+ activityType: "X",
156
+ args: [],
157
+ headers: {},
158
+ options: {},
159
+ seq: 1,
160
+ } as unknown as ActivityInput,
161
+ async (input) => {
162
+ fromSecond = input;
163
+ return undefined;
164
+ },
165
+ );
166
+ expect(fromSecond?.headers).toEqual({});
167
+ });
168
+
169
+ it("gates on workflow types that are export aliases of the barrel — the list cannot name a type that does not exist", () => {
170
+ expect(RUN_WORKFLOW_TYPES).toEqual([EXECUTE_FROM_EXECUTION_WORKFLOW_TYPE]);
171
+ for (const type of RUN_WORKFLOW_TYPES) {
172
+ expect(
173
+ Object.keys(barrel),
174
+ `"${type}" must be exported by workflows/index.ts`,
175
+ ).toContain(type);
176
+ }
177
+ });
178
+ });
@@ -47,6 +47,16 @@ const hydrateProxy = proxyActivities<HydrateActivities>({
47
47
  // Workflow Input Type (Temporal wire contract with Java/Go orchestrators)
48
48
  // ─────────────────────────────────────────────────────────────────────────────
49
49
 
50
+ /**
51
+ * The Temporal workflow type this function is exported as in `index.ts` — the
52
+ * server's `CHILD_WORKFLOW_TYPE`, byte-pinned on both sides. Named here so
53
+ * the run-credential interceptor can gate on it without a second literal; a
54
+ * test pins it to the barrel's export alias (an ES2022 export alias must be a
55
+ * literal, so the barrel cannot import this).
56
+ */
57
+ export const EXECUTE_FROM_EXECUTION_WORKFLOW_TYPE =
58
+ "stigmer/workflow/execute-from-execution";
59
+
50
60
  export interface ExecuteFromExecutionInput {
51
61
  execution_id: string;
52
62
  workflow_instance_id: string;
@@ -55,6 +65,13 @@ export interface ExecuteFromExecutionInput {
55
65
  callback_token?: Uint8Array | null;
56
66
  invoker_identity_account_id?: string;
57
67
  recovery_mode?: boolean;
68
+ /**
69
+ * The run credential the server minted for this execution at dispatch —
70
+ * absent from an older server's dispatch. This workflow never reads it: the
71
+ * interceptor in `interceptors/run-credential.ts` lifts it onto every
72
+ * activity this workflow schedules, as a header (`shared/run-credential.ts`).
73
+ */
74
+ execution_context_token?: string;
58
75
  }
59
76
 
60
77
  // ─────────────────────────────────────────────────────────────────────────────
@@ -13,9 +13,11 @@
13
13
  * us map TypeScript function names to the slash-delimited Temporal
14
14
  * workflow type names that the backend expects.
15
15
  *
16
- * OTel workflow interceptors are registered at bundle time — not
17
- * imported here. Pre-built bundles (scripts/bundle-slim.mjs) bake them
18
- * in; the runtime-bundling fallback passes them to the bundler (see
16
+ * Workflow interceptors are registered at bundle time — not imported
17
+ * here: the runner's own run-credential interceptor
18
+ * (./interceptors/run-credential.ts, always) and OTel's (when the host
19
+ * configures the sink). Pre-built bundles (scripts/bundle-slim.mjs) bake
20
+ * them in; the runtime-bundling fallback passes them to the bundler (see
19
21
  * src/workflow-source.ts).
20
22
  */
21
23
 
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Workflow-side interceptors that carry a run's credential from the workflow
3
+ * input to every activity the workflow schedules — as a Temporal activity
4
+ * header, the SDK's channel for context that crosses the isolate boundary
5
+ * without being anybody's argument (`shared/run-credential.ts` has the
6
+ * whole story; `interceptors/run-credential-activity.ts` is the other end).
7
+ *
8
+ * Gated by WORKFLOW TYPE, not by input shape. The connect workflow's input
9
+ * carries the same key for a different token — a clocked decrypt-lane token
10
+ * bound to a synthetic id that no verifier admits as a run — and stamping it
11
+ * onto that lane's activities would present it where it does not belong.
12
+ * The gate is the positive list below: the workflow types whose input
13
+ * carries a RUN credential. One entry today; a test pins each entry to an
14
+ * export of `workflows/index.ts`, so the list cannot name a type that does
15
+ * not exist.
16
+ *
17
+ * Per-run state lives on this factory's closure: the SDK instantiates the
18
+ * factory once per workflow run, which is the documented contract. The
19
+ * header is a pure function of the input, so replay recomputes exactly what
20
+ * was recorded, and a credential-less input stamps nothing — an in-flight
21
+ * run from before the credential existed replays byte for byte.
22
+ *
23
+ * TEMPORAL SANDBOX: this file is bundled into the workflow isolate. No Node
24
+ * built-ins. It is baked into the pre-built bundle by
25
+ * `scripts/bundle-slim.mjs` and passed to the runtime bundler by both worker
26
+ * roots (`src/workflow-source.ts` names it beside the OTel module).
27
+ */
28
+
29
+ import { workflowInfo } from "@temporalio/workflow";
30
+ import type {
31
+ ActivityInput,
32
+ LocalActivityInput,
33
+ Next,
34
+ WorkflowExecuteInput,
35
+ WorkflowInboundCallsInterceptor,
36
+ WorkflowInterceptorsFactory,
37
+ WorkflowOutboundCallsInterceptor,
38
+ } from "@temporalio/workflow";
39
+ import { defaultPayloadConverter } from "@temporalio/common";
40
+
41
+ import {
42
+ RUN_CREDENTIAL_HEADER,
43
+ readRunCredentialFromInput,
44
+ } from "../../shared/run-credential.js";
45
+ import { EXECUTE_FROM_EXECUTION_WORKFLOW_TYPE } from "../execute-from-execution.js";
46
+
47
+ /**
48
+ * The workflow types whose input carries a run credential. Byte-pinned wire
49
+ * names; each must be an export alias of `workflows/index.ts`.
50
+ */
51
+ export const RUN_WORKFLOW_TYPES: readonly string[] = [
52
+ EXECUTE_FROM_EXECUTION_WORKFLOW_TYPE,
53
+ ];
54
+
55
+ export const interceptors: WorkflowInterceptorsFactory = () => {
56
+ let credential: string | undefined;
57
+
58
+ const inbound: WorkflowInboundCallsInterceptor = {
59
+ async execute(
60
+ input: WorkflowExecuteInput,
61
+ next: Next<WorkflowInboundCallsInterceptor, "execute">,
62
+ ) {
63
+ if (RUN_WORKFLOW_TYPES.includes(workflowInfo().workflowType)) {
64
+ credential = readRunCredentialFromInput(input.args[0]);
65
+ }
66
+ return next(input);
67
+ },
68
+ };
69
+
70
+ const outbound: WorkflowOutboundCallsInterceptor = {
71
+ async scheduleActivity(
72
+ input: ActivityInput,
73
+ next: Next<WorkflowOutboundCallsInterceptor, "scheduleActivity">,
74
+ ) {
75
+ return next(stamped(input));
76
+ },
77
+ async scheduleLocalActivity(
78
+ input: LocalActivityInput,
79
+ next: Next<WorkflowOutboundCallsInterceptor, "scheduleLocalActivity">,
80
+ ) {
81
+ return next(stamped(input));
82
+ },
83
+ };
84
+
85
+ function stamped<T extends { readonly headers: ActivityInput["headers"] }>(
86
+ input: T,
87
+ ): T {
88
+ if (credential === undefined) {
89
+ return input;
90
+ }
91
+ return {
92
+ ...input,
93
+ headers: {
94
+ ...input.headers,
95
+ [RUN_CREDENTIAL_HEADER]: defaultPayloadConverter.toPayload(credential),
96
+ },
97
+ };
98
+ }
99
+
100
+ return { inbound: [inbound], outbound: [outbound] };
101
+ };