@stigmer/server 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 (152) hide show
  1. package/dist/authorization/lane-admission.d.ts +50 -0
  2. package/dist/authorization/lane-admission.d.ts.map +1 -0
  3. package/dist/authorization/lane-admission.js +13 -0
  4. package/dist/authorization/lane-admission.js.map +1 -0
  5. package/dist/authorization/schedule-fire-caller.d.ts +2 -2
  6. package/dist/authorization/schedule-fire-caller.d.ts.map +1 -1
  7. package/dist/authorization/schedule-fire-caller.js +8 -8
  8. package/dist/authorization/schedule-fire-caller.js.map +1 -1
  9. package/dist/boot/compose.d.ts.map +1 -1
  10. package/dist/boot/compose.js +66 -14
  11. package/dist/boot/compose.js.map +1 -1
  12. package/dist/boot/inprocess.d.ts.map +1 -1
  13. package/dist/boot/inprocess.js +6 -1
  14. package/dist/boot/inprocess.js.map +1 -1
  15. package/dist/domain/executioncontext/resolve-values-for-caller.d.ts +33 -14
  16. package/dist/domain/executioncontext/resolve-values-for-caller.d.ts.map +1 -1
  17. package/dist/domain/executioncontext/resolve-values-for-caller.js +32 -8
  18. package/dist/domain/executioncontext/resolve-values-for-caller.js.map +1 -1
  19. package/dist/domain/identityaccount/actor.d.ts +33 -18
  20. package/dist/domain/identityaccount/actor.d.ts.map +1 -1
  21. package/dist/domain/identityaccount/actor.js +20 -3
  22. package/dist/domain/identityaccount/actor.js.map +1 -1
  23. package/dist/domain/identityaccount/resolve.d.ts +19 -0
  24. package/dist/domain/identityaccount/resolve.d.ts.map +1 -1
  25. package/dist/domain/identityaccount/resolve.js +12 -0
  26. package/dist/domain/identityaccount/resolve.js.map +1 -1
  27. package/dist/domain/mcpserver/connect-execution-id.d.ts +5 -0
  28. package/dist/domain/mcpserver/connect-execution-id.d.ts.map +1 -0
  29. package/dist/domain/mcpserver/connect-execution-id.js +32 -0
  30. package/dist/domain/mcpserver/connect-execution-id.js.map +1 -0
  31. package/dist/domain/mcpserver/connect.d.ts +28 -2
  32. package/dist/domain/mcpserver/connect.d.ts.map +1 -1
  33. package/dist/domain/mcpserver/connect.js +27 -7
  34. package/dist/domain/mcpserver/connect.js.map +1 -1
  35. package/dist/domain/mcpserver/engine.d.ts +9 -3
  36. package/dist/domain/mcpserver/engine.d.ts.map +1 -1
  37. package/dist/domain/mcpserver/engine.js.map +1 -1
  38. package/dist/domain/mcpserver/start-connect.js +1 -1
  39. package/dist/domain/mcpserver/start-connect.js.map +1 -1
  40. package/dist/domain/platform/controller.d.ts +24 -11
  41. package/dist/domain/platform/controller.d.ts.map +1 -1
  42. package/dist/domain/platform/controller.js.map +1 -1
  43. package/dist/domain/plugin/overlay/sanitize.d.ts.map +1 -1
  44. package/dist/domain/plugin/overlay/sanitize.js +4 -2
  45. package/dist/domain/plugin/overlay/sanitize.js.map +1 -1
  46. package/dist/domain/workflowexecution/phases.d.ts +21 -0
  47. package/dist/domain/workflowexecution/phases.d.ts.map +1 -0
  48. package/dist/domain/workflowexecution/phases.js +31 -0
  49. package/dist/domain/workflowexecution/phases.js.map +1 -0
  50. package/dist/identity/oidc-verifier.js +8 -1
  51. package/dist/identity/oidc-verifier.js.map +1 -1
  52. package/dist/pipeline/apiresource-meta.d.ts +15 -0
  53. package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
  54. package/dist/pipeline/apiresource-meta.js +34 -0
  55. package/dist/pipeline/apiresource-meta.js.map +1 -1
  56. package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
  57. package/dist/pipeline/steps/guard-memory-capture.js +5 -3
  58. package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
  59. package/dist/runnerauth/bound-execution.d.ts +27 -0
  60. package/dist/runnerauth/bound-execution.d.ts.map +1 -0
  61. package/dist/runnerauth/bound-execution.js +137 -0
  62. package/dist/runnerauth/bound-execution.js.map +1 -0
  63. package/dist/runnerauth/built-in-runner-credential-provider.d.ts +28 -0
  64. package/dist/runnerauth/built-in-runner-credential-provider.d.ts.map +1 -0
  65. package/dist/runnerauth/built-in-runner-credential-provider.js +217 -0
  66. package/dist/runnerauth/built-in-runner-credential-provider.js.map +1 -0
  67. package/dist/runnerauth/constants.d.ts +63 -0
  68. package/dist/runnerauth/constants.d.ts.map +1 -0
  69. package/dist/runnerauth/constants.js +63 -0
  70. package/dist/runnerauth/constants.js.map +1 -0
  71. package/dist/runnerauth/dispatch-credential.d.ts +38 -0
  72. package/dist/runnerauth/dispatch-credential.d.ts.map +1 -0
  73. package/dist/runnerauth/dispatch-credential.js +17 -0
  74. package/dist/runnerauth/dispatch-credential.js.map +1 -0
  75. package/dist/runnerauth/runner-credential-provider.d.ts +49 -11
  76. package/dist/runnerauth/runner-credential-provider.d.ts.map +1 -1
  77. package/dist/runnerauth/runner-credential-provider.js +16 -4
  78. package/dist/runnerauth/runner-credential-provider.js.map +1 -1
  79. package/dist/runnerauth/runner-subject-verifier.d.ts +14 -0
  80. package/dist/runnerauth/runner-subject-verifier.d.ts.map +1 -0
  81. package/dist/runnerauth/runner-subject-verifier.js +115 -0
  82. package/dist/runnerauth/runner-subject-verifier.js.map +1 -0
  83. package/dist/runnerauth/runnerauth.d.ts +33 -4
  84. package/dist/runnerauth/runnerauth.d.ts.map +1 -1
  85. package/dist/runnerauth/runnerauth.js +131 -35
  86. package/dist/runnerauth/runnerauth.js.map +1 -1
  87. package/dist/temporal/agentexecution/engine-client.d.ts +13 -0
  88. package/dist/temporal/agentexecution/engine-client.d.ts.map +1 -1
  89. package/dist/temporal/agentexecution/engine-client.js +10 -2
  90. package/dist/temporal/agentexecution/engine-client.js.map +1 -1
  91. package/dist/temporal/agentexecution/workflow-input.d.ts +13 -0
  92. package/dist/temporal/agentexecution/workflow-input.d.ts.map +1 -1
  93. package/dist/temporal/agentexecution/workflows/invoke-agent-execution.d.ts.map +1 -1
  94. package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js +35 -31
  95. package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js.map +1 -1
  96. package/dist/temporal/workflowexecution/engine-client.d.ts +24 -0
  97. package/dist/temporal/workflowexecution/engine-client.d.ts.map +1 -1
  98. package/dist/temporal/workflowexecution/engine-client.js +26 -19
  99. package/dist/temporal/workflowexecution/engine-client.js.map +1 -1
  100. package/dist/temporal/workflowexecution/workflow-input.d.ts +14 -0
  101. package/dist/temporal/workflowexecution/workflow-input.d.ts.map +1 -1
  102. package/package.json +5 -5
  103. package/src/authorization/__tests__/lane-admission.test.ts +115 -0
  104. package/src/authorization/lane-admission.ts +73 -0
  105. package/src/authorization/schedule-fire-caller.ts +10 -14
  106. package/src/boot/compose.ts +79 -22
  107. package/src/boot/inprocess.ts +7 -1
  108. package/src/domain/executioncontext/__tests__/executioncontext.test.ts +104 -0
  109. package/src/domain/executioncontext/__tests__/resolve-values-capability.test.ts +33 -4
  110. package/src/domain/executioncontext/resolve-values-for-caller.ts +79 -22
  111. package/src/domain/identityaccount/__tests__/account-for-stamp.test.ts +111 -0
  112. package/src/domain/identityaccount/actor.ts +61 -21
  113. package/src/domain/identityaccount/resolve.ts +31 -0
  114. package/src/domain/mcpserver/__tests__/connect.test.ts +17 -2
  115. package/src/domain/mcpserver/connect-execution-id.ts +34 -0
  116. package/src/domain/mcpserver/connect.ts +38 -3
  117. package/src/domain/mcpserver/engine.ts +9 -3
  118. package/src/domain/mcpserver/start-connect.ts +1 -1
  119. package/src/domain/platform/controller.ts +24 -11
  120. package/src/domain/plugin/__tests__/plugin.test.ts +116 -1
  121. package/src/domain/plugin/__tests__/sanitize.test.ts +2 -2
  122. package/src/domain/plugin/overlay/sanitize.ts +4 -2
  123. package/src/domain/workflowexecution/__tests__/phases.test.ts +58 -0
  124. package/src/domain/workflowexecution/phases.ts +33 -0
  125. package/src/extensions/__tests__/extension-composition.test.ts +7 -1
  126. package/src/identity/__tests__/oidc-verifier.test.ts +22 -0
  127. package/src/identity/oidc-verifier.ts +8 -1
  128. package/src/pipeline/__tests__/kind-by-id-prefix.test.ts +75 -0
  129. package/src/pipeline/apiresource-meta.ts +41 -0
  130. package/src/pipeline/steps/__tests__/guard-memory-capture.test.ts +55 -35
  131. package/src/pipeline/steps/guard-memory-capture.ts +5 -3
  132. package/src/runnerauth/__tests__/bound-execution.test.ts +334 -0
  133. package/src/runnerauth/__tests__/built-in-runner-credential-provider.test.ts +516 -0
  134. package/src/runnerauth/__tests__/dispatch-credential.test.ts +94 -0
  135. package/src/runnerauth/__tests__/runner-credential-provider.test.ts +27 -0
  136. package/src/runnerauth/__tests__/runner-subject-composed.test.ts +497 -0
  137. package/src/runnerauth/__tests__/runner-subject-verifier.test.ts +541 -0
  138. package/src/runnerauth/bound-execution.ts +270 -0
  139. package/src/runnerauth/built-in-runner-credential-provider.ts +300 -0
  140. package/src/runnerauth/constants.ts +74 -0
  141. package/src/runnerauth/dispatch-credential.ts +63 -0
  142. package/src/runnerauth/runner-credential-provider.ts +60 -11
  143. package/src/runnerauth/runner-subject-verifier.ts +152 -0
  144. package/src/runnerauth/runnerauth.ts +148 -38
  145. package/src/temporal/agentexecution/__tests__/engine-client.test.ts +107 -38
  146. package/src/temporal/agentexecution/__tests__/invoke-workflow.test.ts +105 -2
  147. package/src/temporal/agentexecution/engine-client.ts +27 -2
  148. package/src/temporal/agentexecution/workflow-input.ts +13 -0
  149. package/src/temporal/agentexecution/workflows/invoke-agent-execution.ts +90 -44
  150. package/src/temporal/workflowexecution/__tests__/engine-client.test.ts +139 -33
  151. package/src/temporal/workflowexecution/engine-client.ts +48 -21
  152. package/src/temporal/workflowexecution/workflow-input.ts +14 -0
