@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
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Pins the runner-subject lane as the composition root wires it, over
3
+ * real boots, in the three authorization postures the server can be in:
4
+ *
5
+ * - OIDC with no unit Authorizer (the self-host's shape; the BUILT-IN
6
+ * posture): the runner-subject verifier is composed BETWEEN `apikey`
7
+ * and `oidc`. The order is the contract, not a detail: the OIDC
8
+ * verifier claims ANY JWT-shaped token and throws when it cannot
9
+ * verify it, and the server's own runner token is JWT-shaped — placed
10
+ * after `oidc`, the runner verifier would never see its own token,
11
+ * and every runner call would fail as the OIDC verifier's fault. That
12
+ * is exactly what a 3.15.x self-host with sign-in on does today
13
+ * (stigmer#1137: no agent execution can run for anyone), and this
14
+ * file's wire arms are the regression test for it.
15
+ * - trusted-local: no runner verifier is composed. Nothing to
16
+ * delegate; the exchange token stays the decrypt-lane discriminator.
17
+ * - a unit's own Authorizer: no runner verifier is composed. The
18
+ * composition brings its own credential story (its provider, its
19
+ * verifiers), and open source must not stamp identities beside it.
20
+ *
21
+ * The wire arms under the built-in posture prove the whole lane with no
22
+ * engine: a live WORKFLOW execution row (its owner is DIRECT — the
23
+ * creator stamp — so no session is needed) is seeded in the composed
24
+ * server's store, stamped by a person who provisioned over the wire; the
25
+ * server's own key mints the run credential; presenting it on the
26
+ * execution's own `get` is ADMITTED (the runner acts as the person who
27
+ * created the run), and once the row is terminal the same credential is
28
+ * UNAUTHENTICATED. A forged token that names our type is refused by the
29
+ * runner verifier's sentence — never INTERNAL, never the OIDC copy.
30
+ *
31
+ * And the exchange as a MINT GATE (2026-09-16): over the same boot, a
32
+ * second person asking getRunnerScopedToken for Carol's run is
33
+ * PERMISSION_DENIED — the line the live claim check recorded the other
34
+ * way round ("A's key … for B's run: minted") — while Carol is minted a
35
+ * run credential that admits her on the wire, and a run that does not
36
+ * exist is NOT_FOUND with the copy a `get` would answer.
37
+ *
38
+ * Written failing on 2026-09-16; the verifier that follows turns it green. The per-arm
39
+ * proofs live in runner-subject-verifier.test.ts and
40
+ * built-in-runner-credential-provider.test.ts; this file is the entry's
41
+ * definition of done at the composition root.
42
+ */
43
+ import { mkdtempSync, rmSync } from "node:fs";
44
+ import { tmpdir } from "node:os";
45
+ import path from "node:path";
46
+ import { createServer } from "node:http";
47
+ import type { Server } from "node:http";
48
+ import { once } from "node:events";
49
+
50
+ import { create } from "@bufbuild/protobuf";
51
+ import { Code, ConnectError, createClient } from "@connectrpc/connect";
52
+ import type { Interceptor, Transport } from "@connectrpc/connect";
53
+ import { createGrpcTransport } from "@connectrpc/connect-node";
54
+ import { SignJWT, exportJWK, generateKeyPair } from "jose";
55
+ import type { GenerateKeyPairResult } from "jose";
56
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
57
+
58
+ import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
59
+ import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/enum_pb";
60
+ import { WorkflowExecutionQueryController } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/query_pb";
61
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
62
+ import { IdentityAccountCommandController } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/command_pb";
63
+ import { PlatformQueryController } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
64
+
65
+ import { loadConfig } from "../../boot/config.js";
66
+ import { composeServer } from "../../boot/compose.js";
67
+ import type { ComposedServer } from "../../boot/compose.js";
68
+ import type { ServerExtension } from "../../extensions/registry.js";
69
+ import {
70
+ baseConfig,
71
+ fakeVerifier,
72
+ silentLogger,
73
+ } from "../../extensions/__tests__/composed-support.js";
74
+ import { authenticateBearerToken } from "../../pipeline/interceptors/auth.js";
75
+ import {
76
+ RUN_CREDENTIAL_NOT_RUNS_PERSON_MESSAGE,
77
+ RUNNER_CREDENTIAL_INVALID_MESSAGE,
78
+ RUNNER_CREDENTIAL_NOT_LIVE_MESSAGE,
79
+ RUNNER_VERIFIER_NAME,
80
+ } from "../constants.js";
81
+ import { isClockedToken, TOKEN_TYPE_EXECUTION_SCOPED } from "../runnerauth.js";
82
+
83
+ const AUDIENCE = "https://api.stigmer.test/";
84
+ const ORG = "runner-subject-org";
85
+
86
+ const segment = (value: unknown): string =>
87
+ Buffer.from(JSON.stringify(value)).toString("base64url");
88
+
89
+ function forgedRunnerToken(executionId: string): string {
90
+ return `${segment({ alg: "HS256", typ: "JWT" })}.${segment({
91
+ token_type: TOKEN_TYPE_EXECUTION_SCOPED,
92
+ execution_id: executionId,
93
+ iat: 1,
94
+ })}.${segment("not-the-servers-mac")}`;
95
+ }
96
+
97
+ function bearer(token: string): Interceptor {
98
+ return (next) => (request) => {
99
+ request.header.set("authorization", `Bearer ${token}`);
100
+ return next(request);
101
+ };
102
+ }
103
+
104
+ function transportFor(port: number, token: string): Transport {
105
+ return createGrpcTransport({
106
+ baseUrl: `http://127.0.0.1:${port}`,
107
+ interceptors: [bearer(token)],
108
+ });
109
+ }
110
+
111
+ async function failureOf(promise: Promise<unknown>): Promise<ConnectError> {
112
+ try {
113
+ await promise;
114
+ } catch (error) {
115
+ expect(error).toBeInstanceOf(ConnectError);
116
+ return error as ConnectError;
117
+ }
118
+ throw new Error("expected the call to fail");
119
+ }
120
+
121
+ /** The smallest hermetic issuer the OIDC posture boots against: discovery, JWKS, userinfo. */
122
+ async function startIssuer(): Promise<{
123
+ issuer: string;
124
+ mint(sub: string, email: string): Promise<string>;
125
+ close(): Promise<void>;
126
+ }> {
127
+ const keys: GenerateKeyPairResult = await generateKeyPair("RS256");
128
+ const jwk = {
129
+ ...(await exportJWK(keys.publicKey)),
130
+ kid: "test-key",
131
+ alg: "RS256",
132
+ };
133
+ let issuer = "";
134
+ const server: Server = createServer((req, res) => {
135
+ if (req.url === "/.well-known/openid-configuration") {
136
+ res.setHeader("content-type", "application/json");
137
+ res.end(
138
+ JSON.stringify({
139
+ issuer,
140
+ jwks_uri: `${issuer}/jwks`,
141
+ userinfo_endpoint: `${issuer}/userinfo`,
142
+ }),
143
+ );
144
+ return;
145
+ }
146
+ if (req.url === "/jwks") {
147
+ res.setHeader("content-type", "application/json");
148
+ res.end(JSON.stringify({ keys: [jwk] }));
149
+ return;
150
+ }
151
+ if (req.url === "/userinfo") {
152
+ const token = (req.headers.authorization ?? "").replace(/^Bearer /, "");
153
+ const claims = JSON.parse(
154
+ Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"),
155
+ ) as { sub?: string; email?: string };
156
+ res.setHeader("content-type", "application/json");
157
+ res.end(
158
+ JSON.stringify({
159
+ sub: claims.sub ?? "",
160
+ email: claims.email ?? "",
161
+ given_name: "Runner",
162
+ family_name: "Subject",
163
+ }),
164
+ );
165
+ return;
166
+ }
167
+ res.statusCode = 404;
168
+ res.end();
169
+ });
170
+ server.listen(0, "127.0.0.1");
171
+ await once(server, "listening");
172
+ const address = server.address();
173
+ if (address === null || typeof address === "string") {
174
+ throw new Error("issuer server did not bind a port");
175
+ }
176
+ issuer = `http://127.0.0.1:${address.port}`;
177
+ return {
178
+ issuer,
179
+ mint: (sub, email) =>
180
+ new SignJWT({ email, name: "Runner Subject" })
181
+ .setProtectedHeader({ alg: "RS256", kid: "test-key" })
182
+ .setIssuer(issuer)
183
+ .setAudience(AUDIENCE)
184
+ .setSubject(sub)
185
+ .setIssuedAt()
186
+ .setExpirationTime("5m")
187
+ .sign(keys.privateKey),
188
+ close: async () => {
189
+ server.close();
190
+ await once(server, "close");
191
+ },
192
+ };
193
+ }
194
+
195
+ describe("the built-in posture (OIDC, no unit Authorizer): the runner acts as the run's human", () => {
196
+ let dir: string;
197
+ let issuer: Awaited<ReturnType<typeof startIssuer>>;
198
+ let server: ComposedServer;
199
+ let port: number;
200
+ let carolId: string;
201
+
202
+ beforeAll(async () => {
203
+ issuer = await startIssuer();
204
+ dir = mkdtempSync(path.join(tmpdir(), "runner-subject-oidc-"));
205
+ server = await composeServer({
206
+ config: loadConfig({
207
+ ...baseConfig(dir),
208
+ STIGMER_OIDC_ISSUER: issuer.issuer,
209
+ STIGMER_OIDC_AUDIENCE: AUDIENCE,
210
+ }),
211
+ logger: silentLogger,
212
+ extensions: [],
213
+ portOverride: 0,
214
+ host: "127.0.0.1",
215
+ });
216
+ port = await server.start();
217
+ // Carol provisions over the wire, the console's first sign-in; her
218
+ // runs are stamped with her account id from then on.
219
+ const carol = createClient(
220
+ IdentityAccountCommandController,
221
+ transportFor(port, await issuer.mint("auth0|carol", "carol@example.com")),
222
+ );
223
+ carolId = (await carol.provisionMyAccount({})).metadata?.id ?? "";
224
+ expect(carolId).not.toBe("");
225
+ });
226
+
227
+ afterAll(async () => {
228
+ await server.shutdown();
229
+ await issuer.close();
230
+ rmSync(dir, { recursive: true, force: true });
231
+ });
232
+
233
+ async function seedWorkflowExecution(
234
+ id: string,
235
+ phase: ExecutionPhase,
236
+ ): Promise<void> {
237
+ await server.store.saveResource(
238
+ ApiResourceKind.workflow_execution,
239
+ id,
240
+ WorkflowExecutionSchema,
241
+ create(WorkflowExecutionSchema, {
242
+ metadata: { id, name: id, org: ORG },
243
+ status: { phase, audit: { specAudit: { createdBy: { id: carolId } } } },
244
+ }),
245
+ );
246
+ }
247
+
248
+ it("the chain is apikey → runner → oidc: ours claims the server's own token before the OIDC verifier can fault on it", () => {
249
+ expect(server.identityVerifiers.map((verifier) => verifier.name)).toEqual([
250
+ "apikey",
251
+ RUNNER_VERIFIER_NAME,
252
+ "oidc",
253
+ ]);
254
+ });
255
+
256
+ it("a forged token naming our type is refused by the runner verifier's sentence — never INTERNAL, never the OIDC copy (stigmer#1137's regression arm)", async () => {
257
+ const failure = await failureOf(
258
+ authenticateBearerToken(
259
+ server.identityVerifiers,
260
+ forgedRunnerToken("wex_forged"),
261
+ silentLogger,
262
+ ),
263
+ );
264
+ expect(failure.code).toBe(Code.Unauthenticated);
265
+ expect(failure.rawMessage).toBe(RUNNER_CREDENTIAL_INVALID_MESSAGE);
266
+ });
267
+
268
+ it("an OIDC token still walks through to the OIDC verifier — the runner verifier passes on every shape but its own", async () => {
269
+ const identity = await authenticateBearerToken(
270
+ server.identityVerifiers,
271
+ await issuer.mint("auth0|carol", "carol@example.com"),
272
+ silentLogger,
273
+ );
274
+ expect(identity?.identityId).toBe(carolId);
275
+ expect(identity?.callerClass).toBe("user");
276
+ });
277
+
278
+ it("the run credential the server mints for a live run, presented on the run's own read, is ADMITTED as the person who created it", async () => {
279
+ await seedWorkflowExecution(
280
+ "wex_live",
281
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
282
+ );
283
+ const credential = server.runnerAuthService.mintRunCredential("wex_live");
284
+
285
+ const identity = await authenticateBearerToken(
286
+ server.identityVerifiers,
287
+ credential,
288
+ silentLogger,
289
+ );
290
+ expect(identity).toMatchObject({
291
+ identityId: carolId,
292
+ callerClass: "runner",
293
+ email: "carol@example.com",
294
+ rawToken: credential,
295
+ });
296
+
297
+ const asRunner = createClient(
298
+ WorkflowExecutionQueryController,
299
+ transportFor(port, credential),
300
+ );
301
+ const row = await asRunner.get({ value: "wex_live" });
302
+ expect(row.metadata?.id).toBe("wex_live");
303
+ });
304
+
305
+ it("once the run is over, the same credential is UNAUTHENTICATED with the liveness sentence — validity is the row's", async () => {
306
+ await seedWorkflowExecution(
307
+ "wex_done",
308
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
309
+ );
310
+ const credential = server.runnerAuthService.mintRunCredential("wex_done");
311
+ const asRunner = createClient(
312
+ WorkflowExecutionQueryController,
313
+ transportFor(port, credential),
314
+ );
315
+ expect((await asRunner.get({ value: "wex_done" })).metadata?.id).toBe(
316
+ "wex_done",
317
+ );
318
+
319
+ await seedWorkflowExecution("wex_done", ExecutionPhase.EXECUTION_COMPLETED);
320
+ const failure = await failureOf(asRunner.get({ value: "wex_done" }));
321
+ expect(failure.code).toBe(Code.Unauthenticated);
322
+ expect(failure.rawMessage).toBe(RUNNER_CREDENTIAL_NOT_LIVE_MESSAGE);
323
+ });
324
+
325
+ it("a credential bound to one run does not open another — the binding is the capability", async () => {
326
+ await seedWorkflowExecution(
327
+ "wex_mine",
328
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
329
+ );
330
+ await seedWorkflowExecution(
331
+ "wex_other",
332
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
333
+ );
334
+ const credential = server.runnerAuthService.mintRunCredential("wex_mine");
335
+ const asRunner = createClient(
336
+ WorkflowExecutionQueryController,
337
+ transportFor(port, credential),
338
+ );
339
+ // Admitted as Carol, who created both rows — so the read of the OTHER
340
+ // run succeeds AS CAROL: the credential narrows WHO the runner is,
341
+ // not WHICH rows that person may read. Pinned so the reach is stated,
342
+ // not discovered (the cloud's sandbox token has the same reach).
343
+ expect((await asRunner.get({ value: "wex_other" })).metadata?.id).toBe(
344
+ "wex_other",
345
+ );
346
+ });
347
+
348
+ describe("the exchange is a mint gate", () => {
349
+ let bruceToken: string;
350
+
351
+ beforeAll(async () => {
352
+ // A second signed-in person. Provisioning after Carol makes Bruce a
353
+ // member of every organization she created — the roles rule — which
354
+ // is exactly the caller the gate must refuse: a legitimate member
355
+ // asking for someone else's run.
356
+ bruceToken = await issuer.mint("auth0|bruce", "bruce@example.com");
357
+ const bruce = createClient(
358
+ IdentityAccountCommandController,
359
+ transportFor(port, bruceToken),
360
+ );
361
+ expect((await bruce.provisionMyAccount({})).metadata?.id).not.toBe("");
362
+ await seedWorkflowExecution(
363
+ "wex_carols",
364
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
365
+ );
366
+ });
367
+
368
+ it("another member asking for Carol's run is PERMISSION_DENIED — the impersonation path, closed", async () => {
369
+ const platform = createClient(
370
+ PlatformQueryController,
371
+ transportFor(port, bruceToken),
372
+ );
373
+ const failure = await failureOf(
374
+ platform.getRunnerScopedToken({
375
+ scope: { case: "workflowExecutionId", value: "wex_carols" },
376
+ }),
377
+ );
378
+ expect(failure.code).toBe(Code.PermissionDenied);
379
+ expect(failure.rawMessage).toBe(RUN_CREDENTIAL_NOT_RUNS_PERSON_MESSAGE);
380
+ });
381
+
382
+ it("the run's own person is minted a run credential that admits her on the wire", async () => {
383
+ const platform = createClient(
384
+ PlatformQueryController,
385
+ transportFor(
386
+ port,
387
+ await issuer.mint("auth0|carol", "carol@example.com"),
388
+ ),
389
+ );
390
+ const minted = await platform.getRunnerScopedToken({
391
+ scope: { case: "workflowExecutionId", value: "wex_carols" },
392
+ });
393
+ expect(minted.runnerScopedToken).not.toBe("");
394
+ expect(minted.tokenType).toBe("Bearer");
395
+ // A run credential has no clock; the proto default says so.
396
+ expect(minted.expiresInSeconds).toBe(0);
397
+ expect(isClockedToken(minted.runnerScopedToken)).toBe(false);
398
+
399
+ const identity = await authenticateBearerToken(
400
+ server.identityVerifiers,
401
+ minted.runnerScopedToken,
402
+ silentLogger,
403
+ );
404
+ expect(identity).toMatchObject({
405
+ identityId: carolId,
406
+ callerClass: "runner",
407
+ });
408
+ });
409
+
410
+ it("a run that does not exist is NOT_FOUND with the copy a `get` would answer — never a hint about who may hold it", async () => {
411
+ const platform = createClient(
412
+ PlatformQueryController,
413
+ transportFor(port, bruceToken),
414
+ );
415
+ const failure = await failureOf(
416
+ platform.getRunnerScopedToken({
417
+ scope: { case: "workflowExecutionId", value: "wex_nowhere" },
418
+ }),
419
+ );
420
+ expect(failure.code).toBe(Code.NotFound);
421
+ expect(failure.rawMessage).toBe(
422
+ "WorkflowExecution not found: wex_nowhere",
423
+ );
424
+ });
425
+ });
426
+ });
427
+
428
+ describe("trusted-local: no runner verifier is composed", () => {
429
+ let dir: string;
430
+ let server: ComposedServer;
431
+
432
+ beforeAll(async () => {
433
+ dir = mkdtempSync(path.join(tmpdir(), "runner-subject-local-"));
434
+ server = await composeServer({
435
+ config: loadConfig(baseConfig(dir)),
436
+ logger: silentLogger,
437
+ extensions: [],
438
+ portOverride: 0,
439
+ host: "127.0.0.1",
440
+ });
441
+ });
442
+
443
+ afterAll(async () => {
444
+ await server.shutdown();
445
+ rmSync(dir, { recursive: true, force: true });
446
+ });
447
+
448
+ it("the chain carries no runner entry — nothing to delegate on a single-operator server", () => {
449
+ expect(
450
+ server.identityVerifiers.map((verifier) => verifier.name),
451
+ ).not.toContain(RUNNER_VERIFIER_NAME);
452
+ });
453
+
454
+ it("the server's run credential is an UNCLAIMED bearer here — the exchange token stays the decrypt-lane discriminator it is today", async () => {
455
+ const credential = server.runnerAuthService.mintRunCredential("wex_local");
456
+ expect(
457
+ await authenticateBearerToken(
458
+ server.identityVerifiers,
459
+ credential,
460
+ silentLogger,
461
+ ),
462
+ ).toBeUndefined();
463
+ });
464
+ });
465
+
466
+ describe("a unit's own Authorizer: no runner verifier is composed", () => {
467
+ let dir: string;
468
+ let server: ComposedServer;
469
+
470
+ beforeAll(async () => {
471
+ dir = mkdtempSync(path.join(tmpdir(), "runner-subject-unit-"));
472
+ const unit: ServerExtension = {
473
+ name: "fake-unit-with-authorizer",
474
+ requireAuthentication: true,
475
+ identityVerifiers: [fakeVerifier],
476
+ authorizer: { authorize: () => Promise.resolve({ kind: "allow" }) },
477
+ };
478
+ server = await composeServer({
479
+ config: loadConfig(baseConfig(dir)),
480
+ logger: silentLogger,
481
+ extensions: [unit],
482
+ portOverride: 0,
483
+ host: "127.0.0.1",
484
+ });
485
+ });
486
+
487
+ afterAll(async () => {
488
+ await server.shutdown();
489
+ rmSync(dir, { recursive: true, force: true });
490
+ });
491
+
492
+ it("the chain is apikey then the unit's verifiers — the composition owns its credential story", () => {
493
+ const names = server.identityVerifiers.map((verifier) => verifier.name);
494
+ expect(names).toEqual(["apikey", fakeVerifier.name]);
495
+ expect(names).not.toContain(RUNNER_VERIFIER_NAME);
496
+ });
497
+ });