@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.
- package/dist/authorization/lane-admission.d.ts +50 -0
- package/dist/authorization/lane-admission.d.ts.map +1 -0
- package/dist/authorization/lane-admission.js +13 -0
- package/dist/authorization/lane-admission.js.map +1 -0
- package/dist/authorization/schedule-fire-caller.d.ts +2 -2
- package/dist/authorization/schedule-fire-caller.d.ts.map +1 -1
- package/dist/authorization/schedule-fire-caller.js +8 -8
- package/dist/authorization/schedule-fire-caller.js.map +1 -1
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +66 -14
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/inprocess.d.ts.map +1 -1
- package/dist/boot/inprocess.js +6 -1
- package/dist/boot/inprocess.js.map +1 -1
- package/dist/domain/executioncontext/resolve-values-for-caller.d.ts +33 -14
- package/dist/domain/executioncontext/resolve-values-for-caller.d.ts.map +1 -1
- package/dist/domain/executioncontext/resolve-values-for-caller.js +32 -8
- package/dist/domain/executioncontext/resolve-values-for-caller.js.map +1 -1
- package/dist/domain/identityaccount/actor.d.ts +33 -18
- package/dist/domain/identityaccount/actor.d.ts.map +1 -1
- package/dist/domain/identityaccount/actor.js +20 -3
- package/dist/domain/identityaccount/actor.js.map +1 -1
- package/dist/domain/identityaccount/resolve.d.ts +19 -0
- package/dist/domain/identityaccount/resolve.d.ts.map +1 -1
- package/dist/domain/identityaccount/resolve.js +12 -0
- package/dist/domain/identityaccount/resolve.js.map +1 -1
- package/dist/domain/mcpserver/connect-execution-id.d.ts +5 -0
- package/dist/domain/mcpserver/connect-execution-id.d.ts.map +1 -0
- package/dist/domain/mcpserver/connect-execution-id.js +32 -0
- package/dist/domain/mcpserver/connect-execution-id.js.map +1 -0
- package/dist/domain/mcpserver/connect.d.ts +28 -2
- package/dist/domain/mcpserver/connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/connect.js +27 -7
- package/dist/domain/mcpserver/connect.js.map +1 -1
- package/dist/domain/mcpserver/engine.d.ts +9 -3
- package/dist/domain/mcpserver/engine.d.ts.map +1 -1
- package/dist/domain/mcpserver/engine.js.map +1 -1
- package/dist/domain/mcpserver/start-connect.js +1 -1
- package/dist/domain/mcpserver/start-connect.js.map +1 -1
- package/dist/domain/platform/controller.d.ts +24 -11
- package/dist/domain/platform/controller.d.ts.map +1 -1
- package/dist/domain/platform/controller.js.map +1 -1
- package/dist/domain/plugin/overlay/sanitize.d.ts.map +1 -1
- package/dist/domain/plugin/overlay/sanitize.js +4 -2
- package/dist/domain/plugin/overlay/sanitize.js.map +1 -1
- package/dist/domain/workflowexecution/phases.d.ts +21 -0
- package/dist/domain/workflowexecution/phases.d.ts.map +1 -0
- package/dist/domain/workflowexecution/phases.js +31 -0
- package/dist/domain/workflowexecution/phases.js.map +1 -0
- package/dist/identity/oidc-verifier.js +8 -1
- package/dist/identity/oidc-verifier.js.map +1 -1
- package/dist/pipeline/apiresource-meta.d.ts +15 -0
- package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
- package/dist/pipeline/apiresource-meta.js +34 -0
- package/dist/pipeline/apiresource-meta.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +5 -3
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/dist/runnerauth/bound-execution.d.ts +27 -0
- package/dist/runnerauth/bound-execution.d.ts.map +1 -0
- package/dist/runnerauth/bound-execution.js +137 -0
- package/dist/runnerauth/bound-execution.js.map +1 -0
- package/dist/runnerauth/built-in-runner-credential-provider.d.ts +28 -0
- package/dist/runnerauth/built-in-runner-credential-provider.d.ts.map +1 -0
- package/dist/runnerauth/built-in-runner-credential-provider.js +217 -0
- package/dist/runnerauth/built-in-runner-credential-provider.js.map +1 -0
- package/dist/runnerauth/constants.d.ts +63 -0
- package/dist/runnerauth/constants.d.ts.map +1 -0
- package/dist/runnerauth/constants.js +63 -0
- package/dist/runnerauth/constants.js.map +1 -0
- package/dist/runnerauth/dispatch-credential.d.ts +38 -0
- package/dist/runnerauth/dispatch-credential.d.ts.map +1 -0
- package/dist/runnerauth/dispatch-credential.js +17 -0
- package/dist/runnerauth/dispatch-credential.js.map +1 -0
- package/dist/runnerauth/runner-credential-provider.d.ts +49 -11
- package/dist/runnerauth/runner-credential-provider.d.ts.map +1 -1
- package/dist/runnerauth/runner-credential-provider.js +16 -4
- package/dist/runnerauth/runner-credential-provider.js.map +1 -1
- package/dist/runnerauth/runner-subject-verifier.d.ts +14 -0
- package/dist/runnerauth/runner-subject-verifier.d.ts.map +1 -0
- package/dist/runnerauth/runner-subject-verifier.js +115 -0
- package/dist/runnerauth/runner-subject-verifier.js.map +1 -0
- package/dist/runnerauth/runnerauth.d.ts +33 -4
- package/dist/runnerauth/runnerauth.d.ts.map +1 -1
- package/dist/runnerauth/runnerauth.js +131 -35
- package/dist/runnerauth/runnerauth.js.map +1 -1
- package/dist/temporal/agentexecution/engine-client.d.ts +13 -0
- package/dist/temporal/agentexecution/engine-client.d.ts.map +1 -1
- package/dist/temporal/agentexecution/engine-client.js +10 -2
- package/dist/temporal/agentexecution/engine-client.js.map +1 -1
- package/dist/temporal/agentexecution/workflow-input.d.ts +13 -0
- package/dist/temporal/agentexecution/workflow-input.d.ts.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.d.ts.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js +35 -31
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js.map +1 -1
- package/dist/temporal/workflowexecution/engine-client.d.ts +24 -0
- package/dist/temporal/workflowexecution/engine-client.d.ts.map +1 -1
- package/dist/temporal/workflowexecution/engine-client.js +26 -19
- package/dist/temporal/workflowexecution/engine-client.js.map +1 -1
- package/dist/temporal/workflowexecution/workflow-input.d.ts +14 -0
- package/dist/temporal/workflowexecution/workflow-input.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/authorization/__tests__/lane-admission.test.ts +115 -0
- package/src/authorization/lane-admission.ts +73 -0
- package/src/authorization/schedule-fire-caller.ts +10 -14
- package/src/boot/compose.ts +79 -22
- package/src/boot/inprocess.ts +7 -1
- package/src/domain/executioncontext/__tests__/executioncontext.test.ts +104 -0
- package/src/domain/executioncontext/__tests__/resolve-values-capability.test.ts +33 -4
- package/src/domain/executioncontext/resolve-values-for-caller.ts +79 -22
- package/src/domain/identityaccount/__tests__/account-for-stamp.test.ts +111 -0
- package/src/domain/identityaccount/actor.ts +61 -21
- package/src/domain/identityaccount/resolve.ts +31 -0
- package/src/domain/mcpserver/__tests__/connect.test.ts +17 -2
- package/src/domain/mcpserver/connect-execution-id.ts +34 -0
- package/src/domain/mcpserver/connect.ts +38 -3
- package/src/domain/mcpserver/engine.ts +9 -3
- package/src/domain/mcpserver/start-connect.ts +1 -1
- package/src/domain/platform/controller.ts +24 -11
- package/src/domain/plugin/__tests__/plugin.test.ts +116 -1
- package/src/domain/plugin/__tests__/sanitize.test.ts +2 -2
- package/src/domain/plugin/overlay/sanitize.ts +4 -2
- package/src/domain/workflowexecution/__tests__/phases.test.ts +58 -0
- package/src/domain/workflowexecution/phases.ts +33 -0
- package/src/extensions/__tests__/extension-composition.test.ts +7 -1
- package/src/identity/__tests__/oidc-verifier.test.ts +22 -0
- package/src/identity/oidc-verifier.ts +8 -1
- package/src/pipeline/__tests__/kind-by-id-prefix.test.ts +75 -0
- package/src/pipeline/apiresource-meta.ts +41 -0
- package/src/pipeline/steps/__tests__/guard-memory-capture.test.ts +55 -35
- package/src/pipeline/steps/guard-memory-capture.ts +5 -3
- package/src/runnerauth/__tests__/bound-execution.test.ts +334 -0
- package/src/runnerauth/__tests__/built-in-runner-credential-provider.test.ts +516 -0
- package/src/runnerauth/__tests__/dispatch-credential.test.ts +94 -0
- package/src/runnerauth/__tests__/runner-credential-provider.test.ts +27 -0
- package/src/runnerauth/__tests__/runner-subject-composed.test.ts +497 -0
- package/src/runnerauth/__tests__/runner-subject-verifier.test.ts +541 -0
- package/src/runnerauth/bound-execution.ts +270 -0
- package/src/runnerauth/built-in-runner-credential-provider.ts +300 -0
- package/src/runnerauth/constants.ts +74 -0
- package/src/runnerauth/dispatch-credential.ts +63 -0
- package/src/runnerauth/runner-credential-provider.ts +60 -11
- package/src/runnerauth/runner-subject-verifier.ts +152 -0
- package/src/runnerauth/runnerauth.ts +148 -38
- package/src/temporal/agentexecution/__tests__/engine-client.test.ts +107 -38
- package/src/temporal/agentexecution/__tests__/invoke-workflow.test.ts +105 -2
- package/src/temporal/agentexecution/engine-client.ts +27 -2
- package/src/temporal/agentexecution/workflow-input.ts +13 -0
- package/src/temporal/agentexecution/workflows/invoke-agent-execution.ts +90 -44
- package/src/temporal/workflowexecution/__tests__/engine-client.test.ts +139 -33
- package/src/temporal/workflowexecution/engine-client.ts +48 -21
- 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.
|
|
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.
|
|
66
|
-
"@stigmer/protos": "3.
|
|
67
|
-
"@stigmer/temporal-codecs": "3.
|
|
68
|
-
"@stigmer/zip-structure": "3.
|
|
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
|
|
26
|
-
* account
|
|
27
|
-
*
|
|
28
|
-
* same two shapes `Person` aliases
|
|
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
|
|
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:
|
|
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),
|
package/src/boot/compose.ts
CHANGED
|
@@ -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)
|
|
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
|
-
?
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
//
|
|
1478
|
-
//
|
|
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({
|
package/src/boot/inprocess.ts
CHANGED
|
@@ -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) =>
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
148
|
+
{
|
|
149
|
+
logger: silentLogger,
|
|
150
|
+
secretService,
|
|
151
|
+
runnerAuthService: provider,
|
|
152
|
+
store,
|
|
153
|
+
},
|
|
125
154
|
ctxWithBearer("sandbox-token"),
|
|
126
155
|
ec,
|
|
127
156
|
);
|