@@ -157,28 +157,77 @@ const approvalGateResolvedSignal = defineSignal(SIGNAL_APPROVAL_GATE_RESOLVED);
157
157
 
158
158
  // ─── Activity proxies (options are contract; see the census table) ──────
159
159
 
160
+ /**
161
+ * The typed input of ExecuteDeepAgent and ExecuteCursor — the runner's
162
+ * ExecuteActivityInput (shared/activity-input.ts), snake_case keys, a
163
+ * cross-component wire contract. `execution_context_token` is the run
164
+ * credential the workflow input carries (workflow-input.ts), handed on
165
+ * so the activity can present it on the run's own RPCs; omitted when the
166
+ * dispatch carried none (the omitempty convention). Adding the key needed
167
+ * no patch marker: an in-flight history's input has no token, so the
168
+ * commands its replay generates are byte-for-byte what they were.
169
+ */
170
+ interface ExecuteActivityInput {
171
+ readonly execution_id: string;
172
+ readonly thread_id: string;
173
+ readonly invoker_identity_account_id: string;
174
+ readonly turn_seq: number;
175
+ readonly execution_context_token?: string;
176
+ }
177
+
178
+ /**
179
+ * The typed input of GenerateSessionSubject — the runner's
180
+ * GenerateSessionSubjectInput (activities/generate-session-subject.ts),
181
+ * snake_case keys. Sent ONLY when the dispatch carries a run credential;
182
+ * otherwise the activity is called with the positional execution id it has
183
+ * always received, byte for byte, because a server whose dispatches carry
184
+ * no credential (the cloud's: its runner credential is provisioned, not
185
+ * dispatched) talks to desktop runners in the field that predate the object
186
+ * shape, and a session title is not worth breaking for them. A runner that
187
+ * knows the object accepts both.
188
+ */
189
+ interface GenerateSessionSubjectInput {
190
+ readonly execution_id: string;
191
+ readonly execution_context_token: string;
192
+ }
193
+
160
194
  interface RunnerActivities {
161
195
  [ENSURE_THREAD_ACTIVITY_NAME]: (
162
196
  sessionId: string,
163
197
  agentId: string,
164
198
  ) => Promise<string>;
165
- [EXECUTE_DEEP_AGENT_ACTIVITY_NAME]: (input: {
166
- execution_id: string;
167
- thread_id: string;
168
- invoker_identity_account_id: string;
169
- turn_seq: number;
170
- }) => Promise<RunnerActivityResult | null>;
171
- [EXECUTE_CURSOR_ACTIVITY_NAME]: (input: {
172
- execution_id: string;
173
- thread_id: string;
174
- invoker_identity_account_id: string;
175
- turn_seq: number;
176
- }) => Promise<RunnerActivityResult | null>;
199
+ [EXECUTE_DEEP_AGENT_ACTIVITY_NAME]: (
200
+ input: ExecuteActivityInput,
201
+ ) => Promise<RunnerActivityResult | null>;
202
+ [EXECUTE_CURSOR_ACTIVITY_NAME]: (
203
+ input: ExecuteActivityInput,
204
+ ) => Promise<RunnerActivityResult | null>;
177
205
  [GENERATE_SESSION_SUBJECT_ACTIVITY_NAME]: (
178
- executionId: string,
206
+ input: GenerateSessionSubjectInput | string,
179
207
  ) => Promise<void>;
180
208
  }
