@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/server",
3
- "version": "3.15.3-dev.20260916211208",
3
+ "version": "3.16.0",
4
4
  "description": "The Stigmer OSS control plane as a composable library: composeServer, the extension registry and the driver seams a composition builds on. The self-contained deployable of the same server is @stigmer/server-slim.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -62,10 +62,10 @@
62
62
  "@connectrpc/connect-node": "^2.0.0",
63
63
  "@kubernetes/client-node": "2.0.0",
64
64
  "@opentelemetry/api": "^1.9.0",
65
- "@stigmer/plugin-package": "3.15.3-dev.20260916211208",
66
- "@stigmer/protos": "3.15.3-dev.20260916211208",
67
- "@stigmer/temporal-codecs": "3.15.3-dev.20260916211208",
68
- "@stigmer/zip-structure": "3.15.3-dev.20260916211208",
65
+ "@stigmer/plugin-package": "3.16.0",
66
+ "@stigmer/protos": "3.16.0",
67
+ "@stigmer/temporal-codecs": "3.16.0",
68
+ "@stigmer/zip-structure": "3.16.0",
69
69
  "@temporalio/activity": "1.16.2",
70
70
  "@temporalio/client": "1.16.2",
71
71
  "@temporalio/common": "1.16.2",
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Pins the lane-admission decorator (lane-admission.ts): a caller the
3
+ * predicate names is ADMITTED on the run-gate checks without the inner
4
+ * Authorizer being consulted, and every other combination — an admitted
5
+ * lane on a non-run-gate check, any other caller on any check — reaches
6
+ * the inner Authorizer untouched, its answer returned as-is (deny and
7
+ * unavailable included, so the decorator can never turn a refusal into
8
+ * an allow off the run gate). The predicate is the whole policy; this
9
+ * file proves the decorator's arithmetic with a plain one, and the
10
+ * built-in provider's tests prove the predicate open source composes.
11
+ */
12
+ import { describe, expect, it } from "vitest";
13
+
14
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
15
+ import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
16
+
17
+ import type {
18
+ Authorizer,
19
+ AuthzCheck,
20
+ AuthzDecision,
21
+ } from "../../extensions/authorizer.js";
22
+ import type { CallerIdentity } from "../../extensions/identity.js";
23
+ import { RUN_GATE_CHECKS } from "../../pipeline/steps/authorize-run-target.js";
24
+ import { newLaneAdmittedAuthorizer } from "../lane-admission.js";
25
+
26
+ const WORKFLOW_RUNNER: CallerIdentity = {
27
+ identityId: "ida_carol",
28
+ callerClass: "runner",
29
+ issuer: "",
30
+ rawToken: "wex-bound",
31
+ };
32
+ const AGENT_RUNNER: CallerIdentity = {
33
+ ...WORKFLOW_RUNNER,
34
+ rawToken: "aex-bound",
35
+ };
36
+ const PERSON: CallerIdentity = {
37
+ identityId: "ida_carol",
38
+ callerClass: "user",
39
+ issuer: "https://issuer.example/",
40
+ rawToken: "eyJ.person.token",
41
+ };
42
+
43
+ const isWorkflowRunner = (caller: CallerIdentity): boolean =>
44
+ caller.callerClass === "runner" && caller.rawToken === "wex-bound";
45
+
46
+ const runGate: AuthzCheck = { ...RUN_GATE_CHECKS.agent, resourceId: "agt_1" };
47
+ const sessionGate: AuthzCheck = {
48
+ ...RUN_GATE_CHECKS.session,
49
+ resourceId: "ses_1",
50
+ };
51
+ const notRunGate: AuthzCheck = {
52
+ permission: IamPermission.can_edit,
53
+ resourceKind: ApiResourceKind.agent_execution,
54
+ resourceId: "aex_1",
55
+ };
56
+
57
+ function recording(answer: AuthzDecision) {
58
+ const seen: Array<{ caller: CallerIdentity; check: AuthzCheck }> = [];
59
+ const inner: Authorizer = {
60
+ authorize(caller, check) {
61
+ seen.push({ caller, check });
62
+ return Promise.resolve(answer);
63
+ },
64
+ };
65
+ return { inner, seen };
66
+ }
67
+
68
+ describe("newLaneAdmittedAuthorizer", () => {
69
+ it("an admitted lane on a run-gate check is allowed without consulting the inner Authorizer", async () => {
70
+ const { inner, seen } = recording({ kind: "deny", reason: "would deny" });
71
+ const authorizer = newLaneAdmittedAuthorizer(inner, isWorkflowRunner);
72
+ expect(await authorizer.authorize(WORKFLOW_RUNNER, runGate)).toEqual({
73
+ kind: "allow",
74
+ });
75
+ expect(await authorizer.authorize(WORKFLOW_RUNNER, sessionGate)).toEqual({
76
+ kind: "allow",
77
+ });
78
+ expect(seen).toEqual([]);
79
+ });
80
+
81
+ it("an admitted lane on a NON-run-gate check reaches the inner Authorizer untouched — the reach is the run gate alone", async () => {
82
+ const { inner, seen } = recording({ kind: "deny", reason: "not yours" });
83
+ const authorizer = newLaneAdmittedAuthorizer(inner, isWorkflowRunner);
84
+ expect(await authorizer.authorize(WORKFLOW_RUNNER, notRunGate)).toEqual({
85
+ kind: "deny",
86
+ reason: "not yours",
87
+ });
88
+ expect(seen).toEqual([{ caller: WORKFLOW_RUNNER, check: notRunGate }]);
89
+ });
90
+
91
+ it.each([
92
+ ["an agent-bound runner", AGENT_RUNNER],
93
+ ["a person", PERSON],
94
+ ])(
95
+ "%s on a run-gate check is checked by the inner Authorizer as themselves",
96
+ async (_label, caller) => {
97
+ const { inner, seen } = recording({ kind: "not-found" });
98
+ const authorizer = newLaneAdmittedAuthorizer(inner, isWorkflowRunner);
99
+ expect(await authorizer.authorize(caller, runGate)).toEqual({
100
+ kind: "not-found",
101
+ });
102
+ expect(seen).toEqual([{ caller, check: runGate }]);
103
+ },
104
+ );
105
+
106
+ it("an unavailable answer off the run gate is returned as-is — the decorator never masks a fault", async () => {
107
+ const cause = new Error("evaluator down");
108
+ const { inner } = recording({ kind: "unavailable", cause });
109
+ const authorizer = newLaneAdmittedAuthorizer(inner, isWorkflowRunner);
110
+ expect(await authorizer.authorize(PERSON, runGate)).toEqual({
111
+ kind: "unavailable",
112
+ cause,
113
+ });
114
+ });
115
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Lane admission over the Authorizer seam — open source's rendering of
3
+ * the cloud's decorator (stigmer-cloud `authorizer/lane-admission.ts`,
4
+ * stigmer-cloud#709), composed around the built-in Authorizer under the
5
+ * built-in authorization posture (boot/compose.ts).
6
+ *
7
+ * The OSS AuthorizeRunTarget step asks the Authorizer, for every caller
8
+ * but the internal class, whether the caller may run what a session or
9
+ * execution targets — the run-gate checks (`isRunGateCheck`, the
10
+ * OSS-owned definition of that set: `can_execute` on the blueprints and
11
+ * instances, `can_create_execution_in` on a session). A runner acting
12
+ * for a WORKFLOW execution creates the workflow's child agent executions
13
+ * (the `agent_call` activity), and it does so AS THE HUMAN whose workflow
14
+ * it is (runnerauth/runner-subject-verifier.ts). Checked as that human,
15
+ * a member running an organization-visible workflow that calls another
16
+ * member's private agent would be refused at the child create — but
17
+ * the parent workflow execution already passed its own run gate, and
18
+ * the human's visibility of the agents a shared workflow calls is not
19
+ * the question. The cloud admits its `workflow_sandbox` lane on exactly
20
+ * these checks for exactly this reason; this decorator gives an
21
+ * enforcing self-host the same answer, so a workflow behaves the same in
22
+ * both editions (the parent program's rule: nothing diverges silently).
23
+ *
24
+ * Why a predicate and not a class set: the cloud keys its decorator on
25
+ * `callerClass` because there the class IS the token type. In open
26
+ * source the class is `runner` for every bound execution, and the
27
+ * binding's KIND lives in the token, readable by the one function that
28
+ * interprets runner tokens (`isWorkflowBoundRunner` in
29
+ * runnerauth/built-in-runner-credential-provider.ts). Handing that
30
+ * reading in as `isAdmittedLane` keeps this module free of token
31
+ * knowledge and lets the cloud adopt the same shape later without a
32
+ * second class name. An AGENT-bound runner is NOT admitted: a subagent
33
+ * create is checked as the human, the cloud's `sandbox` posture.
34
+ *
35
+ * Narrow by construction: an admitted lane on any NON-run-gate check,
36
+ * and any other caller on any check, delegate untouched. The cloud's
37
+ * header records one caveat — a human who can view a workflow execution
38
+ * can exchange for its token and replay it against the run gate. Open
39
+ * source has no such caveat: under this posture the exchange is a mint
40
+ * gate that answers only the run's own person (the built-in provider's
41
+ * exchangeScopedToken, runnerauth/built-in-runner-credential-provider.ts),
42
+ * so the only human who can hold a workflow's credential is the one whose
43
+ * workflow it is — and their own run gate already passed.
44
+ */
45
+ import type { CallerIdentity } from "../extensions/identity.js";
46
+ import type {
47
+ Authorizer,
48
+ AuthzCheck,
49
+ AuthzDecision,
50
+ } from "../extensions/authorizer.js";
51
+ import { isRunGateCheck } from "../pipeline/steps/authorize-run-target.js";
52
+
53
+ const ADMITTED: AuthzDecision = { kind: "allow" };
54
+
55
+ /** Whether `caller` is a lane whose admission to CREATE a run was decided upstream. */
56
+ export type LaneAdmissionPredicate = (caller: CallerIdentity) => boolean;
57
+
58
+ export function newLaneAdmittedAuthorizer(
59
+ inner: Authorizer,
60
+ isAdmittedLane: LaneAdmissionPredicate,
61
+ ): Authorizer {
62
+ return {
63
+ authorize(
64
+ caller: CallerIdentity,
65
+ check: AuthzCheck,
66
+ ): Promise<AuthzDecision> {
67
+ if (isRunGateCheck(check) && isAdmittedLane(caller)) {
68
+ return Promise.resolve(ADMITTED);
69
+ }
70
+ return inner.authorize(caller, check);
71
+ },
72
+ };
73
+ }
@@ -22,10 +22,12 @@
22
22
  * client-suppliable schedule-id label is what the cloud explicitly
23
23
  * refused.
24
24
  *
25
- * The creator's account is resolved from the row's stamp two ways — as an
26
- * account id (rows stamped since 3.15.0) and as the raw issuer subject
27
- * (rows the 3.14.x verifiers stamped) — through the account port, the
28
- * same two shapes `Person` aliases. A stamp that resolves to no account
25
+ * The creator's account is resolved from the row's stamp through the
26
+ * identity-account domain's `accountForStamp` — as an account id (rows
27
+ * stamped since 3.15.0) and as the raw issuer subject (rows the 3.14.x
28
+ * verifiers stamped), the same two shapes `Person` aliases and the same
29
+ * read the runner-subject verifier makes of an execution's stamp. A
30
+ * stamp that resolves to no account
29
31
  * is the seam's DETERMINISTIC refusal (`ScheduleFireCallerRefusedError`):
30
32
  * the empty stamp, the laptop's `"system"`, an email from the
31
33
  * trusted-local era, a deleted account — no retry will make that schedule
@@ -43,7 +45,8 @@ import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/a
43
45
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
44
46
 
45
47
  import { accountAsCaller } from "../domain/identityaccount/actor.js";
46
- import type { IdentityAccountStore } from "../domain/identityaccount/store.js";
48
+ import { accountForStamp } from "../domain/identityaccount/resolve.js";
49
+ import type { AccountsByCaller } from "../domain/identityaccount/resolve.js";
47
50
  import type { ScheduleFireCallerMint } from "../extensions/schedule-fire-caller.js";
48
51
  import { ScheduleFireCallerRefusedError } from "../extensions/schedule-fire-caller.js";
49
52
  import { auditOf } from "../pipeline/steps/defaults.js";
@@ -52,10 +55,7 @@ import type { Store } from "../store/interface.js";
52
55
  export interface BuiltInScheduleFireCallerDeps {
53
56
  readonly store: Store;
54
57
  /** The account port: the stamp is resolved as an account id, then as a direct subject. */
55
- readonly accounts: Pick<
56
- IdentityAccountStore,
57
- "findById" | "findDirectByIdpId"
58
- >;
58
+ readonly accounts: AccountsByCaller;
59
59
  }
60
60
 
61
61
  /** The refusal's copy — written for the person who scheduled the run (the schedule's status carries it). */
@@ -75,11 +75,7 @@ export function newBuiltInScheduleFireCaller(
75
75
  );
76
76
  const stamp =
77
77
  auditOf(ScheduleSchema, schedule)?.specAudit?.createdBy?.id ?? "";
78
- const account =
79
- stamp === ""
80
- ? undefined
81
- : ((await deps.accounts.findById(stamp)) ??
82
- (await deps.accounts.findDirectByIdpId(stamp)));
78
+ const account = await accountForStamp(deps.accounts, stamp);
83
79
  if (account === undefined) {
84
80
  throw new ScheduleFireCallerRefusedError(
85
81
  scheduleHasNoPersonMessage(scheduleId),
@@ -33,6 +33,7 @@ import { ServerEdition } from "@stigmer/protos/ai/stigmer/platform/v1/server_inf
33
33
  import { HealthCheckResponse_ServingStatus as ServingStatus } from "@stigmer/protos/grpc/health/v1/health_pb";
34
34
 
35
35
  import { newBuiltInAuthorizer } from "../authorization/authorizer.js";
36
+ import { newLaneAdmittedAuthorizer } from "../authorization/lane-admission.js";
36
37
  import { newBuiltInListReadScope } from "../authorization/list-read-scope.js";
37
38
  import { newBuiltInOrganizationDirectory } from "../authorization/organization-directory.js";
38
39
  import { authorizationPostureOf } from "../authorization/posture.js";
@@ -150,7 +151,12 @@ import { operatorIdentitySnapshot } from "../pipeline/steps/defaults.js";
150
151
  import { createErrorBoundaryInterceptor } from "../pipeline/interceptors/error-boundary.js";
151
152
  import { createRequestMetricsInterceptor } from "../pipeline/interceptors/request-metrics.js";
152
153
  import { newPermissiveSingleTeamAuthorizer } from "../pipeline/steps/authorize.js";
154
+ import {
155
+ isWorkflowBoundRunner,
156
+ newBuiltInRunnerCredentialProvider,
157
+ } from "../runnerauth/built-in-runner-credential-provider.js";
153
158
  import { newExecutionScopedRunnerCredentialProvider } from "../runnerauth/runner-credential-provider.js";
159
+ import { newRunnerSubjectIdentityVerifier } from "../runnerauth/runner-subject-verifier.js";
154
160
  import { RunnerAuthService } from "../runnerauth/runnerauth.js";
155
161
  import { PostgresStore } from "../store/postgres/store.js";
156
162
  import { SqliteStore } from "../store/sqlite/store.js";
@@ -431,12 +437,44 @@ export async function composeServer(
431
437
  operatorEmail: operatorIdentity.email,
432
438
  })
433
439
  : undefined;
440
+ // Stage: runner credentials (§6c, O5). The concrete service is
441
+ // constructed and exposed UNCONDITIONALLY — its boot-fatal key posture
442
+ // is the ratified cross-domain invariant, and the boot/EC-decrypt tests
443
+ // mint against the server's own key through it. The provider over it is
444
+ // the edition's whole credential story in one object: a composed
445
+ // driver; otherwise, under the built-in authorization posture, open
446
+ // source's built-in provider (runnerauth/built-in-runner-credential-
447
+ // provider.ts — the execution-scoped default plus the lineage-vouching,
448
+ // memory-capture and exchange-gating capabilities an enforcing
449
+ // self-host needs, and the one reading of a runner's binding the
450
+ // Authorizer below admits a workflow-bound runner on); otherwise the
451
+ // execution-scoped default, byte-identical to the wiring this seam
452
+ // replaced. Both open-source providers also mint the RUN credential the
453
+ // two execution engines put on every dispatch (the engines below take
454
+ // this object; runnerauth/dispatch-credential.ts) — a composed driver
455
+ // that leaves that capability undefined dispatches without one, its
456
+ // runners being credentialed another way. Bound HERE, before the
457
+ // Authorizer, because the lane admission reads the binding.
458
+ const runnerAuthService = RunnerAuthService.fromEnv();
459
+ const runnerCredentials =
460
+ extensions.drivers.runnerCredentialProvider ??
461
+ (authorizationPosture === "built-in"
462
+ ? newBuiltInRunnerCredentialProvider({
463
+ service: runnerAuthService,
464
+ store,
465
+ accounts: identityAccounts,
466
+ })
467
+ : newExecutionScopedRunnerCredentialProvider(runnerAuthService));
434
468
  // The ONE composed Authorizer (DD-007 §3), bound here — after the store,
435
469
  // the account port and the IamPolicy port it reads — and handed to every
436
470
  // controller as an explicit dependency; the Authorize step at position 1
437
471
  // of every chain calls it (O2). By posture: an extension's registration;
438
472
  // open source's built-in Authorizer (authorization/authorizer.ts — the
439
- // cloud's model evaluated over the tuples the row would have had); or
473
+ // cloud's model evaluated over the tuples the row would have had),
474
+ // decorated with the lane admission (authorization/lane-admission.ts) so
475
+ // a runner acting for a WORKFLOW execution passes the run gate on the
476
+ // child executions its `agent_call` creates — the parent workflow
477
+ // already passed its own, the cloud's `workflow_sandbox` posture; or
440
478
  // the permissive single-team default. The organization directory and the
441
479
  // schedule fire caller are bound beside it under the same posture, so
442
480
  // `findMyOrganizations` lists what a person may view and a scheduled run
@@ -445,13 +483,16 @@ export async function composeServer(
445
483
  const authorizer: Authorizer =
446
484
  extensions.authorizer ??
447
485
  (authorizationPosture === "built-in"
448
- ? newBuiltInAuthorizer({
449
- store,
450
- policies: iamPolicies,
451
- accounts: identityAccounts,
452
- edition: extensions.edition,
453
- logger,
454
- })
486
+ ? newLaneAdmittedAuthorizer(
487
+ newBuiltInAuthorizer({
488
+ store,
489
+ policies: iamPolicies,
490
+ accounts: identityAccounts,
491
+ edition: extensions.edition,
492
+ logger,
493
+ }),
494
+ (caller) => isWorkflowBoundRunner(runnerAuthService, caller),
495
+ )
455
496
  : newPermissiveSingleTeamAuthorizer());
456
497
  const organizationDirectory: OrganizationDirectory | undefined =
457
498
  extensions.drivers.organizationDirectory ??
@@ -626,16 +667,6 @@ export async function composeServer(
626
667
  writeVersionValue === "" ? DEFAULT_WRITE_VERSION : writeVersionValue,
627
668
  registeredCodecs: [...secretCodecs.keys()].sort(),
628
669
  });
629
- const runnerAuthService = RunnerAuthService.fromEnv();
630
- // The runner-credential seam (§6c, O5): the composed provider, or the
631
- // OSS execution-scoped default over the service above. The concrete
632
- // service is constructed and exposed UNCONDITIONALLY either way — its
633
- // boot-fatal key posture is the ratified cross-domain invariant, and the
634
- // boot/EC-decrypt tests mint against the server's own key through it.
635
- const runnerCredentials =
636
- extensions.drivers.runnerCredentialProvider ??
637
- newExecutionScopedRunnerCredentialProvider(runnerAuthService);
638
-
639
670
  // Stage: temporal (#18). Construction is sync and connection-free —
640
671
  // initialConnect/startWorkers/startHealthMonitor run in start(), all
641
672
  // NON-fatal (Go server.go: the server boots and serves with the engine
@@ -837,6 +868,7 @@ export async function composeServer(
837
868
  manager: temporalManager,
838
869
  config: temporalConfig,
839
870
  store,
871
+ runnerCredentials,
840
872
  logger,
841
873
  });
842
874
  // The workflow-execution twin: the same provider-is-the-injection
@@ -844,6 +876,7 @@ export async function composeServer(
844
876
  const workflowExecutionEngineState = newWorkflowExecutionEngineStateProvider({
845
877
  manager: temporalManager,
846
878
  config: workflowExecutionTemporalConfig,
879
+ runnerCredentials,
847
880
  logger,
848
881
  });
849
882
  // The artifact blob store (Go server.go 349: shared by agentexecution
@@ -1355,8 +1388,8 @@ export async function composeServer(
1355
1388
  requireInProcess().executionEnvironmentReader.getSecretValue(input),
1356
1389
  },
1357
1390
  executionContext: {
1358
- create: (ec) =>
1359
- requireInProcess().connectExecutionContextClient.create(ec),
1391
+ create: (ec, caller) =>
1392
+ requireInProcess().connectExecutionContextClient.create(ec, caller),
1360
1393
  delete: (input) =>
1361
1394
  requireInProcess().connectExecutionContextClient.delete(input),
1362
1395
  },
@@ -1474,12 +1507,36 @@ export async function composeServer(
1474
1507
  // port the domain writes through (`identityAccounts`, the
1475
1508
  // identity-accounts stage — a composed driver included), so a row
1476
1509
  // provisioning creates resolves on the very next request.
1477
- // The runner's credential in either posture is an operator-minted API
1478
- // token via STIGMER_TOKEN (O3 ruling Q3) — no runner-specific verifier.
1510
+ // - the runner-subject verifier rides the AUTHORIZATION posture
1511
+ // (`built-in`, beside the Authorizer and the fire caller above): it
1512
+ // admits the bearer of the server's own execution-scoped runner
1513
+ // token as the human whose run it is, which only means something
1514
+ // when the server enforces who may report on a run. Its place in
1515
+ // the chain is the contract: BETWEEN `apikey` and `oidc`, because
1516
+ // the OIDC verifier claims any JWT-shaped token and throws on one
1517
+ // it cannot verify — composed after it, ours would never see its
1518
+ // own token, and every runner call would fail as the OIDC verifier's
1519
+ // fault (stigmer#1137). Until 2026-09-16 this comment read "the
1520
+ // runner's credential in either posture is an operator-minted API
1521
+ // token via STIGMER_TOKEN — no runner-specific verifier"; that
1522
+ // described a server whose Authorizer was permissive. The operator's
1523
+ // key is still the runner's PROCESS credential (bootstrap, registry);
1524
+ // the run credential is how it acts for each run. A unit with its
1525
+ // own Authorizer composes none of this — its credential story is its
1526
+ // own (the cloud's sandbox lanes).
1479
1527
  const identityVerifiers = [
1480
1528
  ...(requireAuthentication
1481
1529
  ? [newApiKeyIdentityVerifier({ store, accounts: identityAccounts })]
1482
1530
  : []),
1531
+ ...(authorizationPosture === "built-in"
1532
+ ? [
1533
+ newRunnerSubjectIdentityVerifier({
1534
+ store,
1535
+ accounts: identityAccounts,
1536
+ credentials: runnerCredentials,
1537
+ }),
1538
+ ]
1539
+ : []),
1483
1540
  ...(authEnabled
1484
1541
  ? [
1485
1542
  newOidcIdentityVerifier({
@@ -332,8 +332,14 @@ export function createInProcessClients(
332
332
  executionContextCreator: {
333
333
  create: (ec) => executionContextCommand.create(ec),
334
334
  },
335
+ // The connect lane's ephemeral EC is created AS THE CONNECTING PERSON
336
+ // (ruling R5's asCaller lane): its creator stamp is what the
337
+ // runner-subject verifier resolves the connect token's bearer to under
338
+ // the built-in posture (runnerauth/bound-execution.ts). The delete
339
+ // stays the server's own act — the row is the lane's, whoever asked.
335
340
  connectExecutionContextClient: {
336
- create: (ec) => executionContextCommand.create(ec),
341
+ create: (ec, caller) =>
342
+ executionContextCommand.create(ec, asCaller(caller)),
337
343
  delete: (input) => executionContextCommand.delete(input),
338
344
  },
339
345
  // The workflowexecution edges: the default-instance self-heal creates
@@ -36,6 +36,8 @@ import type { Client, Transport } from "@connectrpc/connect";
36
36
  import { createGrpcTransport } from "@connectrpc/connect-node";
37
37
  import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
38
38
 
39
+ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
40
+ import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
39
41
  import { ExecutionContextSchema } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/api_pb";
40
42
  import type { ExecutionContext } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/api_pb";
41
43
  import { ExecutionContextCommandController } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/command_pb";
@@ -53,6 +55,7 @@ import {
53
55
  } from "../../../encryption/encryption.js";
54
56
  import { SqliteStore } from "../../../store/sqlite/store.js";
55
57
  import { REDACTED_MARKER } from "../../environment/constants.js";
58
+ import { newConnectExecutionId } from "../../mcpserver/connect-execution-id.js";
56
59
  import {
57
60
  DELETE_EXECUTION_CONTEXT_ACTIVITY_NAME,
58
61
  deleteExecutionContextForExecution,
@@ -419,6 +422,107 @@ describe("executioncontext domain (encryption + runner auth enabled)", () => {
419
422
  });
420
423
  });
421
424
 
425
+ describe("the run credential on the decrypt lane — validity is the row's", () => {
426
+ // A no-`exp` token: the row is the only thing that can end it, so the
427
+ // lane reads the bound execution's liveness for exactly this shape and
428
+ // keeps the clocked arms above untouched (their ECs name no row).
429
+ const RUN_ID = "aex_run_credential";
430
+
431
+ async function seedRun(
432
+ phase: ExecutionPhase,
433
+ completedAt = "",
434
+ ): Promise<void> {
435
+ await ts.server.store.saveResource(
436
+ ApiResourceKind.agent_execution,
437
+ RUN_ID,
438
+ AgentExecutionSchema,
439
+ create(AgentExecutionSchema, {
440
+ metadata: { id: RUN_ID, name: RUN_ID, org: "test-org" },
441
+ status: { phase, completedAt },
442
+ }),
443
+ );
444
+ }
445
+
446
+ beforeAll(async () => {
447
+ await ts.command.create(ecInput({ executionId: RUN_ID }));
448
+ });
449
+
450
+ async function readRun(): Promise<ExecutionContext> {
451
+ const credential = ts.server.runnerAuthService.mintRunCredential(RUN_ID);
452
+ return await ts.query.getByExecutionId(
453
+ { executionId: RUN_ID },
454
+ { headers: bearerHeaders(credential) },
455
+ );
456
+ }
457
+
458
+ it("decrypts while the run is live", async () => {
459
+ await seedRun(ExecutionPhase.EXECUTION_IN_PROGRESS);
460
+ expect((await readRun()).spec!.data["API_TOKEN"]!.value).toBe(
461
+ "super-secret-token",
462
+ );
463
+ });
464
+
465
+ it("still decrypts just after the run ended — the grace covers the writes that trail the terminal stamp", async () => {
466
+ await seedRun(
467
+ ExecutionPhase.EXECUTION_COMPLETED,
468
+ new Date().toISOString(),
469
+ );
470
+ expect((await readRun()).spec!.data["API_TOKEN"]!.value).toBe(
471
+ "super-secret-token",
472
+ );
473
+ });
474
+
475
+ it("redacts once the run is over past the grace — a token in Temporal history cannot decrypt a finished run's secrets", async () => {
476
+ await seedRun(
477
+ ExecutionPhase.EXECUTION_COMPLETED,
478
+ new Date(Date.now() - 60 * 60 * 1000).toISOString(),
479
+ );
480
+ expect((await readRun()).spec!.data["API_TOKEN"]!.value).toBe(
481
+ REDACTED_MARKER,
482
+ );
483
+ });
484
+
485
+ it("redacts for a terminal run that does not say when it ended — no grace without a timestamp", async () => {
486
+ await seedRun(ExecutionPhase.EXECUTION_FAILED);
487
+ expect((await readRun()).spec!.data["API_TOKEN"]!.value).toBe(
488
+ REDACTED_MARKER,
489
+ );
490
+ });
491
+
492
+ it("redacts when the credential names a run this server does not have", async () => {
493
+ const orphan = "aex_no_such_run";
494
+ await ts.command.create(ecInput({ executionId: orphan }));
495
+ const credential = ts.server.runnerAuthService.mintRunCredential(orphan);
496
+ const ec = await ts.query.getByExecutionId(
497
+ { executionId: orphan },
498
+ { headers: bearerHeaders(credential) },
499
+ );
500
+ expect(ec.spec!.data["API_TOKEN"]!.value).toBe(REDACTED_MARKER);
501
+ });
502
+
503
+ it("redacts for a clockless token bound to a connect's EC — a connect is not a run, and no mint produces that shape (the verifier refuses it through the same predicate)", async () => {
504
+ const connectId = newConnectExecutionId("mcps_conn");
505
+ await ts.command.create(ecInput({ executionId: connectId }));
506
+ const clockless =
507
+ ts.server.runnerAuthService.mintRunCredential(connectId);
508
+ const redacted = await ts.query.getByExecutionId(
509
+ { executionId: connectId },
510
+ { headers: bearerHeaders(clockless) },
511
+ );
512
+ expect(redacted.spec!.data["API_TOKEN"]!.value).toBe(REDACTED_MARKER);
513
+ // The connect lane's own CLOCKED token keeps decrypting the same EC:
514
+ // the clocked arms above are untouched by the run-credential rule.
515
+ const clocked = ts.server.runnerAuthService.mint(connectId, 300).token;
516
+ const decrypted = await ts.query.getByExecutionId(
517
+ { executionId: connectId },
518
+ { headers: bearerHeaders(clocked) },
519
+ );
520
+ expect(decrypted.spec!.data["API_TOKEN"]!.value).toBe(
521
+ "super-secret-token",
522
+ );
523
+ });
524
+ });
525
+
422
526
  describe("decrypt error doctrine", () => {
423
527
  it("tampered stored ciphertext drops the ONE key while the read succeeds; intact keys decrypt", async () => {
424
528
  const created = await ts.command.create(
@@ -19,6 +19,7 @@ import {
19
19
  EncryptionScope,
20
20
  SecretService,
21
21
  } from "../../../encryption/encryption.js";
22
+ import type { BoundExecutionStore } from "../../../runnerauth/bound-execution.js";
22
23
  import { InvalidTokenError } from "../../../runnerauth/runnerauth.js";
23
24
  import type { RunnerCredentialProvider } from "../../../runnerauth/runner-credential-provider.js";
24
25
  import { resolveValuesForCaller } from "../resolve-values-for-caller.js";
@@ -31,6 +32,14 @@ const silentLogger = createLogger({
31
32
 
32
33
  const secretService = SecretService.create(Buffer.alloc(32, 7));
33
34
 
35
+ /** The capability arm never reads the store — a store that proves it. */
36
+ const store: BoundExecutionStore = {
37
+ getResource: () =>
38
+ Promise.reject(new Error("the capability arm must not read the store")),
39
+ findByField: () =>
40
+ Promise.reject(new Error("the capability arm must not read the store")),
41
+ };
42
+
34
43
  /** A minimal HandlerContext: the resolve path reads only the auth header. */
35
44
  function ctxWithBearer(token: string): HandlerContext {
36
45
  const headers = new Headers();
@@ -84,7 +93,12 @@ describe("resolveValuesForCaller (capability delegation — C4)", () => {
84
93
  const provider = providerWith(async () => true);
85
94
  const ec = await executionContext();
86
95
  await resolveValuesForCaller(
87
- { logger: silentLogger, secretService, runnerAuthService: provider },
96
+ {
97
+ logger: silentLogger,
98
+ secretService,
99
+ runnerAuthService: provider,
100
+ store,
101
+ },
88
102
  ctxWithBearer("sandbox-token"),
89
103
  ec,
90
104
  );
@@ -96,7 +110,12 @@ describe("resolveValuesForCaller (capability delegation — C4)", () => {
96
110
  const provider = providerWith(async () => false);
97
111
  const ec = await executionContext();
98
112
  await resolveValuesForCaller(
99
- { logger: silentLogger, secretService, runnerAuthService: provider },
113
+ {
114
+ logger: silentLogger,
115
+ secretService,
116
+ runnerAuthService: provider,
117
+ store,
118
+ },
100
119
  ctxWithBearer("someone-elses-token"),
101
120
  ec,
102
121
  );
@@ -107,7 +126,12 @@ describe("resolveValuesForCaller (capability delegation — C4)", () => {
107
126
  const provider = providerWith(async () => true);
108
127
  const ec = await executionContext();
109
128
  await resolveValuesForCaller(
110
- { logger: silentLogger, secretService, runnerAuthService: provider },
129
+ {
130
+ logger: silentLogger,
131
+ secretService,
132
+ runnerAuthService: provider,
133
+ store,
134
+ },
111
135
  ctxWithBearer(""),
112
136
  ec,
113
137
  );
@@ -121,7 +145,12 @@ describe("resolveValuesForCaller (capability delegation — C4)", () => {
121
145
  });
122
146
  const ec = await executionContext();
123
147
  await resolveValuesForCaller(
124
- { logger: silentLogger, secretService, runnerAuthService: provider },
148
+ {
149
+ logger: silentLogger,
150
+ secretService,
151
+ runnerAuthService: provider,
152
+ store,
153
+ },
125
154
  ctxWithBearer("sandbox-token"),
126
155
  ec,
127
156
  );