@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,541 @@
1
+ /**
2
+ * Pins the runner-subject identity verifier on both store drivers: the
3
+ * OSS execution-scoped runner token, presented as a bearer under the
4
+ * built-in authorization posture, admits its bearer AS THE HUMAN WHO
5
+ * ASKED — for a run, the person the execution row's creator stamp
6
+ * names, for exactly as long as that execution lives; for an MCP
7
+ * connect, the person the connect's ExecutionContext row was created by,
8
+ * for as long as that row exists — in the `runner` caller class. A
9
+ * clockless token bound to a connect is a shape no mint produces and is
10
+ * refused as the credential it is not.
11
+ *
12
+ * Why a verifier reads the store: the token is a capability for one
13
+ * execution, not an identity assertion. Its claims name only the
14
+ * execution; the server resolves the person from the row (the account
15
+ * id stamp since 3.15.0, the raw issuer subject the 3.14.x verifiers
16
+ * stamped — the schedule fire caller's two shapes) and mints the account
17
+ * acting as itself, so the audit actor carries the person's email and
18
+ * display name that no token could. Validity is the row's: a terminal or
19
+ * missing execution refuses; a run credential carries no `exp`, because
20
+ * runs wait on humans with no timeout and outlive any clock, and a
21
+ * present `exp` in the past still refuses (the exchange lane's tokens
22
+ * keep their clock).
23
+ *
24
+ * Claim rule: exactly `token_type === "execution_scoped"` read from the
25
+ * UNTRUSTED payload; every other shape passes (the OIDC verifier's JWTs,
26
+ * the API-key lane's `stk_`, garbage). A token that names the type and
27
+ * fails the HMAC is claimed and refused — never passed to a laxer
28
+ * verifier (the identity.ts contract). Refusals are UNAUTHENTICATED with
29
+ * one sentence for the token arms and one for the resolution arms (a
30
+ * finer reason invites branching; the runner is the only caller and its
31
+ * operator reads the log). A store fault propagates as the fault it is.
32
+ *
33
+ * Written failing on 2026-09-16, before src/runnerauth/runner-subject-verifier.ts
34
+ * exists; the module that follows turns it green. The composed three-posture proof is
35
+ * runner-subject-composed.test.ts; the capabilities the same token feeds
36
+ * are built-in-runner-credential-provider.test.ts.
37
+ */
38
+ import { randomBytes } from "node:crypto";
39
+
40
+ import { create } from "@bufbuild/protobuf";
41
+ import { Code, ConnectError } from "@connectrpc/connect";
42
+ import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
43
+
44
+ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
45
+ import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
46
+ import { ExecutionContextSchema } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/api_pb";
47
+ import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
48
+ import { ExecutionPhase as WorkflowExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/enum_pb";
49
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
50
+ import { IdentityAccountSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
51
+ import { IdentityAccountProvisioningMode } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/enum_pb";
52
+
53
+ import {
54
+ driverFixtures,
55
+ dropPostgresFixture,
56
+ } from "../../authorization/__tests__/drivers.js";
57
+ import type { OpenedStore } from "../../authorization/__tests__/drivers.js";
58
+ import { accountIdFor } from "../../domain/identityaccount/constants.js";
59
+ import { newResourceIdentityAccountStore } from "../../domain/identityaccount/resource-store.js";
60
+ import { newConnectExecutionId } from "../../domain/mcpserver/connect-execution-id.js";
61
+ import type { IdentityAccountStore } from "../../domain/identityaccount/store.js";
62
+ import { newOrganizationOnlyGrantScope } from "../../domain/iampolicy/grant-scope.js";
63
+ import {
64
+ RUN_CREDENTIAL_GRACE_AFTER_TERMINAL_MS,
65
+ RUNNER_CREDENTIAL_INVALID_MESSAGE,
66
+ RUNNER_CREDENTIAL_NOT_LIVE_MESSAGE,
67
+ RUNNER_VERIFIER_NAME,
68
+ } from "../constants.js";
69
+ import { newExecutionScopedRunnerCredentialProvider } from "../runner-credential-provider.js";
70
+ import { newRunnerSubjectIdentityVerifier } from "../runner-subject-verifier.js";
71
+ import {
72
+ RunnerAuthService,
73
+ TOKEN_TYPE_EXECUTION_SCOPED,
74
+ } from "../runnerauth.js";
75
+
76
+ const KEY = randomBytes(32);
77
+ const SUBJECT = "auth0|carol";
78
+ const CAROL = accountIdFor(SUBJECT);
79
+ const CAROL_IDENTITY = {
80
+ identityId: CAROL,
81
+ callerClass: "runner",
82
+ issuer: "",
83
+ email: "carol@example.com",
84
+ displayName: "Carol Danvers",
85
+ };
86
+
87
+ const segment = (value: unknown): string =>
88
+ Buffer.from(JSON.stringify(value)).toString("base64url");
89
+
90
+ /** A three-segment token whose payload we control and whose signature is NOT the server's. */
91
+ function forgedToken(payload: Record<string, unknown>): string {
92
+ return `${segment({ alg: "HS256", typ: "JWT" })}.${segment(payload)}.${segment("not-the-servers-mac")}`;
93
+ }
94
+
95
+ async function rejectionOf(promise: Promise<unknown>): Promise<unknown> {
96
+ return promise.then(
97
+ () => {
98
+ throw new Error("expected rejection");
99
+ },
100
+ (error: unknown) => error,
101
+ );
102
+ }
103
+
104
+ function expectUnauthenticated(failure: unknown, message: string): void {
105
+ expect(failure).toBeInstanceOf(ConnectError);
106
+ expect((failure as ConnectError).code).toBe(Code.Unauthenticated);
107
+ expect((failure as ConnectError).rawMessage).toBe(message);
108
+ }
109
+
110
+ afterAll(dropPostgresFixture);
111
+
112
+ describe.each(
113
+ driverFixtures([
114
+ ApiResourceKind.agent_execution,
115
+ ApiResourceKind.workflow_execution,
116
+ ApiResourceKind.execution_context,
117
+ ApiResourceKind.identity_account,
118
+ ]),
119
+ )("the runner-subject verifier on $name", (fixture) => {
120
+ describe.skipIf(fixture.skip)("verify", () => {
121
+ let opened: OpenedStore;
122
+ let accounts: IdentityAccountStore;
123
+ const service = RunnerAuthService.create(KEY);
124
+ const credentials = newExecutionScopedRunnerCredentialProvider(service);
125
+
126
+ beforeEach(async () => {
127
+ opened = await fixture.open();
128
+ accounts = newResourceIdentityAccountStore(opened.store);
129
+ await accounts.save(
130
+ create(IdentityAccountSchema, {
131
+ metadata: { id: CAROL, name: "Carol Danvers" },
132
+ spec: {
133
+ idpId: SUBJECT,
134
+ email: "carol@example.com",
135
+ provisioningMode: IdentityAccountProvisioningMode.direct,
136
+ },
137
+ }),
138
+ );
139
+ });
140
+
141
+ afterEach(async () => {
142
+ await opened.close();
143
+ });
144
+
145
+ async function agentExecution(
146
+ id: string,
147
+ createdBy: string,
148
+ phase: ExecutionPhase = ExecutionPhase.EXECUTION_IN_PROGRESS,
149
+ ): Promise<void> {
150
+ await opened.store.saveResource(
151
+ ApiResourceKind.agent_execution,
152
+ id,
153
+ AgentExecutionSchema,
154
+ create(AgentExecutionSchema, {
155
+ metadata: { id, name: id, org: "acme" },
156
+ status: {
157
+ phase,
158
+ audit: { specAudit: { createdBy: { id: createdBy } } },
159
+ },
160
+ }),
161
+ );
162
+ }
163
+
164
+ async function workflowExecution(
165
+ id: string,
166
+ createdBy: string,
167
+ phase: WorkflowExecutionPhase = WorkflowExecutionPhase.EXECUTION_IN_PROGRESS,
168
+ ): Promise<void> {
169
+ await opened.store.saveResource(
170
+ ApiResourceKind.workflow_execution,
171
+ id,
172
+ WorkflowExecutionSchema,
173
+ create(WorkflowExecutionSchema, {
174
+ metadata: { id, name: id, org: "acme" },
175
+ status: {
176
+ phase,
177
+ audit: { specAudit: { createdBy: { id: createdBy } } },
178
+ },
179
+ }),
180
+ );
181
+ }
182
+
183
+ /** The connect lane's ephemeral EC, created as `createdBy` (connect.ts `prepareConnect`). */
184
+ async function connectContext(
185
+ executionId: string,
186
+ createdBy: string,
187
+ ): Promise<void> {
188
+ await opened.store.saveResource(
189
+ ApiResourceKind.execution_context,
190
+ `ectx_${executionId}`,
191
+ ExecutionContextSchema,
192
+ create(ExecutionContextSchema, {
193
+ metadata: {
194
+ id: `ectx_${executionId}`,
195
+ name: `exec-ctx-${executionId}`,
196
+ org: "acme",
197
+ },
198
+ spec: { executionId },
199
+ status: { audit: { specAudit: { createdBy: { id: createdBy } } } },
200
+ }),
201
+ );
202
+ }
203
+
204
+ function verifier() {
205
+ return newRunnerSubjectIdentityVerifier({
206
+ store: opened.store,
207
+ accounts,
208
+ credentials,
209
+ });
210
+ }
211
+
212
+ it("is named for the boot log and the auth failures", () => {
213
+ expect(verifier().name).toBe(RUNNER_VERIFIER_NAME);
214
+ expect(RUNNER_VERIFIER_NAME).toBe("runner");
215
+ });
216
+
217
+ describe("claim-or-pass", () => {
218
+ it.each([
219
+ ["an API key", "stk_notajwt"],
220
+ ["garbage", "not.even.close.to.a.token"],
221
+ [
222
+ "an OIDC-shaped JWT (no token_type claim)",
223
+ forgedToken({
224
+ sub: "auth0|someone",
225
+ iss: "https://issuer.example.com/",
226
+ }),
227
+ ],
228
+ [
229
+ "another edition's runner token (a token_type that is not ours)",
230
+ forgedToken({
231
+ token_type: "sandbox",
232
+ session_id: "ses_x",
233
+ sub: "auth0|someone",
234
+ }),
235
+ ],
236
+ ])(
237
+ "passes (null) on %s — the next verifier's business",
238
+ async (_label, token) => {
239
+ expect(await verifier().verify(token)).toBeNull();
240
+ },
241
+ );
242
+
243
+ it("a token that names our type and fails the HMAC is CLAIMED and refused — never passed to a laxer verifier", async () => {
244
+ await agentExecution("aex_forged", CAROL);
245
+ const failure = await rejectionOf(
246
+ verifier().verify(
247
+ forgedToken({
248
+ token_type: TOKEN_TYPE_EXECUTION_SCOPED,
249
+ execution_id: "aex_forged",
250
+ }),
251
+ ),
252
+ );
253
+ expectUnauthenticated(failure, RUNNER_CREDENTIAL_INVALID_MESSAGE);
254
+ });
255
+ });
256
+
257
+ describe("the run credential admits its bearer as the run's human", () => {
258
+ it("an agent execution stamped with the creator's account id: the account acting as itself, class runner, the raw token carried", async () => {
259
+ await agentExecution("aex_by_id", CAROL);
260
+ const token = service.mintRunCredential("aex_by_id");
261
+ expect(await verifier().verify(token)).toEqual({
262
+ ...CAROL_IDENTITY,
263
+ rawToken: token,
264
+ });
265
+ });
266
+
267
+ it("an agent execution stamped with the raw issuer subject (the 3.14.x shape) resolves to the same account", async () => {
268
+ await agentExecution("aex_by_sub", SUBJECT);
269
+ const identity = await verifier().verify(
270
+ service.mintRunCredential("aex_by_sub"),
271
+ );
272
+ expect(identity?.identityId).toBe(CAROL);
273
+ expect(identity?.callerClass).toBe("runner");
274
+ });
275
+
276
+ it("a workflow execution binding resolves the same way — the kind is read off the id's prefix, no second read", async () => {
277
+ await workflowExecution("wex_by_id", CAROL);
278
+ const token = service.mintRunCredential("wex_by_id");
279
+ expect(await verifier().verify(token)).toEqual({
280
+ ...CAROL_IDENTITY,
281
+ rawToken: token,
282
+ });
283
+ });
284
+
285
+ it("a PENDING execution is live — the runner's first read happens before any phase write", async () => {
286
+ await agentExecution(
287
+ "aex_pending",
288
+ CAROL,
289
+ ExecutionPhase.EXECUTION_PENDING,
290
+ );
291
+ expect(
292
+ (await verifier().verify(service.mintRunCredential("aex_pending")))
293
+ ?.identityId,
294
+ ).toBe(CAROL);
295
+ });
296
+
297
+ it("the exchange lane's clocked token (a present, future exp) admits the same way while its clock runs", async () => {
298
+ await agentExecution("aex_clocked", CAROL);
299
+ const minted = service.mint("aex_clocked", 300);
300
+ expect((await verifier().verify(minted.token))?.identityId).toBe(CAROL);
301
+ });
302
+ });
303
+
304
+ describe("the connect token admits its bearer as the person who asked for the connect", () => {
305
+ const connectId = newConnectExecutionId("mcps_carols");
306
+
307
+ it("the connect lane's clocked token resolves through the connect's ExecutionContext row: its creator, class runner", async () => {
308
+ await connectContext(connectId, CAROL);
309
+ const { token } = service.mint(connectId, 300);
310
+ expect(await verifier().verify(token)).toEqual({
311
+ ...CAROL_IDENTITY,
312
+ rawToken: token,
313
+ });
314
+ });
315
+
316
+ it("once the connect settles and its row is gone, the token is the token sentence — it names no row", async () => {
317
+ const { token } = service.mint(connectId, 300);
318
+ expectUnauthenticated(
319
+ await rejectionOf(verifier().verify(token)),
320
+ RUNNER_CREDENTIAL_INVALID_MESSAGE,
321
+ );
322
+ });
323
+
324
+ it("a clockless token bound to a connect is refused even while the row exists — no mint produces that shape", async () => {
325
+ await connectContext(connectId, CAROL);
326
+ expectUnauthenticated(
327
+ await rejectionOf(
328
+ verifier().verify(service.mintRunCredential(connectId)),
329
+ ),
330
+ RUNNER_CREDENTIAL_INVALID_MESSAGE,
331
+ );
332
+ });
333
+
334
+ it("a connect row created by nobody the server recognizes is the liveness sentence — a discovery for no one", async () => {
335
+ await connectContext(connectId, "system");
336
+ expectUnauthenticated(
337
+ await rejectionOf(
338
+ verifier().verify(service.mint(connectId, 300).token),
339
+ ),
340
+ RUNNER_CREDENTIAL_NOT_LIVE_MESSAGE,
341
+ );
342
+ });
343
+ });
344
+
345
+ describe("validity is the row's, not a clock's", () => {
346
+ it.each([
347
+ ["COMPLETED", ExecutionPhase.EXECUTION_COMPLETED],
348
+ ["FAILED", ExecutionPhase.EXECUTION_FAILED],
349
+ ["CANCELLED", ExecutionPhase.EXECUTION_CANCELLED],
350
+ ])(
351
+ "a %s execution's credential is refused with the liveness sentence",
352
+ async (_label, phase) => {
353
+ await agentExecution("aex_over", CAROL, phase);
354
+ const failure = await rejectionOf(
355
+ verifier().verify(service.mintRunCredential("aex_over")),
356
+ );
357
+ expectUnauthenticated(failure, RUNNER_CREDENTIAL_NOT_LIVE_MESSAGE);
358
+ },
359
+ );
360
+
361
+ it("a run that ended moments ago still admits — the grace covers the writes that trail the terminal stamp", async () => {
362
+ await opened.store.saveResource(
363
+ ApiResourceKind.agent_execution,
364
+ "aex_just_over",
365
+ AgentExecutionSchema,
366
+ create(AgentExecutionSchema, {
367
+ metadata: {
368
+ id: "aex_just_over",
369
+ name: "aex_just_over",
370
+ org: "acme",
371
+ },
372
+ status: {
373
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
374
+ completedAt: new Date().toISOString(),
375
+ audit: { specAudit: { createdBy: { id: CAROL } } },
376
+ },
377
+ }),
378
+ );
379
+ expect(
380
+ (await verifier().verify(service.mintRunCredential("aex_just_over")))
381
+ ?.identityId,
382
+ ).toBe(CAROL);
383
+ });
384
+
385
+ it("a run that ended past the grace is refused — the grace is a constant, not a second clock", async () => {
386
+ await opened.store.saveResource(
387
+ ApiResourceKind.agent_execution,
388
+ "aex_long_over",
389
+ AgentExecutionSchema,
390
+ create(AgentExecutionSchema, {
391
+ metadata: {
392
+ id: "aex_long_over",
393
+ name: "aex_long_over",
394
+ org: "acme",
395
+ },
396
+ status: {
397
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
398
+ completedAt: new Date(
399
+ Date.now() - RUN_CREDENTIAL_GRACE_AFTER_TERMINAL_MS - 1000,
400
+ ).toISOString(),
401
+ audit: { specAudit: { createdBy: { id: CAROL } } },
402
+ },
403
+ }),
404
+ );
405
+ expectUnauthenticated(
406
+ await rejectionOf(
407
+ verifier().verify(service.mintRunCredential("aex_long_over")),
408
+ ),
409
+ RUNNER_CREDENTIAL_NOT_LIVE_MESSAGE,
410
+ );
411
+ });
412
+
413
+ it("a terminal WORKFLOW execution's credential is refused the same way", async () => {
414
+ await workflowExecution(
415
+ "wex_over",
416
+ CAROL,
417
+ WorkflowExecutionPhase.EXECUTION_COMPLETED,
418
+ );
419
+ const failure = await rejectionOf(
420
+ verifier().verify(service.mintRunCredential("wex_over")),
421
+ );
422
+ expectUnauthenticated(failure, RUNNER_CREDENTIAL_NOT_LIVE_MESSAGE);
423
+ });
424
+
425
+ it("an execution that does not exist is UNAUTHENTICATED with the token sentence — the credential is invalid, the run is not the caller's to learn about", async () => {
426
+ const failure = await rejectionOf(
427
+ verifier().verify(service.mintRunCredential("aex_missing")),
428
+ );
429
+ expectUnauthenticated(failure, RUNNER_CREDENTIAL_INVALID_MESSAGE);
430
+ });
431
+
432
+ it("a token bound to an id whose prefix names no kind this server serves is the token sentence, not a store read", async () => {
433
+ const failure = await rejectionOf(
434
+ verifier().verify(service.mintRunCredential("zzz_unknown_kind")),
435
+ );
436
+ expectUnauthenticated(failure, RUNNER_CREDENTIAL_INVALID_MESSAGE);
437
+ });
438
+
439
+ it("a present exp in the past is refused even while the row lives — the exchange lane's clock still counts", async () => {
440
+ await agentExecution("aex_expired", CAROL);
441
+ const minted = service.mint("aex_expired", 1);
442
+ await new Promise((resolve) => setTimeout(resolve, 1100));
443
+ expectUnauthenticated(
444
+ await rejectionOf(verifier().verify(minted.token)),
445
+ RUNNER_CREDENTIAL_INVALID_MESSAGE,
446
+ );
447
+ });
448
+ });
449
+
450
+ describe("a run whose stamp names no person is nobody's", () => {
451
+ it.each([
452
+ ["the empty stamp", ""],
453
+ ["the laptop's placeholder", "system"],
454
+ ["a trusted-local email stamp", "operator@example.com"],
455
+ ["a deleted account", accountIdFor("auth0|gone")],
456
+ ])(
457
+ "%s refuses with the liveness sentence — deterministic, never a retry",
458
+ async (_label, stamp) => {
459
+ await agentExecution("aex_nobody", stamp);
460
+ const failure = await rejectionOf(
461
+ verifier().verify(service.mintRunCredential("aex_nobody")),
462
+ );
463
+ expectUnauthenticated(failure, RUNNER_CREDENTIAL_NOT_LIVE_MESSAGE);
464
+ },
465
+ );
466
+ });
467
+
468
+ describe("faults are faults", () => {
469
+ it("a store fault loading the execution propagates as the error it is — the chassis maps INTERNAL, never a credential rejection", async () => {
470
+ await agentExecution("aex_fault", CAROL);
471
+ const fault = new Error("connection reset");
472
+ const failing = newRunnerSubjectIdentityVerifier({
473
+ store: { ...opened.store, getResource: () => Promise.reject(fault) },
474
+ accounts,
475
+ credentials,
476
+ });
477
+ await expect(
478
+ failing.verify(service.mintRunCredential("aex_fault")),
479
+ ).rejects.toBe(fault);
480
+ });
481
+
482
+ it("a keyless service refuses every token that names our type (fail closed: without a key no token can be genuine)", async () => {
483
+ await agentExecution("aex_keyless", CAROL);
484
+ const keyless = newRunnerSubjectIdentityVerifier({
485
+ store: opened.store,
486
+ accounts,
487
+ credentials: newExecutionScopedRunnerCredentialProvider(
488
+ RunnerAuthService.create(undefined),
489
+ ),
490
+ });
491
+ const failure = await rejectionOf(
492
+ keyless.verify(service.mintRunCredential("aex_keyless")),
493
+ );
494
+ expectUnauthenticated(failure, RUNNER_CREDENTIAL_INVALID_MESSAGE);
495
+ });
496
+ });
497
+ });
498
+ });
499
+
500
+ describe("the person admitted is the person the model lets report", () => {
501
+ it("open source grants nobody a role on a session, so a run's creator is its session's owner — the fact the verifier's header rests on", () => {
502
+ // `agent_execution.can_edit` is `owner from session`; this verifier
503
+ // admits the execution's CREATOR. They are one person only while a
504
+ // session's viewers are its owner alone. The contract lists `viewer`
505
+ // as grantable on a session; open source's grant scope refuses it.
506
+ expect(
507
+ newOrganizationOnlyGrantScope().grantableRoles(ApiResourceKind.session),
508
+ ).toEqual([]);
509
+ });
510
+ });
511
+
512
+ describe("the run credential's shape (the service, no store)", () => {
513
+ const service = RunnerAuthService.create(KEY);
514
+
515
+ it("carries the execution_scoped type and the binding, and NO exp — its validity is the run's, not a clock's", () => {
516
+ const token = service.mintRunCredential("aex_shape");
517
+ const payload = JSON.parse(
518
+ Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"),
519
+ ) as Record<string, unknown>;
520
+ expect(payload["token_type"]).toBe(TOKEN_TYPE_EXECUTION_SCOPED);
521
+ expect(payload["execution_id"]).toBe("aex_shape");
522
+ expect(typeof payload["iat"]).toBe("number");
523
+ expect(payload).not.toHaveProperty("exp");
524
+ });
525
+
526
+ it("verify accepts the run credential and returns its binding — the decrypt lane keeps its binding-equality decision over it", () => {
527
+ expect(service.verify(service.mintRunCredential("aex_shape"))).toBe(
528
+ "aex_shape",
529
+ );
530
+ });
531
+
532
+ it("verify still refuses a present exp in the past — the exchange lane's tokens keep their clock", async () => {
533
+ const minted = service.mint("aex_shape", 1);
534
+ await new Promise((resolve) => setTimeout(resolve, 1100));
535
+ expect(() => service.verify(minted.token)).toThrow();
536
+ });
537
+
538
+ it("refuses to mint for an empty binding", () => {
539
+ expect(() => service.mintRunCredential("")).toThrow();
540
+ });
541
+ });