181
209
 
210
+ /**
211
+ * The ONE builder of an Execute* input for both harness flows and both
212
+ * invocations (first turn and every HITL re-invocation): the run's
213
+ * coordinates from the workflow input, the harness thread and the turn
214
+ * from the caller.
215
+ */
216
+ function executeActivityInput(
217
+ input: InvokeAgentExecutionWorkflowInput,
218
+ threadId: string,
219
+ turnSeq: number,
220
+ ): ExecuteActivityInput {
221
+ const runCredential = input.execution_context_token ?? "";
222
+ return {
223
+ execution_id: input.execution_id,
224
+ thread_id: threadId,
225
+ invoker_identity_account_id: input.invoker_identity_account_id ?? "",
226
+ turn_seq: turnSeq,
227
+ ...(runCredential !== "" ? { execution_context_token: runCredential } : {}),
228
+ };
229
+ }
230
+
182
231
  function newEnsureThreadProxy(taskQueue: string) {
183
232
  return proxyActivities<Pick<RunnerActivities, typeof ENSURE_THREAD_ACTIVITY_NAME>>({
184
233
  taskQueue,
@@ -516,7 +565,6 @@ async function executeDeepAgentFlow(
516
565
  ): Promise<RunnerActivityResult> {
517
566
  const executionId = input.execution_id;
518
567
  const sessionId = input.session_id;
519
- const invoker = input.invoker_identity_account_id ?? "";
520
568
 
521
569
  log.info("Step 1: Ensuring thread", { sessionId, agentId: input.agent_id });
522
570
  const ensureThread = newEnsureThreadProxy(activityTaskQueue);
@@ -531,7 +579,7 @@ async function executeDeepAgentFlow(
531
579
  }
532
580
  log.info("Thread ensured", { threadId });
533
581
 
534
- fireGenerateSessionSubject(activityTaskQueue, executionId);
582
+ fireGenerateSessionSubject(activityTaskQueue, input);
535
583
 
536
584
  log.info("Step 2: Executing deep agent", { executionId, threadId });
537
585
  const agentProxy = newExecuteAgentProxy(activityTaskQueue);
@@ -547,20 +595,14 @@ async function executeDeepAgentFlow(
547
595
  signals,
548
596
  parentWorkflowId: input.parent_workflow_id ?? "",
549
597
  firstInvoke: () =>
550
- agentProxy[EXECUTE_DEEP_AGENT_ACTIVITY_NAME]({
551
- execution_id: executionId,
552
- thread_id: threadId,
553
- invoker_identity_account_id: invoker,
554
- turn_seq: 0,
555
- }),
598
+ agentProxy[EXECUTE_DEEP_AGENT_ACTIVITY_NAME](
599
+ executeActivityInput(input, threadId, 0),
600
+ ),
556
601
  // The LangGraph thread id is stable — re-invocations reuse it.
557
602
  reinvoke: (turnSeq) =>
558
- agentProxy[EXECUTE_DEEP_AGENT_ACTIVITY_NAME]({
559
- execution_id: executionId,
560
- thread_id: threadId,
561
- invoker_identity_account_id: invoker,
562
- turn_seq: turnSeq,
563
- }),
603
+ agentProxy[EXECUTE_DEEP_AGENT_ACTIVITY_NAME](
604
+ executeActivityInput(input, threadId, turnSeq),
605
+ ),
564
606
  nullResultMessage: "activity returned null status - this should never happen",
565
607
  nullResultAfterApprovalMessage:
566
608
  "activity returned null status after approval - this should never happen",
@@ -584,9 +626,8 @@ async function executeCursorFlow(
584
626
  ): Promise<RunnerActivityResult> {
585
627
  const executionId = input.execution_id;
586
628
  const sessionId = input.session_id;
587
- const invoker = input.invoker_identity_account_id ?? "";
588
629
 
589
- fireGenerateSessionSubject(activityTaskQueue, executionId);
630
+ fireGenerateSessionSubject(activityTaskQueue, input);
590
631
 
591
632
  log.info("Executing Cursor agent", { executionId, sessionId });
592
633
  const agentProxy = newExecuteAgentProxy(activityTaskQueue);
@@ -612,12 +653,9 @@ async function executeCursorFlow(
612
653
  );
613
654
  harnessStateId = "";
614
655
  }
615
- return agentProxy[EXECUTE_CURSOR_ACTIVITY_NAME]({
616
- execution_id: executionId,
617
- thread_id: harnessStateId,
618
- invoker_identity_account_id: invoker,
619
- turn_seq: 0,
620
- });
656
+ return agentProxy[EXECUTE_CURSOR_ACTIVITY_NAME](
657
+ executeActivityInput(input, harnessStateId, 0),
658
+ );
621
659
  },
622
660
  reinvoke: async (turnSeq) => {
623
661
  let harnessStateId: string;
@@ -634,12 +672,9 @@ async function executeCursorFlow(
634
672
  cycle: turnSeq,
635
673
  harnessStateId,
636
674
  });
637
- return agentProxy[EXECUTE_CURSOR_ACTIVITY_NAME]({
638
- execution_id: executionId,
639
- thread_id: harnessStateId,
640
- invoker_identity_account_id: invoker,
641
- turn_seq: turnSeq,
642
- });
675
+ return agentProxy[EXECUTE_CURSOR_ACTIVITY_NAME](
676
+ executeActivityInput(input, harnessStateId, turnSeq),
677
+ );
643
678
  },
644
679
  nullResultMessage:
645
680
  "cursor activity returned null status - this should never happen",
@@ -649,15 +684,26 @@ async function executeCursorFlow(
649
684
  });
650
685
  }
651
686
 
652
- /** GenerateSessionSubject: fire-and-forget, non-blocking (issue #665). */
687
+ /**
688
+ * GenerateSessionSubject: fire-and-forget, non-blocking (issue #665). The
689
+ * argument shape follows the run credential's presence — see
690
+ * GenerateSessionSubjectInput for why the credential-less dispatch keeps
691
+ * the positional string.
692
+ */
653
693
  function fireGenerateSessionSubject(
654
694
  activityTaskQueue: string,
655
- executionId: string,
695
+ input: InvokeAgentExecutionWorkflowInput,
656
696
  ): void {
697
+ const executionId = input.execution_id;
698
+ const runCredential = input.execution_context_token ?? "";
699
+ const argument: GenerateSessionSubjectInput | string =
700
+ runCredential !== ""
701
+ ? { execution_id: executionId, execution_context_token: runCredential }
702
+ : executionId;
657
703
  const subjectProxy = newSubjectProxy(activityTaskQueue);
658
704
  void (async () => {
659
705
  try {
660
- await subjectProxy[GENERATE_SESSION_SUBJECT_ACTIVITY_NAME](executionId);
706
+ await subjectProxy[GENERATE_SESSION_SUBJECT_ACTIVITY_NAME](argument);
661
707
  } catch (error) {
662
708
  log.warn("Session subject generation failed (non-critical)", {
663
709
  executionId,
@@ -6,9 +6,18 @@
6
6
  * raw-signal convention, the WorkflowNotFoundError → seam-sentinel
7
7
  * mapping, and the provider's memoization/disconnected semantics.
8
8
  *
9
+ * And the run credential on the input (2026-09-16): minted through the
10
+ * composed provider's capability on BOTH dispatch lanes (start and
11
+ * signalWithStart), bound to the execution with no clock; the key is
12
+ * ABSENT when the provider lacks the capability (an edition credentialed
13
+ * another way), when it is keyless, and when the mint throws — a dispatch
14
+ * never fails on a mint.
15
+ *
9
16
  * The Temporal Client is a recording double — the real dial path is
10
17
  * proven by local-execution.
11
18
  */
19
+ import { randomBytes } from "node:crypto";
20
+
12
21
  import { describe, expect, it } from "vitest";
13
22
 
14
23
  import type { Client } from "@temporalio/client";
@@ -27,11 +36,18 @@ import {
27
36
  WORKFLOW_ROUTING_GLOBAL,
28
37
  WorkflowExecutionTemporalConfig,
29
38
  } from "../../../domain/workflowexecution/temporal/config.js";
39
+ import type { RunCredentialMint } from "../../../runnerauth/dispatch-credential.js";
40
+ import { newExecutionScopedRunnerCredentialProvider } from "../../../runnerauth/runner-credential-provider.js";
41
+ import {
42
+ isClockedToken,
43
+ RunnerAuthService,
44
+ } from "../../../runnerauth/runnerauth.js";
30
45
  import type { TemporalManager } from "../../manager.js";
31
46
  import {
32
47
  newWorkflowExecutionEngineStateProvider,
33
48
  TemporalWorkflowExecutionEngine,
34
49
  } from "../engine-client.js";
50
+ import type { TemporalWorkflowExecutionEngineDeps } from "../engine-client.js";
35
51
 
36
52
  const silentLogger = createLogger({
37
53
  level: "error",
@@ -46,6 +62,27 @@ const config = new WorkflowExecutionTemporalConfig(
46
62
  WORKFLOW_DEFAULT_EXECUTION_TARGET_LOCAL,
47
63
  );
48
64
 
65
+ /** The keyed OSS default — what a real open-source boot composes. */
66
+ const runnerAuth = RunnerAuthService.create(randomBytes(32));
67
+ const keyedCredentials = newExecutionScopedRunnerCredentialProvider(runnerAuth);
68
+
69
+ /**
70
+ * One construction site for every arm: the realistic deps by default,
71
+ * any of them overridable, so the next dependency costs one line here.
72
+ */
73
+ function newEngine(
74
+ client: Client,
75
+ overrides: Partial<Omit<TemporalWorkflowExecutionEngineDeps, "client">> = {},
76
+ ): TemporalWorkflowExecutionEngine {
77
+ return new TemporalWorkflowExecutionEngine({
78
+ client,
79
+ config,
80
+ runnerCredentials: keyedCredentials,
81
+ logger: silentLogger,
82
+ ...overrides,
83
+ });
84
+ }
85
+
49
86
  interface RecordedStart {
50
87
  readonly workflowType: string;
51
88
  readonly options: Record<string, unknown>;
@@ -93,6 +130,15 @@ function newClientDouble(handleError?: Error) {
93
130
  return { client, starts, signalWithStarts, handleCalls };
94
131
  }
95
132
 
133
+ /** The one workflow input a recorded start (or signalWithStart) carried. */
134
+ function inputOf(recorded: RecordedStart[]): Record<string, unknown> {
135
+ expect(recorded).toHaveLength(1);
136
+ return (recorded[0]!.options["args"] as unknown[])[0] as Record<
137
+ string,
138
+ unknown
139
+ >;
140
+ }
141
+
96
142
  function startInput(
97
143
  overrides: Partial<StartWorkflowExecutionInput> = {},
98
144
  ): StartWorkflowExecutionInput {
@@ -110,11 +156,7 @@ function startInput(
110
156
  describe("TemporalWorkflowExecutionEngine", () => {
111
157
  it("starts the orchestrator with the pinned ID, queue, memo, and omitempty input", async () => {
112
158
  const double = newClientDouble();
113
- const engine = new TemporalWorkflowExecutionEngine({
114
- client: double.client,
115
- config,
116
- logger: silentLogger,
117
- });
159
+ const engine = newEngine(double.client);
118
160
 
119
161
  await engine.startInvokeWorkflow(startInput());
120
162
 
@@ -125,7 +167,9 @@ describe("TemporalWorkflowExecutionEngine", () => {
125
167
  "stigmer/workflow-execution/invoke/wfe-1",
126
168
  );
127
169
  expect(start.options["taskQueue"]).toBe("workflow_execution_stigmer");
128
- expect(start.options["memo"]).toEqual({ runnerTaskQueue: "stigmer_runner" });
170
+ expect(start.options["memo"]).toEqual({
171
+ runnerTaskQueue: "stigmer_runner",
172
+ });
129
173
  // Go's omitempty shape: recovery_mode false is OMITTED, not false.
130
174
  expect(start.options["args"]).toEqual([
131
175
  {
@@ -133,6 +177,8 @@ describe("TemporalWorkflowExecutionEngine", () => {
133
177
  workflow_instance_id: "wfi-1",
134
178
  workflow_id: "wf-1",
135
179
  org_id: "org-1",
180
+ // The run credential, minted per dispatch (its own arms below).
181
+ execution_context_token: expect.any(String),
136
182
  },
137
183
  ]);
138
184
  // No workflowRunTimeout — durable execution (module header).
@@ -141,11 +187,7 @@ describe("TemporalWorkflowExecutionEngine", () => {
141
187
 
142
188
  it("omits every zero-valued optional field and carries recovery_mode when set", async () => {
143
189
  const double = newClientDouble();
144
- const engine = new TemporalWorkflowExecutionEngine({
145
- client: double.client,
146
- config,
147
- logger: silentLogger,
148
- });
190
+ const engine = newEngine(double.client);
149
191
 
150
192
  await engine.startInvokeWorkflow(
151
193
  startInput({
@@ -157,18 +199,89 @@ describe("TemporalWorkflowExecutionEngine", () => {
157
199
  );
158
200
 
159
201
  expect(double.starts[0]!.options["args"]).toEqual([
160
- { execution_id: "wfe-1", recovery_mode: true },
202
+ {
203
+ execution_id: "wfe-1",
204
+ recovery_mode: true,
205
+ execution_context_token: expect.any(String),
206
+ },
161
207
  ]);
162
208
  });
163
209
 
164
- it("signalWithStart carries the signal, its payload, and the same start options", async () => {
210
+ it("carries the run credential minted through the provider: bound to the execution, no clock", async () => {
165
211
  const double = newClientDouble();
166
- const engine = new TemporalWorkflowExecutionEngine({
167
- client: double.client,
168
- config,
169
- logger: silentLogger,
212
+ const engine = newEngine(double.client);
213
+
214
+ await engine.startInvokeWorkflow(startInput({ executionId: "wex_cred" }));
215
+
216
+ const token = inputOf(double.starts)["execution_context_token"];
217
+ expect(typeof token).toBe("string");
218
+ expect(runnerAuth.verify(token as string)).toBe("wex_cred");
219
+ expect(isClockedToken(token as string)).toBe(false);
220
+ });
221
+
222
+ it("signalWithStart mints the same credential — the lane that may be the start", async () => {
223
+ const double = newClientDouble();
224
+ const engine = newEngine(double.client);
225
+
226
+ await engine.signalWithStart(
227
+ startInput({ executionId: "wex_sws" }),
228
+ "relaySignal",
229
+ {
230
+ signalName: "human_input_step1",
231
+ payload: { answer: 42 },
232
+ },
233
+ );
234
+
235
+ const token = inputOf(double.signalWithStarts)["execution_context_token"];
236
+ expect(runnerAuth.verify(token as string)).toBe("wex_sws");
237
+ });
238
+
239
+ it("omits the credential when the provider lacks the capability — an edition credentialed another way", async () => {
240
+ const double = newClientDouble();
241
+ const withoutCapability: RunCredentialMint = {};
242
+ const engine = newEngine(double.client, {
243
+ runnerCredentials: withoutCapability,
170
244
  });
171
245
 
246
+ await engine.startInvokeWorkflow(startInput());
247
+ await engine.signalWithStart(startInput(), "relaySignal", null);
248
+
249
+ expect("execution_context_token" in inputOf(double.starts)).toBe(false);
250
+ expect("execution_context_token" in inputOf(double.signalWithStarts)).toBe(
251
+ false,
252
+ );
253
+ });
254
+
255
+ it("omits the credential when the provider is keyless", async () => {
256
+ const double = newClientDouble();
257
+ const keyless = newExecutionScopedRunnerCredentialProvider(
258
+ RunnerAuthService.create(undefined),
259
+ );
260
+ const engine = newEngine(double.client, { runnerCredentials: keyless });
261
+
262
+ await engine.startInvokeWorkflow(startInput());
263
+
264
+ expect("execution_context_token" in inputOf(double.starts)).toBe(false);
265
+ });
266
+
267
+ it("a mint that throws degrades to a dispatch without the credential — the start still happens", async () => {
268
+ const double = newClientDouble();
269
+ const throwing: RunCredentialMint = {
270
+ mintRunCredential(): string {
271
+ throw new Error("signing failed");
272
+ },
273
+ };
274
+ const engine = newEngine(double.client, { runnerCredentials: throwing });
275
+
276
+ await engine.startInvokeWorkflow(startInput());
277
+
278
+ expect("execution_context_token" in inputOf(double.starts)).toBe(false);
279
+ });
280
+
281
+ it("signalWithStart carries the signal, its payload, and the same start options", async () => {
282
+ const double = newClientDouble();
283
+ const engine = newEngine(double.client);
284
+
172
285
  await engine.signalWithStart(startInput(), "relaySignal", {
173
286
  signalName: "human_input_step1",
174
287
  payload: { answer: 42 },
@@ -189,17 +302,17 @@ describe("TemporalWorkflowExecutionEngine", () => {
189
302
 
190
303
  it("raw signal sends exactly one argument, null when the payload is absent", async () => {
191
304
  const double = newClientDouble();
192
- const engine = new TemporalWorkflowExecutionEngine({
193
- client: double.client,
194
- config,
195
- logger: silentLogger,
196
- });
305
+ const engine = newEngine(double.client);
197
306
 
198
307
  await engine.signalWorkflow("wf-id-1", "pause", "user requested");
199
308
  await engine.signalWorkflow("wf-id-1", "resume", undefined);
200
309
 
201
310
  expect(double.handleCalls).toEqual([
202
- { workflowId: "wf-id-1", method: "signal", args: ["pause", "user requested"] },
311
+ {
312
+ workflowId: "wf-id-1",
313
+ method: "signal",
314
+ args: ["pause", "user requested"],
315
+ },
203
316
  // Go SignalWorkflow(nil) — one nil argument, not zero arguments.
204
317
  { workflowId: "wf-id-1", method: "signal", args: ["resume", null] },
205
318
  ]);
@@ -208,11 +321,7 @@ describe("TemporalWorkflowExecutionEngine", () => {
208
321
  it("maps WorkflowNotFoundError onto the seam's sentinel for signal/cancel/terminate", async () => {
209
322
  const notFound = new WorkflowNotFoundError("gone", "wf-id-x", undefined);
210
323
  const double = newClientDouble(notFound);
211
- const engine = new TemporalWorkflowExecutionEngine({
212
- client: double.client,
213
- config,
214
- logger: silentLogger,
215
- });
324
+ const engine = newEngine(double.client);
216
325
 
217
326
  await expect(
218
327
  engine.signalWorkflow("wf-id-x", "pause", "r"),
@@ -228,11 +337,7 @@ describe("TemporalWorkflowExecutionEngine", () => {
228
337
  it("passes non-not-found errors through unchanged", async () => {
229
338
  const boom = new Error("connection refused");
230
339
  const double = newClientDouble(boom);
231
- const engine = new TemporalWorkflowExecutionEngine({
232
- client: double.client,
233
- config,
234
- logger: silentLogger,
235
- });
340
+ const engine = newEngine(double.client);
236
341
 
237
342
  await expect(engine.cancelWorkflow("wf-id-x")).rejects.toBe(boom);
238
343
  });
@@ -248,6 +353,7 @@ describe("newWorkflowExecutionEngineStateProvider", () => {
248
353
  const provider = newWorkflowExecutionEngineStateProvider({
249
354
  manager,
250
355
  config,
356
+ runnerCredentials: keyedCredentials,
251
357
  logger: silentLogger,
252
358
  });
253
359
 
@@ -26,6 +26,18 @@
26
26
  * signal/cancel/terminate operations take a workflowId verbatim. That
27
27
  * asymmetry is the seam's documented contract.
28
28
  *
29
+ * The run credential rides the input (2026-09-16): every dispatch — a
30
+ * fresh start, a recovery, and both signalWithStart lanes (sendSignal,
31
+ * submitWorkflowTaskApproval), which may be the start when they race the
32
+ * create — asks the composed credential provider for the run's credential
33
+ * (runnerauth/dispatch-credential.ts) and carries it as
34
+ * `execution_context_token`, the connect lane's shape. signalWithStart
35
+ * mints even when the workflow already exists and Temporal discards the
36
+ * input: the mint is a local HMAC with no read, and one input builder
37
+ * serving every lane is worth more than the spared signature. A provider
38
+ * without the capability (an edition credentialed another way) leaves the
39
+ * key off.
40
+ *
29
41
  * Proven by the workflowexecution suites on local-execution.
30
42
  */
31
43
  import type { JsonValue } from "@bufbuild/protobuf";
@@ -42,6 +54,8 @@ import {
42
54
  type WorkflowExecutionEngineStateProvider,
43
55
  } from "../../domain/workflowexecution/engine.js";
44
56
  import type { WorkflowExecutionTemporalConfig } from "../../domain/workflowexecution/temporal/config.js";
57
+ import { runCredentialForDispatch } from "../../runnerauth/dispatch-credential.js";
58
+ import type { RunCredentialMint } from "../../runnerauth/dispatch-credential.js";
45
59
  import type { TemporalManager } from "../manager.js";
46
60
  import { resolveWorkflowTaskQueue } from "./dispatch.js";
47
61
  import {
@@ -54,6 +68,8 @@ import type { InvokeWorkflowExecutionWorkflowInput } from "./workflow-input.js";
54
68
  export interface TemporalWorkflowExecutionEngineDeps {
55
69
  readonly client: Client;
56
70
  readonly config: WorkflowExecutionTemporalConfig;
71
+ /** The composed credential provider — the dispatch reads its run-credential capability (module header). */
72
+ readonly runnerCredentials: RunCredentialMint;
57
73
  readonly logger: Logger;
58
74
  }
59
75
 
@@ -82,7 +98,7 @@ export class TemporalWorkflowExecutionEngine
82
98
  workflowId,
83
99
  taskQueue: config.stigmerQueue,
84
100
  memo: { [MEMO_RUNNER_TASK_QUEUE]: dispatch.taskQueue },
85
- args: [buildWorkflowInput(input)],
101
+ args: [this.buildWorkflowInput(input)],
86
102
  });
87
103
 
88
104
  logger.info("Started InvokeWorkflowExecutionWorkflow", {
@@ -117,7 +133,7 @@ export class TemporalWorkflowExecutionEngine
117
133
  workflowId,
118
134
  taskQueue: config.stigmerQueue,
119
135
  memo: { [MEMO_RUNNER_TASK_QUEUE]: dispatch.taskQueue },
120
- args: [buildWorkflowInput(input)],
136
+ args: [this.buildWorkflowInput(input)],
121
137
  signal: signalName,
122
138
  signalArgs: [payload],
123
139
  },
@@ -164,26 +180,35 @@ export class TemporalWorkflowExecutionEngine
164
180
  throw mapNotFound(error, workflowId);
165
181
  }
166
182
  }
167
- }
168
183
 
169
- /**
170
- * The slim input with Go's omitempty shape (workflow-input.ts): zero
171
- * values are omitted so TS-authored histories carry the same keys a
172
- * Go-authored one would. The cloud-only fields are unmodeled (the seam's
173
- * ratified boundary).
174
- */
175
- function buildWorkflowInput(
176
- input: StartWorkflowExecutionInput,
177
- ): InvokeWorkflowExecutionWorkflowInput {
178
- return {
179
- execution_id: input.executionId,
180
- ...(input.workflowInstanceId !== ""
181
- ? { workflow_instance_id: input.workflowInstanceId }
182
- : {}),
183
- ...(input.workflowId !== "" ? { workflow_id: input.workflowId } : {}),
184
- ...(input.orgId !== "" ? { org_id: input.orgId } : {}),
185
- ...(input.recoveryMode ? { recovery_mode: true } : {}),
186
- };
184
+ /**
185
+ * The slim input with Go's omitempty shape (workflow-input.ts): zero
186
+ * values are omitted so TS-authored histories carry the same keys a
187
+ * Go-authored one would. The cloud-only fields are unmodeled (the
188
+ * seam's ratified boundary). The run credential is the one key minted
189
+ * here rather than copied from the caller (module header).
190
+ */
191
+ private buildWorkflowInput(
192
+ input: StartWorkflowExecutionInput,
193
+ ): InvokeWorkflowExecutionWorkflowInput {
194
+ const runCredential = runCredentialForDispatch(
195
+ this.deps.runnerCredentials,
196
+ input.executionId,
197
+ this.deps.logger,
198
+ );
199
+ return {
200
+ execution_id: input.executionId,
201
+ ...(input.workflowInstanceId !== ""
202
+ ? { workflow_instance_id: input.workflowInstanceId }
203
+ : {}),
204
+ ...(input.workflowId !== "" ? { workflow_id: input.workflowId } : {}),
205
+ ...(input.orgId !== "" ? { org_id: input.orgId } : {}),
206
+ ...(input.recoveryMode ? { recovery_mode: true } : {}),
207
+ ...(runCredential !== ""
208
+ ? { execution_context_token: runCredential }
209
+ : {}),
210
+ };
211
+ }
187
212
  }
188
213
 
189
214
  /**
@@ -200,6 +225,7 @@ function mapNotFound(error: unknown, workflowId: string): unknown {
200
225
  export interface WorkflowExecutionEngineStateProviderDeps {
201
226
  readonly manager: TemporalManager;
202
227
  readonly config: WorkflowExecutionTemporalConfig;
228
+ readonly runnerCredentials: RunCredentialMint;
203
229
  readonly logger: Logger;
204
230
  }
205
231
 
@@ -232,6 +258,7 @@ export function newWorkflowExecutionEngineStateProvider(
232
258
  engine: new TemporalWorkflowExecutionEngine({
233
259
  client,
234
260
  config: deps.config,
261
+ runnerCredentials: deps.runnerCredentials,
235
262
  logger: deps.logger,
236
263
  }),
237
264
  },
@@ -28,6 +28,20 @@ export interface InvokeWorkflowExecutionWorkflowInput {
28
28
  * recover pipeline; normal executions omit it (Go omitempty).
29
29
  */
30
30
  readonly recovery_mode?: boolean;
31
+ /**
32
+ * The run credential (runnerauth: a clockless execution-scoped token
33
+ * bound to this execution), minted by the engine client at dispatch
34
+ * when the composed credential provider defines `mintRunCredential`
35
+ * and omitted otherwise. The connect lane's key for the same token
36
+ * type (domain/mcpserver/engine.ts). The orchestrator passes this
37
+ * object whole to the runner's child workflow, so the runner reads it
38
+ * from here and presents it on the run's own RPCs: under the built-in
39
+ * authorization posture it admits the runner as the human whose run
40
+ * this is, for as long as the run lives. In Temporal history in the
41
+ * clear, like the connect lane's — history is operator-only, and the
42
+ * operator holds the signing key already.
43
+ */
44
+ readonly execution_context_token?: string;
31
45
  }
32
46
 
33
47
  /**