@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
@@ -14,21 +14,37 @@
14
14
  * distinguishes itself with an execution-scoped token minted by
15
15
  * getRunnerScopedToken and presented as a Bearer authorization header
16
16
  * (the same header shape a cloud runner uses for its sandbox credential).
17
- * Decrypt requires the FULL binding: a valid, unexpired token whose
18
- * execution_id claim equals this EC's spec.execution_id. Everything else
19
- * — no header, malformed or expired token, or a token minted for a
20
- * different execution — falls closed to the same redaction
21
- * get/getByReference apply, as a SUCCESSFUL response, not an error.
17
+ * Decrypt requires the FULL binding: a valid token whose execution_id
18
+ * claim equals this EC's spec.execution_id, and — for a RUN credential,
19
+ * the no-`exp` token the dispatch path hands the runner — a bound
20
+ * execution that is still live (runnerauth/bound-execution.ts: not
21
+ * terminal, or ended within the grace). Everything else — no header, a
22
+ * malformed or expired token, a token minted for a different execution,
23
+ * a run credential whose run is over — falls closed to the same
24
+ * redaction get/getByReference apply, as a SUCCESSFUL response, not an
25
+ * error.
22
26
  *
23
- * Token verification lives HERE, in the domain, not on the identity
24
- * chassis (O2, 20260827.01): the runner token is a lane discriminator,
25
- * not a caller identity — the chassis deliberately lets it fall through
26
- * to the trusted-local identity (ruling Q6), and this is the one RPC
27
- * that reads the raw header — exactly the consumer the runnerauth module
28
- * header reserves ("the executioncontext resolve step"). The
29
- * redaction-as-success contract is pinned by the conformance suites and
30
- * must survive every future verifier: a runner token is NEVER an
31
- * authentication credential.
27
+ * The decrypt decision lives HERE, in the domain, whatever the identity
28
+ * chassis makes of the token. Under trusted-local no verifier claims it
29
+ * and this is the one RPC that reads the raw header (the consumer the
30
+ * runnerauth module header reserves, "the executioncontext resolve
31
+ * step"). Under the built-in authorization posture the runner-subject
32
+ * verifier ALSO admits the same token as the run's human at position 1
33
+ * (runnerauth.ts header, 2026-09-16) — that decides WHO is calling and
34
+ * whether they may read the row; whether the row's secrets are decrypted
35
+ * for them is still this lane's binding check, so a person who can view
36
+ * an execution's context never receives its plaintext by holding a
37
+ * credential for a different one. The redaction-as-success contract is
38
+ * pinned by the conformance suites and holds on every arm.
39
+ *
40
+ * Why liveness is read only for a no-`exp` token: a clocked token's
41
+ * validity was decided by `verify` (its clock) and its binding, and the
42
+ * ExecutionContext it opens need not name an execution row at all — the
43
+ * connect lane's token opens an EC for an MCP discovery, and the rosters
44
+ * pin a clocked token decrypting an EC whose execution id names no row.
45
+ * A run credential has no clock; the row is the only thing that can end
46
+ * it, and a plaintext token in Temporal history must not decrypt a
47
+ * finished run's secrets for good.
32
48
  *
33
49
  * # Decrypt error doctrine (the oss#405 runtime-resolution doctrine,
34
50
  * # arms per the two-armed taxonomy in encryption/errors.ts)
@@ -56,8 +72,16 @@ import { EncryptionUnavailableError } from "../../encryption/encryption.js";
56
72
  import type { SecretService } from "../../encryption/encryption.js";
57
73
  import { internalError } from "../../pipeline/errors.js";
58
74
  import { parseBearerToken } from "../../pipeline/interceptors/auth.js";
75
+ import {
76
+ bindsARun,
77
+ loadBoundExecution,
78
+ } from "../../runnerauth/bound-execution.js";
79
+ import type { BoundExecutionStore } from "../../runnerauth/bound-execution.js";
59
80
  import type { RunnerCredentialProvider } from "../../runnerauth/runner-credential-provider.js";
60
- import { TOKEN_TYPE_EXECUTION_SCOPED } from "../../runnerauth/runnerauth.js";
81
+ import {
82
+ isClockedToken,
83
+ TOKEN_TYPE_EXECUTION_SCOPED,
84
+ } from "../../runnerauth/runnerauth.js";
61
85
  import { encryptionKeyMissingMessage } from "./constants.js";
62
86
  import { redactExecutionContextSecrets } from "./redact.js";
63
87
 
@@ -65,6 +89,8 @@ export interface ResolveValuesDeps {
65
89
  readonly logger: Logger;
66
90
  readonly secretService: SecretService;
67
91
  readonly runnerAuthService: RunnerCredentialProvider;
92
+ /** Where the bound execution lives — read for a run credential's liveness only. */
93
+ readonly store: BoundExecutionStore;
68
94
  }
69
95
 
70
96
  /**
@@ -123,15 +149,23 @@ async function runnerMayDecrypt(
123
149
  }
124
150
  }
125
151
 
126
- const executionId = verifyRunnerToken(deps, ctx, ec);
127
- if (executionId !== undefined) {
152
+ const token = bearerToken(ctx);
153
+ const executionId = verifyRunnerToken(deps, token, ec);
154
+ if (executionId === undefined) {
155
+ return false;
156
+ }
157
+ if (!isClockedToken(token) && !(await runIsLive(deps, executionId))) {
128
158
  deps.logger.debug(
129
- "Scope-bound runner token presented - decrypting execution context secrets",
159
+ "Run credential presented for a run that is over - redacting execution context secrets",
130
160
  { executionId },
131
161
  );
132
- return true;
162
+ return false;
133
163
  }
134
- return false;
164
+ deps.logger.debug(
165
+ "Scope-bound runner token presented - decrypting execution context secrets",
166
+ { executionId },
167
+ );
168
+ return true;
135
169
  }
136
170
 
137
171
  /**
@@ -148,10 +182,9 @@ async function runnerMayDecrypt(
148
182
  */
149
183
  function verifyRunnerToken(
150
184
  deps: ResolveValuesDeps,
151
- ctx: HandlerContext,
185
+ token: string,
152
186
  ec: ExecutionContext,
153
187
  ): string | undefined {
154
- const token = bearerToken(ctx);
155
188
  if (token === "") {
156
189
  return undefined;
157
190
  }
@@ -183,6 +216,30 @@ function verifyRunnerToken(
183
216
  return tokenExecutionId;
184
217
  }
185
218
 
219
+ /**
220
+ * A run credential's liveness: the bound execution exists, IS a run
221
+ * (`bindsARun` — a clockless token bound to a connect is a shape no mint
222
+ * produces, refused here and by the verifier through the one predicate)
223
+ * and is live (runnerauth/bound-execution.ts). A missing row is "not
224
+ * live" — the credential opens nothing. A store fault is an
225
+ * infrastructure fault, sanitized here as every direct handler does (the
226
+ * store-fault doctrine): an outage must not read as a redaction decision,
227
+ * and the same store just served the row this read is for.
228
+ */
229
+ async function runIsLive(
230
+ deps: ResolveValuesDeps,
231
+ executionId: string,
232
+ ): Promise<boolean> {
233
+ try {
234
+ const execution = await loadBoundExecution(deps.store, executionId);
235
+ return (
236
+ execution !== undefined && bindsARun(execution.kind) && execution.live
237
+ );
238
+ } catch (error) {
239
+ throw internalError(error, "failed to load the run credential's execution");
240
+ }
241
+ }
242
+
186
243
  /**
187
244
  * The Bearer credential from the request's authorization header; empty
188
245
  * when absent or differently shaped. The parsing shape (Go's exact
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Pins `accountForStamp` (resolve.ts): the account a row's CREATOR STAMP
3
+ * names, read the two ways a stamp has been written — as an account id
4
+ * (rows stamped since 3.15.0) and as the raw issuer subject (rows the
5
+ * 3.14.x verifiers stamped) — stated ONCE so the schedule fire caller and
6
+ * the runner-subject verifier, which both make a server lane act as the
7
+ * person a row names, cannot resolve the same stamp two ways. The read
8
+ * order is `accountForCaller`'s: by id first, then the direct subject
9
+ * lookup.
10
+ *
11
+ * - an account-id stamp resolves by that id;
12
+ * - a raw-subject stamp resolves through the direct lookup;
13
+ * - the empty stamp is `undefined` with NO read — the store is never
14
+ * asked about "";
15
+ * - a stamp naming nobody (the laptop's `"system"`, a trusted-local
16
+ * email, a deleted account) is `undefined` — the CALLER decides what
17
+ * that means (the fire caller's deterministic refusal; the verifier's
18
+ * liveness sentence); this function never throws for it;
19
+ * - a store fault propagates as the same error object.
20
+ *
21
+ * Written failing on 2026-09-16, before the export exists; the change that follows
22
+ * extracts it from the fire caller's inline read (extract, do not copy).
23
+ */
24
+ import { create } from "@bufbuild/protobuf";
25
+ import { describe, expect, it, vi } from "vitest";
26
+
27
+ import { IdentityAccountSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
28
+ import { IdentityAccountProvisioningMode } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/enum_pb";
29
+
30
+ import { accountIdFor } from "../constants.js";
31
+ import { accountForStamp } from "../resolve.js";
32
+ import type { AccountsByCaller } from "../resolve.js";
33
+ import { fakeIdentityAccountStore } from "./support.js";
34
+
35
+ const SUBJECT = "auth0|carol";
36
+ const CAROL = accountIdFor(SUBJECT);
37
+
38
+ function seeded() {
39
+ const accounts = fakeIdentityAccountStore();
40
+ accounts.rows.set(
41
+ CAROL,
42
+ create(IdentityAccountSchema, {
43
+ metadata: { id: CAROL, name: "Carol Danvers" },
44
+ spec: {
45
+ idpId: SUBJECT,
46
+ email: "carol@example.com",
47
+ provisioningMode: IdentityAccountProvisioningMode.direct,
48
+ },
49
+ }),
50
+ );
51
+ return accounts;
52
+ }
53
+
54
+ describe("accountForStamp", () => {
55
+ it("an account-id stamp resolves by that id", async () => {
56
+ const account = await accountForStamp(seeded(), CAROL);
57
+ expect(account?.metadata?.id).toBe(CAROL);
58
+ expect(account?.spec?.email).toBe("carol@example.com");
59
+ });
60
+
61
+ it("a raw-subject stamp (the 3.14.x shape) resolves through the direct lookup to the same account", async () => {
62
+ expect((await accountForStamp(seeded(), SUBJECT))?.metadata?.id).toBe(
63
+ CAROL,
64
+ );
65
+ });
66
+
67
+ it('the empty stamp is undefined with NO read — the store is never asked about ""', async () => {
68
+ const findById = vi.fn(async () => undefined);
69
+ const findDirectByIdpId = vi.fn(async () => undefined);
70
+ const accounts: AccountsByCaller = { findById, findDirectByIdpId };
71
+ expect(await accountForStamp(accounts, "")).toBeUndefined();
72
+ expect(findById).not.toHaveBeenCalled();
73
+ expect(findDirectByIdpId).not.toHaveBeenCalled();
74
+ });
75
+
76
+ it.each([
77
+ ["the laptop's placeholder", "system"],
78
+ ["a trusted-local email stamp", "operator@example.com"],
79
+ ["a deleted account", accountIdFor("auth0|gone")],
80
+ ])(
81
+ "%s is undefined — the caller decides what nobody means",
82
+ async (_label, stamp) => {
83
+ expect(await accountForStamp(seeded(), stamp)).toBeUndefined();
84
+ },
85
+ );
86
+
87
+ it("reads by id first, then by subject — two primary-key reads at most, in accountForCaller's order", async () => {
88
+ const calls: string[] = [];
89
+ const accounts: AccountsByCaller = {
90
+ findById: async (id) => {
91
+ calls.push(`id:${id}`);
92
+ return undefined;
93
+ },
94
+ findDirectByIdpId: async (idpId) => {
95
+ calls.push(`sub:${idpId}`);
96
+ return undefined;
97
+ },
98
+ };
99
+ await accountForStamp(accounts, SUBJECT);
100
+ expect(calls).toEqual([`id:${SUBJECT}`, `sub:${SUBJECT}`]);
101
+ });
102
+
103
+ it("a store fault propagates as the same error object, never a credential rejection", async () => {
104
+ const fault = new Error("connection reset");
105
+ const accounts: AccountsByCaller = {
106
+ findById: () => Promise.reject(fault),
107
+ findDirectByIdpId: async () => undefined,
108
+ };
109
+ await expect(accountForStamp(accounts, CAROL)).rejects.toBe(fault);
110
+ });
111
+ });
@@ -1,30 +1,73 @@
1
1
  /**
2
- * An account acting as itself — the ONE construction of the caller
3
- * identity server code stamps when it acts on a person's behalf without
4
- * a token in hand. The shape is the trusted-local identity's
5
- * (pipeline/interceptors/auth.ts trustedLocalIdentityFor: no issuer, no
6
- * token; email and display name only when known) with the account id as
7
- * the principal, so the audit actor on every write it makes is the
8
- * account, named the way every other write names it.
2
+ * An account acting as itself — the ONE place server code builds the
3
+ * caller identity for a person it has resolved from a row rather than
4
+ * from the token that person presented. Two constructions, one shape:
5
+ * the account id is the principal, so the audit actor on every write
6
+ * names the account the way every other write names it, and email and
7
+ * display name ride along when the row knows them (the trusted-local
8
+ * identity's shape, pipeline/interceptors/auth.ts).
9
9
  *
10
- * Two lanes build it: the trusted-local boot ensure, which grants the
11
- * operator ownership AS their account (domain/iampolicy/membership.ts),
12
- * and the built-in schedule fire caller, which makes a fire act as the
13
- * schedule's creator (authorization/schedule-fire-caller.ts). Both ride
14
- * the in-process transport, whose interceptor stamps `origin:
15
- * "in-process"` on top — the transport-trust marker is never set here.
10
+ * `accountAsCaller(account)` — the IN-PROCESS lane: no issuer, no token,
11
+ * class `user`. Two consumers: the trusted-local boot ensure, which
12
+ * grants the operator ownership AS their account
13
+ * (domain/iampolicy/membership.ts), and the built-in schedule fire
14
+ * caller, which makes a fire act as the schedule's creator
15
+ * (authorization/schedule-fire-caller.ts). Both ride the in-process
16
+ * transport, whose interceptor stamps `origin: "in-process"` on top —
17
+ * the transport-trust marker is never set here.
16
18
  *
17
- * Why not a token: open source mints no credential for a person the
18
- * server is composing a request for; the identity is what the
19
- * propagation header carries, and every consumer that reads a subject off
20
- * the raw token (`idpIdOf`) is reached only through `accountForCaller`,
21
- * which resolves this identity by its id first and never asks the token.
19
+ * `accountAsRunnerCaller(account, rawToken)` — the WIRE lane: the
20
+ * runner-subject verifier (runnerauth/runner-subject-verifier.ts) admits
21
+ * the bearer of a run credential as the human whose run it is. Class
22
+ * `runner`, because the Authorizer's lane admission and the memory
23
+ * capture gate must tell a runner acting as a person from the person
24
+ * (extensions/identity.ts names the class for exactly this), and the
25
+ * verified token carried, because the provider's capabilities re-read the
26
+ * binding off `rawToken` (the cloud's own pattern) — one HMAC, no store
27
+ * read. No issuer: the server signed it.
28
+ *
29
+ * Why not a token for the in-process lane: open source mints no
30
+ * credential for a person the server is composing a request for; the
31
+ * identity is what the propagation header carries, and every consumer
32
+ * that reads a subject off the raw token (`idpIdOf`) is reached only
33
+ * through `accountForCaller`, which resolves this identity by its id
34
+ * first and never asks the token. The same holds for the runner lane:
35
+ * our token carries no `sub`, and nothing downstream needs one.
22
36
  */
23
37
  import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
24
38
 
25
39
  import type { CallerIdentity } from "../../extensions/identity.js";
26
40
 
27
41
  export function accountAsCaller(account: IdentityAccount): CallerIdentity {
42
+ return {
43
+ ...principalOf(account),
44
+ callerClass: "user",
45
+ issuer: "",
46
+ rawToken: "",
47
+ };
48
+ }
49
+
50
+ export function accountAsRunnerCaller(
51
+ account: IdentityAccount,
52
+ rawToken: string,
53
+ ): CallerIdentity {
54
+ if (rawToken === "") {
55
+ throw new Error(
56
+ "a runner caller carries the credential it presented — refusing to build one with none",
57
+ );
58
+ }
59
+ return {
60
+ ...principalOf(account),
61
+ callerClass: "runner",
62
+ issuer: "",
63
+ rawToken,
64
+ };
65
+ }
66
+
67
+ /** The account id as principal, with the display identity the row knows. */
68
+ function principalOf(
69
+ account: IdentityAccount,
70
+ ): Pick<CallerIdentity, "identityId" | "email" | "displayName"> {
28
71
  const accountId = account.metadata?.id ?? "";
29
72
  if (accountId === "") {
30
73
  throw new Error(
@@ -35,9 +78,6 @@ export function accountAsCaller(account: IdentityAccount): CallerIdentity {
35
78
  const displayName = account.metadata?.name ?? "";
36
79
  return {
37
80
  identityId: accountId,
38
- callerClass: "user",
39
- issuer: "",
40
- rawToken: "",
41
81
  ...(email !== "" ? { email } : {}),
42
82
  ...(displayName !== "" ? { displayName } : {}),
43
83
  };
@@ -41,6 +41,19 @@
41
41
  * cannot answer the same question two ways. A credential naming no
42
42
  * subject is `undefined` with no second read: the store is never asked
43
43
  * about "".
44
+ *
45
+ * The third reading is a ROW's creator stamp, `accountForStamp`: the
46
+ * account a `created_by.id` names, read the two ways a stamp has been
47
+ * written — as an account id (rows stamped since 3.15.0) and as the raw
48
+ * issuer subject (rows the 3.14.x verifiers stamped) — in
49
+ * `accountForCaller`'s order. Two lanes make the server act as the
50
+ * person a row names: the built-in schedule fire caller (a fire acts as
51
+ * the schedule's creator) and the runner-subject verifier (a run
52
+ * credential admits its bearer as the execution's creator). Stated here
53
+ * so they cannot resolve one stamp two ways; each decides for itself
54
+ * what "nobody" means (the fire caller's deterministic refusal, the
55
+ * verifier's liveness sentence), so this function answers `undefined`
56
+ * and never throws for it. The empty stamp is `undefined` with no read.
44
57
  */
45
58
  import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
46
59
 
@@ -93,3 +106,21 @@ export async function accountForCaller(
93
106
  }
94
107
  return accounts.findDirectByIdpId(subject);
95
108
  }
109
+
110
+ /**
111
+ * The account a row's creator stamp names, or `undefined` when it names
112
+ * nobody (the laptop's `"system"`, a trusted-local email, a deleted
113
+ * account, the empty stamp). Faults propagate as they are.
114
+ */
115
+ export async function accountForStamp(
116
+ accounts: AccountsByCaller,
117
+ stamp: string,
118
+ ): Promise<IdentityAccount | undefined> {
119
+ if (stamp === "") {
120
+ return undefined;
121
+ }
122
+ return (
123
+ (await accounts.findById(stamp)) ??
124
+ (await accounts.findDirectByIdpId(stamp))
125
+ );
126
+ }
@@ -28,6 +28,7 @@ import { ExecutionContextSchema } from "@stigmer/protos/ai/stigmer/agentic/execu
28
28
 
29
29
  import { createLogger } from "../../../boot/logger.js";
30
30
  import { SecretService } from "../../../encryption/encryption.js";
31
+ import type { CallerIdentity } from "../../../extensions/identity.js";
31
32
  import { newExecutionScopedRunnerCredentialProvider } from "../../../runnerauth/runner-credential-provider.js";
32
33
  import { RunnerAuthService } from "../../../runnerauth/runnerauth.js";
33
34
  import { SqliteStore } from "../../../store/sqlite/store.js";
@@ -41,6 +42,7 @@ import {
41
42
  startBestEffortConnect,
42
43
  } from "../connect.js";
43
44
  import type { McpServerConnectDeps } from "../connect.js";
45
+ import { isConnectExecutionId } from "../connect-execution-id.js";
44
46
  import { ManagedEnvironmentService } from "../oauth/managed-env.js";
45
47
  import {
46
48
  RUNNER_QUEUE_WARNING,
@@ -115,6 +117,8 @@ interface Harness {
115
117
  deps: McpServerConnectDeps;
116
118
  engine: FakeEngine;
117
119
  ecCreates: number;
120
+ /** The caller each EC create was handed — the connect's person, never the server (the mcp-connect binding's stamp). */
121
+ ecCreators: CallerIdentity[];
118
122
  ecDeletes: string[];
119
123
  }
120
124
 
@@ -150,6 +154,7 @@ function makeHarness(options: FakeEngineOptions = {}): Harness {
150
154
  const harness: Harness = {
151
155
  engine,
152
156
  ecCreates: 0,
157
+ ecCreators: [],
153
158
  ecDeletes: [],
154
159
  deps: {
155
160
  store,
@@ -165,8 +170,9 @@ function makeHarness(options: FakeEngineOptions = {}): Harness {
165
170
  },
166
171
  },
167
172
  executionContext: {
168
- create: async () => {
173
+ create: async (_ec, caller) => {
169
174
  harness.ecCreates += 1;
175
+ harness.ecCreators.push(caller);
170
176
  return create(ExecutionContextSchema, {
171
177
  metadata: { id: `ectx_${harness.ecCreates}` },
172
178
  });
@@ -356,7 +362,7 @@ describe("connect (blocking lane)", () => {
356
362
  expect(second.status?.toolApprovals[0]?.toolName).toBe("search");
357
363
  });
358
364
 
359
- it("creates the ephemeral EC from runtime_env, mints the decrypt token, and deletes the EC after settle", async () => {
365
+ it("creates the ephemeral EC from runtime_env AS THE CALLER, mints the decrypt token, and deletes the EC after settle", async () => {
360
366
  const harness = makeHarness();
361
367
  const server = await seedServer();
362
368
  await connect(
@@ -366,9 +372,16 @@ describe("connect (blocking lane)", () => {
366
372
  }),
367
373
  );
368
374
  expect(harness.ecCreates).toBe(1);
375
+ // The row's creator stamp is the connect token's person under the
376
+ // built-in posture (runnerauth/bound-execution.ts), so the connect
377
+ // hands the client the caller, never the server's own identity.
378
+ expect(harness.ecCreators).toEqual([testCaller]);
369
379
  expect(harness.ecDeletes).toEqual(["ectx_1"]);
370
380
  const input = harness.engine.startedInputs[0];
371
381
  expect(input?.execution_context_id).toMatch(/^connect-mcps_test_/);
382
+ // The id is the one shape the runner-credential lane recognizes as a
383
+ // connect binding — builder and predicate share one module.
384
+ expect(isConnectExecutionId(input?.execution_context_id ?? "")).toBe(true);
372
385
  // oss#535: the decrypt-lane token rides the payload.
373
386
  expect(input?.execution_context_token).toBeTruthy();
374
387
  });
@@ -584,6 +597,8 @@ describe("startConnect (async lane)", () => {
584
597
  );
585
598
  expect(result.metadata?.id).toBe(server.metadata!.id);
586
599
  expect(harness.ecCreates).toBe(1);
600
+ // The async lane creates the EC as the caller too — one prepareConnect.
601
+ expect(harness.ecCreators).toEqual([testCaller]);
587
602
  await vi.waitFor(() => expect(harness.ecDeletes).toEqual(["ectx_1"]));
588
603
  });
589
604
  });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The synthetic execution id of an MCP connect — the ONE home for its
3
+ * shape, built here by the connect lane and recognized here by the
4
+ * runner-credential lane, so the two can never drift apart.
5
+ *
6
+ * A connect is not an execution: it has no AgentExecution or
7
+ * WorkflowExecution row. What it has is an ephemeral ExecutionContext
8
+ * (connect.ts `createConnectExecutionContext`) whose `spec.execution_id`
9
+ * must name SOMETHING for the decrypt lane's binding check
10
+ * (executioncontext/resolve-values-for-caller.ts: the token's
11
+ * `execution_id` claim must equal the EC's), and this id is that
12
+ * something — an id that names no resource kind, so the contract's
13
+ * `kind_meta` prefix table (pipeline/apiresource-meta.ts) cannot mistake
14
+ * it for a row. The runner-credential lane reads it the other way round
15
+ * (runnerauth/bound-execution.ts `boundExecutionKindOf`): an id this
16
+ * predicate recognizes is a connect binding, resolved through the EC row
17
+ * whose `spec.execution_id` it is. Keep the builder and the predicate
18
+ * together; a second copy of the prefix anywhere would be the drift this
19
+ * module exists to prevent.
20
+ */
21
+ import { randomUUID } from "node:crypto";
22
+
23
+ /** The prefix every connect execution id carries — no resource kind's id starts with it. */
24
+ const CONNECT_EXECUTION_ID_PREFIX = "connect-";
25
+
26
+ /** One connect's execution id: the server it discovers plus eight hex of entropy per attempt. */
27
+ export function newConnectExecutionId(mcpServerId: string): string {
28
+ return `${CONNECT_EXECUTION_ID_PREFIX}${mcpServerId}-${randomUUID().slice(0, 8)}`;
29
+ }
30
+
31
+ /** Whether `executionId` is a connect's — the runner-credential lane's recognition of the third binding. */
32
+ export function isConnectExecutionId(executionId: string): boolean {
33
+ return executionId.startsWith(CONNECT_EXECUTION_ID_PREFIX);
34
+ }
@@ -17,7 +17,6 @@
17
17
  import { create } from "@bufbuild/protobuf";
18
18
  import type { MessageInitShape } from "@bufbuild/protobuf";
19
19
  import { Code, ConnectError } from "@connectrpc/connect";
20
- import { randomUUID } from "node:crypto";
21
20
 
22
21
  import type {
23
22
  EnvironmentList,
@@ -62,6 +61,7 @@ import type {
62
61
  } from "../../store/interface.js";
63
62
  import { ResourceNotFoundError } from "../../store/interface.js";
64
63
  import { resolveOAuthAppRef } from "../oauthapp/refresolution.js";
64
+ import { newConnectExecutionId } from "./connect-execution-id.js";
65
65
  import {
66
66
  persistConnectFailure,
67
67
  persistConnectResult,
@@ -169,10 +169,21 @@ export interface ConnectEnvironmentReader {
169
169
  /**
170
170
  * The ExecutionContext lifecycle surface for the ephemeral connect EC —
171
171
  * Go's downstream executioncontext client (create + delete).
172
+ *
173
+ * `create` takes the connecting caller and the composition creates the
174
+ * row AS THAT PERSON (boot/inprocess.ts, the `asCaller` lane of ruling
175
+ * R5), never under the internal class: the row's creator stamp is what
176
+ * the runner-subject verifier resolves the connect's person from when
177
+ * the runner presents the connect token under the built-in posture
178
+ * (runnerauth/bound-execution.ts, the `mcp-connect` binding). A row
179
+ * stamped `internal` would name nobody, and the credentialed connect
180
+ * would be refused at identity on every enforcing self-host. `delete`
181
+ * stays the server's own act: the row is the lane's, whoever asked.
172
182
  */
173
183
  export interface ConnectExecutionContextClient {
174
184
  create(
175
185
  executionContext: MessageInitShape<typeof ExecutionContextSchema>,
186
+ caller: CallerIdentity,
176
187
  ): Promise<ExecutionContext>;
177
188
  delete(
178
189
  input: MessageInitShape<typeof ApiResourceDeleteInputSchema>,
@@ -272,7 +283,7 @@ export async function connect(
272
283
  input,
273
284
  );
274
285
 
275
- const prepared = await prepareConnect(deps, mcpServer, input);
286
+ const prepared = await prepareConnect(deps, mcpServer, input, identity);
276
287
 
277
288
  try {
278
289
  let run: ConnectRun;
@@ -385,11 +396,28 @@ export async function connect(
385
396
  * read the caller's grant and secrets, which a background task has no
386
397
  * request context to do (the same constraint that scopes
387
398
  * startBestEffortConnect to env-less servers).
399
+ *
400
+ * A connect's discovery runs on the runner with TWO credentials, and the
401
+ * split is deliberate. The runner's own process credential reads the
402
+ * McpServer's metadata — under the built-in posture that credential is
403
+ * the operator's API key, and the operator is an organization admin
404
+ * (deploy/helm/stigmer: the signed-in operator mints the runner's key),
405
+ * whom the model makes an owner of every McpServer in the organization
406
+ * (authorization/model/mcp_server.ts), so a member's private server is
407
+ * readable. The connect token minted below reads the SECRETS: it is
408
+ * bound to this connect's ExecutionContext, and under the built-in
409
+ * posture the runner-subject verifier resolves its bearer to the person
410
+ * that row was created by — which is why the row is created as `identity`
411
+ * and not as the server. The end state where every RPC of a discovery
412
+ * acts as the person (the cloud's connect-sandbox shape) needs a row an
413
+ * env-less connect would also create; it creates none today, so that is
414
+ * a design act of its own, not a widening to make here.
388
415
  */
389
416
  export async function prepareConnect(
390
417
  deps: McpServerConnectDeps,
391
418
  mcpServer: McpServer,
392
419
  input: ConnectInput,
420
+ identity: CallerIdentity,
393
421
  ): Promise<PreparedConnect> {
394
422
  const mcpServerId = mcpServer.metadata?.id ?? "";
395
423
  const callerOrg = input.org;
@@ -402,7 +430,7 @@ export async function prepareConnect(
402
430
  await refreshOAuthTokenIfNeeded(deps, mcpServer, callerOrg);
403
431
  }
404
432
 
405
- const executionId = `connect-${mcpServerId}-${randomUUID().slice(0, 8)}`;
433
+ const executionId = newConnectExecutionId(mcpServerId);
406
434
 
407
435
  const ecResourceId = await createConnectExecutionContext(
408
436
  deps,
@@ -410,6 +438,7 @@ export async function prepareConnect(
410
438
  executionId,
411
439
  callerOrg,
412
440
  input.runtimeEnv,
441
+ identity,
413
442
  );
414
443
 
415
444
  const workflowInput: ConnectWorkflowInput = {
@@ -464,6 +493,10 @@ export async function prepareConnect(
464
493
  * sources: OAuth-managed variables from the grant's managed environment,
465
494
  * the remainder from the user's personal environment. Returns "" when the
466
495
  * MCP server has no env declarations and no runtime_env.
496
+ *
497
+ * The row is created as `caller`, the person connecting (see the
498
+ * ConnectExecutionContextClient doc): its creator stamp is the connect
499
+ * token's person under the built-in posture.
467
500
  */
468
501
  async function createConnectExecutionContext(
469
502
  deps: McpServerConnectDeps,
@@ -471,6 +504,7 @@ async function createConnectExecutionContext(
471
504
  executionId: string,
472
505
  callerOrg: string,
473
506
  runtimeEnv: { [key: string]: ExecutionValue },
507
+ caller: CallerIdentity,
474
508
  ): Promise<string> {
475
509
  let ecData: { [key: string]: ExecutionValue };
476
510
 
@@ -540,6 +574,7 @@ async function createConnectExecutionContext(
540
574
  data: ecData,
541
575
  },
542
576
  }),
577
+ caller,
543
578
  );
544
579
  } catch (error) {
545
580
  throw internalError(error, "failed to create connect ExecutionContext");
@@ -26,9 +26,15 @@
26
26
  * read RPC redacts secrets unless the caller presents an execution-scoped
27
27
  * runner token, and the discovery activity has no execution of its own to
28
28
  * exchange for one — the capability travels with the work item. It is a
29
- * decrypt-lane discriminator, not a secret value: short-TTL, bound to
30
- * this connect flow's ephemeral EC (deleted when the handler returns),
31
- * and useless once either expires.
29
+ * short-TTL token bound to this connect flow's ephemeral EC (deleted when
30
+ * the connect settles, on the blocking lane and the async one alike) and
31
+ * useless once either expires. What it unlocks depends on the posture:
32
+ * under trusted-local it is a decrypt-lane discriminator and nothing more;
33
+ * under the built-in authorization posture the same token also admits its
34
+ * bearer AS THE PERSON who asked for the connect, on every RPC, for as
35
+ * long as the EC row exists (runnerauth/runnerauth.ts, the two lanes by
36
+ * posture; runnerauth/bound-execution.ts, the `mcp-connect` binding). It
37
+ * sits in Temporal history in the clear like every server-written input.
32
38
  */
33
39
  export interface ConnectWorkflowInput {
34
40
  readonly mcp_server_id: string;