@stigmer/server 3.15.3-dev.20260916211208 → 3.16.1-dev.20260917224426
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 +12 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +68 -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 +31 -11
- package/dist/domain/platform/controller.d.ts.map +1 -1
- package/dist/domain/platform/controller.js +2 -3
- package/dist/domain/platform/controller.js.map +1 -1
- package/dist/domain/plugin/materialize/agent.d.ts +10 -1
- package/dist/domain/plugin/materialize/agent.d.ts.map +1 -1
- package/dist/domain/plugin/materialize/agent.js +35 -1
- package/dist/domain/plugin/materialize/agent.js.map +1 -1
- package/dist/domain/plugin/materialize/plan.d.ts.map +1 -1
- package/dist/domain/plugin/materialize/plan.js +1 -1
- package/dist/domain/plugin/materialize/plan.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/project/reconcile.d.ts.map +1 -1
- package/dist/domain/project/reconcile.js +14 -10
- package/dist/domain/project/reconcile.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 +93 -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/__tests__/platform.test.ts +47 -1
- package/src/domain/platform/controller.ts +33 -14
- package/src/domain/plugin/__tests__/materialize.test.ts +60 -1
- package/src/domain/plugin/__tests__/plugin.test.ts +122 -1
- package/src/domain/plugin/__tests__/sanitize.test.ts +2 -2
- package/src/domain/plugin/materialize/agent.ts +38 -0
- package/src/domain/plugin/materialize/plan.ts +7 -1
- package/src/domain/plugin/overlay/sanitize.ts +4 -2
- package/src/domain/project/__tests__/reconcile.test.ts +51 -5
- package/src/domain/project/reconcile.ts +20 -15
- 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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins `isTerminalWorkflowExecutionPhase` (phases.ts) over every member
|
|
3
|
+
* of the workflow ExecutionPhase enum, so the set is stated by
|
|
4
|
+
* enumeration and a new phase added to the contract lands in exactly one
|
|
5
|
+
* arm here. The contrast with subscribe.ts's `isWorkflowTerminalPhase`
|
|
6
|
+
* (TERMINATED omitted on purpose) is pinned so the two are never merged.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
|
|
10
|
+
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/enum_pb";
|
|
11
|
+
|
|
12
|
+
import { isTerminalWorkflowExecutionPhase } from "../phases.js";
|
|
13
|
+
import { isWorkflowTerminalPhase } from "../subscribe.js";
|
|
14
|
+
|
|
15
|
+
const TERMINAL = [
|
|
16
|
+
ExecutionPhase.EXECUTION_COMPLETED,
|
|
17
|
+
ExecutionPhase.EXECUTION_FAILED,
|
|
18
|
+
ExecutionPhase.EXECUTION_CANCELLED,
|
|
19
|
+
ExecutionPhase.EXECUTION_TERMINATED,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
describe("isTerminalWorkflowExecutionPhase", () => {
|
|
23
|
+
it.each(TERMINAL.map((phase) => [ExecutionPhase[phase], phase] as const))(
|
|
24
|
+
"%s will never run again",
|
|
25
|
+
(_name, phase) => {
|
|
26
|
+
expect(isTerminalWorkflowExecutionPhase(phase)).toBe(true);
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
it.each([
|
|
31
|
+
["UNSPECIFIED", ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED],
|
|
32
|
+
["PENDING", ExecutionPhase.EXECUTION_PENDING],
|
|
33
|
+
["IN_PROGRESS", ExecutionPhase.EXECUTION_IN_PROGRESS],
|
|
34
|
+
["PAUSED", ExecutionPhase.EXECUTION_PAUSED],
|
|
35
|
+
])("%s may still run", (_name, phase) => {
|
|
36
|
+
expect(isTerminalWorkflowExecutionPhase(phase)).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("covers every member of the enum — a new phase must be classified here", () => {
|
|
40
|
+
const members = Object.values(ExecutionPhase).filter(
|
|
41
|
+
(value): value is ExecutionPhase => typeof value === "number",
|
|
42
|
+
);
|
|
43
|
+
const classified = members.filter((phase) =>
|
|
44
|
+
isTerminalWorkflowExecutionPhase(phase),
|
|
45
|
+
);
|
|
46
|
+
expect(classified.sort()).toEqual([...TERMINAL].sort());
|
|
47
|
+
expect(members).toHaveLength(8);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("differs from the subscribe stream's close set on TERMINATED alone — deliberate, not to be merged", () => {
|
|
51
|
+
expect(isWorkflowTerminalPhase(ExecutionPhase.EXECUTION_TERMINATED)).toBe(
|
|
52
|
+
false,
|
|
53
|
+
);
|
|
54
|
+
expect(
|
|
55
|
+
isTerminalWorkflowExecutionPhase(ExecutionPhase.EXECUTION_TERMINATED),
|
|
56
|
+
).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workflow execution's "will this run ever run again?" predicate —
|
|
3
|
+
* the twin of domain/agentexecution/phases.ts `isTerminalExecutionPhase`,
|
|
4
|
+
* stated once so every consumer names the same set: COMPLETED, FAILED,
|
|
5
|
+
* CANCELLED and TERMINATED. PAUSED is NOT terminal (lifecycle.ts:
|
|
6
|
+
* `applyLifecyclePhaseTransition` keeps `completed_at` as-is on a pause
|
|
7
|
+
* and clears it on the way back to IN_PROGRESS), and neither is PENDING.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately distinct from `isWorkflowTerminalPhase` in subscribe.ts,
|
|
10
|
+
* which OMITS TERMINATED — the disclosed Go quirk the subscribe stream
|
|
11
|
+
* ports faithfully as its close set. Do not merge the two; that file's
|
|
12
|
+
* header carries the reason.
|
|
13
|
+
*
|
|
14
|
+
* The first consumer is the runner-credential lane (runnerauth/
|
|
15
|
+
* bound-execution.ts): a run credential is valid while its run may still
|
|
16
|
+
* run, so a terminated workflow's credential must die exactly as a
|
|
17
|
+
* completed one's does.
|
|
18
|
+
*/
|
|
19
|
+
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/enum_pb";
|
|
20
|
+
|
|
21
|
+
export function isTerminalWorkflowExecutionPhase(
|
|
22
|
+
phase: ExecutionPhase,
|
|
23
|
+
): boolean {
|
|
24
|
+
switch (phase) {
|
|
25
|
+
case ExecutionPhase.EXECUTION_COMPLETED:
|
|
26
|
+
case ExecutionPhase.EXECUTION_FAILED:
|
|
27
|
+
case ExecutionPhase.EXECUTION_CANCELLED:
|
|
28
|
+
case ExecutionPhase.EXECUTION_TERMINATED:
|
|
29
|
+
return true;
|
|
30
|
+
default:
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -544,7 +544,13 @@ describe("extension composition (require-authentication posture)", () => {
|
|
|
544
544
|
};
|
|
545
545
|
expect(parsed.posture).toBe("required");
|
|
546
546
|
expect(parsed.source).toBe("extension 'fake-identity'");
|
|
547
|
-
|
|
547
|
+
// A unit that declares the posture and brings verifiers but NO
|
|
548
|
+
// Authorizer is the BUILT-IN authorization posture: open source
|
|
549
|
+
// composes its Authorizer, and with it the runner-subject verifier
|
|
550
|
+
// between the API-key lane and the unit's own (runnerauth/
|
|
551
|
+
// runner-subject-verifier.ts). A unit with its own Authorizer gets
|
|
552
|
+
// neither (runner-subject-composed.test.ts pins that chain).
|
|
553
|
+
expect(parsed.verifiers).toBe("apikey, runner, fake-verifier");
|
|
548
554
|
});
|
|
549
555
|
|
|
550
556
|
it("a declared posture whose unit registers no verifier of its own is a boot throw naming the unit — the API-key lane alone cannot bootstrap", async () => {
|
|
@@ -218,6 +218,28 @@ describe("the byte-pinned classifyAuthError arms", () => {
|
|
|
218
218
|
expect(ConnectError.from(error).code).toBe(Code.Unauthenticated);
|
|
219
219
|
expect(ConnectError.from(error).rawMessage).toBe(INVALID_TOKEN_MESSAGE);
|
|
220
220
|
});
|
|
221
|
+
|
|
222
|
+
it("a JWT whose algorithm the JWKS cannot serve (HS256 against RS256 keys) → 'invalid token', a CREDENTIAL rejection, never a plain error the chassis maps INTERNAL", async () => {
|
|
223
|
+
// The shape the server's own execution-scoped runner token has
|
|
224
|
+
// (runnerauth.ts). Whether this verifier should ever SEE it is the
|
|
225
|
+
// chain order's business (the runner verifier claims it first); that
|
|
226
|
+
// it must not answer INTERNAL when it does is this arm's. Until
|
|
227
|
+
// 2026-09-16 it threw jose's JOSENotSupported unclassified, and a
|
|
228
|
+
// self-host with sign-in on failed every agent execution with
|
|
229
|
+
// "internal server error" (stigmer#1137).
|
|
230
|
+
const segment = (value: unknown): string =>
|
|
231
|
+
Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
232
|
+
const hs256 = `${segment({ alg: "HS256", typ: "JWT" })}.${segment({
|
|
233
|
+
token_type: "execution_scoped",
|
|
234
|
+
execution_id: "aex_1",
|
|
235
|
+
iat: 1,
|
|
236
|
+
exp: 4102444800,
|
|
237
|
+
})}.${segment("a-mac-this-verifier-cannot-check")}`;
|
|
238
|
+
const error = await rejectionOf(verifier().verify(hs256));
|
|
239
|
+
expect(error).toBeInstanceOf(ConnectError);
|
|
240
|
+
expect(ConnectError.from(error).code).toBe(Code.Unauthenticated);
|
|
241
|
+
expect(ConnectError.from(error).rawMessage).toBe(INVALID_TOKEN_MESSAGE);
|
|
242
|
+
});
|
|
221
243
|
});
|
|
222
244
|
|
|
223
245
|
describe("subject → account resolution (20260911.11 Q-IA-2, A1)", () => {
|
|
@@ -159,7 +159,14 @@ function classifyJoseError(error: unknown): unknown {
|
|
|
159
159
|
}
|
|
160
160
|
if (
|
|
161
161
|
error instanceof joseErrors.JWTInvalid ||
|
|
162
|
-
error instanceof joseErrors.JWSInvalid
|
|
162
|
+
error instanceof joseErrors.JWSInvalid ||
|
|
163
|
+
// A JWT whose algorithm the issuer's key set cannot serve (an HS256
|
|
164
|
+
// token against an RS256 JWKS): a credential this verifier recognizes
|
|
165
|
+
// but cannot verify, so a credential rejection — never the
|
|
166
|
+
// infrastructure arm. Until 2026-09-16 it fell through to INTERNAL,
|
|
167
|
+
// and a self-host with sign-in on failed every agent execution on its
|
|
168
|
+
// own runner token (stigmer#1137).
|
|
169
|
+
error instanceof joseErrors.JOSENotSupported
|
|
163
170
|
) {
|
|
164
171
|
return new ConnectError(INVALID_TOKEN_MESSAGE, Code.Unauthenticated);
|
|
165
172
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins `kindByIdPrefix` (pipeline/apiresource-meta.ts), the inverse of
|
|
3
|
+
* `getIdPrefix` over the ids the server mints (`<prefix>_<ulid>`,
|
|
4
|
+
* pipeline/steps/defaults.ts). The runner-credential lane reads it to
|
|
5
|
+
* learn which execution kind a token's binding names — an agent
|
|
6
|
+
* execution (`aex_`) or a workflow execution (`wex_`) — without a second
|
|
7
|
+
* claim on the token or a guess.
|
|
8
|
+
*
|
|
9
|
+
* Two properties are load-bearing:
|
|
10
|
+
*
|
|
11
|
+
* - NEVER a throw. The id arrives inside a credential; the caller
|
|
12
|
+
* refuses with its own sentence, so anything that is not a known
|
|
13
|
+
* prefix followed by an underscore is the unknown kind.
|
|
14
|
+
* - Every `id_prefix` in the contract is UNIQUE. The lookup is a Map
|
|
15
|
+
* over the `kind_meta` table; a duplicate prefix would silently win
|
|
16
|
+
* for one kind and lose for the other. The pin below turns a future
|
|
17
|
+
* duplicate into a reviewed change.
|
|
18
|
+
*/
|
|
19
|
+
import { describe, expect, it } from "vitest";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
ApiResourceKind,
|
|
23
|
+
ApiResourceKindSchema,
|
|
24
|
+
kind_meta,
|
|
25
|
+
} from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
26
|
+
import { getOption, hasOption } from "@bufbuild/protobuf";
|
|
27
|
+
|
|
28
|
+
import { getIdPrefix, kindByIdPrefix } from "../apiresource-meta.js";
|
|
29
|
+
|
|
30
|
+
describe("kindByIdPrefix — a minted id names its kind by prefix", () => {
|
|
31
|
+
it.each([
|
|
32
|
+
["aex_01m2mp45efcjvq2z1e1yn4cyz7", ApiResourceKind.agent_execution],
|
|
33
|
+
["wex_01m2mp45efcjvq2z1e1yn4cyz7", ApiResourceKind.workflow_execution],
|
|
34
|
+
["ses_x", ApiResourceKind.session],
|
|
35
|
+
["ida_carol", ApiResourceKind.identity_account],
|
|
36
|
+
])("%s is %s", (id, kind) => {
|
|
37
|
+
expect(kindByIdPrefix(id)).toBe(kind);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it.each([
|
|
41
|
+
["zzz_unknown_kind", "an unknown prefix"],
|
|
42
|
+
["aex", "a prefix with no underscore"],
|
|
43
|
+
["_aex", "a leading underscore names no prefix"],
|
|
44
|
+
["", "empty"],
|
|
45
|
+
["AEX_upper", "prefixes are lowercase in the contract"],
|
|
46
|
+
["stk_notajwt", "an API key's prefix is not a kind"],
|
|
47
|
+
])("%s is the unknown kind (%s) — never a throw", (id) => {
|
|
48
|
+
expect(kindByIdPrefix(id)).toBe(ApiResourceKind.api_resource_kind_unknown);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("round-trips with getIdPrefix over every kind the contract declares", () => {
|
|
52
|
+
for (const value of ApiResourceKindSchema.values) {
|
|
53
|
+
if (!hasOption(value, kind_meta)) continue;
|
|
54
|
+
const kind = value.number as ApiResourceKind;
|
|
55
|
+
expect(kindByIdPrefix(`${getIdPrefix(kind)}_01anyulid`), value.name).toBe(
|
|
56
|
+
kind,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("every id_prefix in the contract is unique — the table pin a duplicate must change", () => {
|
|
62
|
+
const seen = new Map<string, string>();
|
|
63
|
+
for (const value of ApiResourceKindSchema.values) {
|
|
64
|
+
if (!hasOption(value, kind_meta)) continue;
|
|
65
|
+
const prefix = getOption(value, kind_meta).idPrefix;
|
|
66
|
+
expect(prefix, `${value.name} declares no id_prefix`).not.toBe("");
|
|
67
|
+
const holder = seen.get(prefix);
|
|
68
|
+
expect(
|
|
69
|
+
holder,
|
|
70
|
+
`id_prefix '${prefix}' is declared by both ${holder} and ${value.name}`,
|
|
71
|
+
).toBeUndefined();
|
|
72
|
+
seen.set(prefix, value.name);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -351,3 +351,44 @@ function kindsByEnumName(): Map<string, ApiResourceKind> {
|
|
|
351
351
|
}
|
|
352
352
|
return enumNameCache;
|
|
353
353
|
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The kind a minted resource id belongs to, read off its prefix — the
|
|
357
|
+
* inverse of `getIdPrefix` over the ids `generateId` mints
|
|
358
|
+
* (`<prefix>_<ulid>`, pipeline/steps/defaults.ts). The one consumer today
|
|
359
|
+
* is the runner-credential lane, whose token binds an execution by id and
|
|
360
|
+
* must know whether that id names an agent execution or a workflow
|
|
361
|
+
* execution without a second claim or a guess. Anything that is not
|
|
362
|
+
* `<known prefix>_<rest>` — no underscore, an unknown prefix, the empty
|
|
363
|
+
* string — is `api_resource_kind_unknown`; never a throw, because the id
|
|
364
|
+
* arrived inside a credential and the caller refuses with its own
|
|
365
|
+
* sentence. Every `id_prefix` in the contract is unique; the table pin in
|
|
366
|
+
* __tests__ makes a future duplicate a reviewed change, since this lookup
|
|
367
|
+
* would otherwise resolve it silently to one of the two.
|
|
368
|
+
*/
|
|
369
|
+
export function kindByIdPrefix(id: string): ApiResourceKind {
|
|
370
|
+
const separator = id.indexOf("_");
|
|
371
|
+
if (separator <= 0) {
|
|
372
|
+
return ApiResourceKind.api_resource_kind_unknown;
|
|
373
|
+
}
|
|
374
|
+
return (
|
|
375
|
+
kindsByIdPrefix().get(id.slice(0, separator)) ??
|
|
376
|
+
ApiResourceKind.api_resource_kind_unknown
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let idPrefixCache: Map<string, ApiResourceKind> | undefined;
|
|
381
|
+
|
|
382
|
+
function kindsByIdPrefix(): Map<string, ApiResourceKind> {
|
|
383
|
+
if (idPrefixCache === undefined) {
|
|
384
|
+
idPrefixCache = new Map(
|
|
385
|
+
ApiResourceKindSchema.values
|
|
386
|
+
.filter((value) => hasOption(value, kind_meta))
|
|
387
|
+
.map((value) => [
|
|
388
|
+
getOption(value, kind_meta).idPrefix,
|
|
389
|
+
value.number as ApiResourceKind,
|
|
390
|
+
]),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
return idPrefixCache;
|
|
394
|
+
}
|
|
@@ -60,7 +60,10 @@ function memoryCtx(
|
|
|
60
60
|
|
|
61
61
|
/** A provider whose only relevant surface is the capture capability. */
|
|
62
62
|
function providerWith(
|
|
63
|
-
decide?: (
|
|
63
|
+
decide?: (
|
|
64
|
+
caller: CallerIdentity,
|
|
65
|
+
org: string,
|
|
66
|
+
) => MemoryCaptureDecision | Promise<MemoryCaptureDecision>,
|
|
64
67
|
): RunnerCredentialProvider {
|
|
65
68
|
return {
|
|
66
69
|
isEnabled: () => false,
|
|
@@ -79,8 +82,34 @@ function jwtWith(payload: Record<string, unknown>): string {
|
|
|
79
82
|
return `header.${body}.signature`;
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
async function rejectionOf(run: void | Promise<void>): Promise<unknown> {
|
|
86
|
+
return Promise.resolve(run).then(
|
|
87
|
+
() => {
|
|
88
|
+
throw new Error("expected the step to refuse");
|
|
89
|
+
},
|
|
90
|
+
(error: unknown) => error,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
describe("GuardMemoryCapture with the capability composed", () => {
|
|
83
|
-
it("
|
|
95
|
+
it("a capability that answers with a promise is awaited — the built-in provider reads the run's row for the session", async () => {
|
|
96
|
+
const step = newGuardMemoryCaptureStep<typeof MemorySchema>(
|
|
97
|
+
providerWith(() =>
|
|
98
|
+
Promise.resolve({
|
|
99
|
+
verdict: "admit" as const,
|
|
100
|
+
subjectIdentityAccountId: "ida_human",
|
|
101
|
+
provedSessionId: "ses_from_row",
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
const ctx = memoryCtx(SANDBOX_CALLER);
|
|
106
|
+
await step.execute(ctx);
|
|
107
|
+
expect(memoryCaptureCredentialOf(ctx)?.provedSessionId).toBe(
|
|
108
|
+
"ses_from_row",
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("admit stashes the proved claims for ResolveMemoryDefaults", async () => {
|
|
84
113
|
const step = newGuardMemoryCaptureStep<typeof MemorySchema>(
|
|
85
114
|
providerWith(() => ({
|
|
86
115
|
verdict: "admit",
|
|
@@ -89,23 +118,18 @@ describe("GuardMemoryCapture with the capability composed", () => {
|
|
|
89
118
|
})),
|
|
90
119
|
);
|
|
91
120
|
const ctx = memoryCtx(SANDBOX_CALLER);
|
|
92
|
-
step.execute(ctx);
|
|
121
|
+
await step.execute(ctx);
|
|
93
122
|
expect(memoryCaptureCredentialOf(ctx)).toEqual({
|
|
94
123
|
subjectIdentityAccountId: "ida_human",
|
|
95
124
|
provedSessionId: "ses_proved",
|
|
96
125
|
});
|
|
97
126
|
});
|
|
98
127
|
|
|
99
|
-
it("refuse answers the byte-pinned PERMISSION_DENIED copy", () => {
|
|
128
|
+
it("refuse answers the byte-pinned PERMISSION_DENIED copy", async () => {
|
|
100
129
|
const step = newGuardMemoryCaptureStep<typeof MemorySchema>(
|
|
101
130
|
providerWith(() => ({ verdict: "refuse" })),
|
|
102
131
|
);
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
step.execute(memoryCtx(SANDBOX_CALLER));
|
|
106
|
-
} catch (e) {
|
|
107
|
-
error = e;
|
|
108
|
-
}
|
|
132
|
+
const error = await rejectionOf(step.execute(memoryCtx(SANDBOX_CALLER)));
|
|
109
133
|
expect(error).toBeInstanceOf(ConnectError);
|
|
110
134
|
expect((error as ConnectError).code).toBe(Code.PermissionDenied);
|
|
111
135
|
expect((error as ConnectError).rawMessage).toBe(
|
|
@@ -113,7 +137,7 @@ describe("GuardMemoryCapture with the capability composed", () => {
|
|
|
113
137
|
);
|
|
114
138
|
});
|
|
115
139
|
|
|
116
|
-
it("hands the capability the caller and the capture org", () => {
|
|
140
|
+
it("hands the capability the caller and the capture org", async () => {
|
|
117
141
|
const observed: Array<{ caller: CallerIdentity; org: string }> = [];
|
|
118
142
|
const step = newGuardMemoryCaptureStep<typeof MemorySchema>(
|
|
119
143
|
providerWith((caller, org) => {
|
|
@@ -121,11 +145,11 @@ describe("GuardMemoryCapture with the capability composed", () => {
|
|
|
121
145
|
return { verdict: "no-opinion" };
|
|
122
146
|
}),
|
|
123
147
|
);
|
|
124
|
-
step.execute(memoryCtx(SANDBOX_CALLER, "org_zeta"));
|
|
148
|
+
await step.execute(memoryCtx(SANDBOX_CALLER, "org_zeta"));
|
|
125
149
|
expect(observed).toEqual([{ caller: SANDBOX_CALLER, org: "org_zeta" }]);
|
|
126
150
|
});
|
|
127
151
|
|
|
128
|
-
it("an org-mismatch throw from the capability propagates untouched", () => {
|
|
152
|
+
it("an org-mismatch throw from the capability propagates untouched", async () => {
|
|
129
153
|
const mismatch = new ConnectError(
|
|
130
154
|
"memory capture is scoped to the session's organization",
|
|
131
155
|
Code.PermissionDenied,
|
|
@@ -135,16 +159,12 @@ describe("GuardMemoryCapture with the capability composed", () => {
|
|
|
135
159
|
throw mismatch;
|
|
136
160
|
}),
|
|
137
161
|
);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
} catch (e) {
|
|
142
|
-
error = e;
|
|
143
|
-
}
|
|
144
|
-
expect(error).toBe(mismatch);
|
|
162
|
+
expect(
|
|
163
|
+
await rejectionOf(step.execute(memoryCtx(SANDBOX_CALLER, "org_other"))),
|
|
164
|
+
).toBe(mismatch);
|
|
145
165
|
});
|
|
146
166
|
|
|
147
|
-
it("no-opinion falls through: platform_client_id carriers still refuse", () => {
|
|
167
|
+
it("no-opinion falls through: platform_client_id carriers still refuse", async () => {
|
|
148
168
|
const step = newGuardMemoryCaptureStep<typeof MemorySchema>(
|
|
149
169
|
providerWith(() => ({ verdict: "no-opinion" })),
|
|
150
170
|
);
|
|
@@ -154,18 +174,18 @@ describe("GuardMemoryCapture with the capability composed", () => {
|
|
|
154
174
|
issuer: "stigmer",
|
|
155
175
|
rawToken: jwtWith({ platform_client_id: "pc_1" }),
|
|
156
176
|
};
|
|
157
|
-
expect(
|
|
177
|
+
await expect(step.execute(memoryCtx(minted))).rejects.toThrowError(
|
|
158
178
|
MEMORY_CAPTURE_CALLER_MESSAGE,
|
|
159
179
|
);
|
|
160
180
|
});
|
|
161
181
|
|
|
162
|
-
it("internal and in-process callers never consult the capability", () => {
|
|
182
|
+
it("internal and in-process callers never consult the capability", async () => {
|
|
163
183
|
const step = newGuardMemoryCaptureStep<typeof MemorySchema>(
|
|
164
184
|
providerWith(() => {
|
|
165
185
|
throw new Error("must not be consulted");
|
|
166
186
|
}),
|
|
167
187
|
);
|
|
168
|
-
step.execute(
|
|
188
|
+
await step.execute(
|
|
169
189
|
memoryCtx({
|
|
170
190
|
identityId: "internal",
|
|
171
191
|
callerClass: "internal",
|
|
@@ -173,15 +193,15 @@ describe("GuardMemoryCapture with the capability composed", () => {
|
|
|
173
193
|
rawToken: "",
|
|
174
194
|
}),
|
|
175
195
|
);
|
|
176
|
-
step.execute(memoryCtx({ ...LOCAL_USER, origin: "in-process" }));
|
|
196
|
+
await step.execute(memoryCtx({ ...LOCAL_USER, origin: "in-process" }));
|
|
177
197
|
});
|
|
178
198
|
});
|
|
179
199
|
|
|
180
200
|
describe("GuardMemoryCapture without the capability (the pre-seam gate)", () => {
|
|
181
|
-
it("admits trusted-local and OIDC callers", () => {
|
|
201
|
+
it("admits trusted-local and OIDC callers", async () => {
|
|
182
202
|
const step = newGuardMemoryCaptureStep<typeof MemorySchema>(providerWith());
|
|
183
|
-
step.execute(memoryCtx(LOCAL_USER));
|
|
184
|
-
step.execute(
|
|
203
|
+
await step.execute(memoryCtx(LOCAL_USER));
|
|
204
|
+
await step.execute(
|
|
185
205
|
memoryCtx({
|
|
186
206
|
identityId: "ida_oidc",
|
|
187
207
|
callerClass: "user",
|
|
@@ -191,26 +211,26 @@ describe("GuardMemoryCapture without the capability (the pre-seam gate)", () =>
|
|
|
191
211
|
);
|
|
192
212
|
});
|
|
193
213
|
|
|
194
|
-
it("refuses machine callers and platform_client_id carriers", () => {
|
|
214
|
+
it("refuses machine callers and platform_client_id carriers", async () => {
|
|
195
215
|
const step = newGuardMemoryCaptureStep<typeof MemorySchema>(providerWith());
|
|
196
|
-
expect(
|
|
216
|
+
await expect(
|
|
197
217
|
step.execute(memoryCtx({ ...LOCAL_USER, callerClass: "machine" })),
|
|
198
|
-
).toThrowError(MEMORY_CAPTURE_CALLER_MESSAGE);
|
|
199
|
-
expect(
|
|
218
|
+
).rejects.toThrowError(MEMORY_CAPTURE_CALLER_MESSAGE);
|
|
219
|
+
await expect(
|
|
200
220
|
step.execute(
|
|
201
221
|
memoryCtx({
|
|
202
222
|
...LOCAL_USER,
|
|
203
223
|
rawToken: jwtWith({ platform_client_id: "pc_1" }),
|
|
204
224
|
}),
|
|
205
225
|
),
|
|
206
|
-
).toThrowError(MEMORY_CAPTURE_CALLER_MESSAGE);
|
|
226
|
+
).rejects.toThrowError(MEMORY_CAPTURE_CALLER_MESSAGE);
|
|
207
227
|
});
|
|
208
228
|
|
|
209
|
-
it("admits a runner-shaped caller — the recorded pre-fix posture", () => {
|
|
229
|
+
it("admits a runner-shaped caller — the recorded pre-fix posture", async () => {
|
|
210
230
|
// With no capability composed the gate cannot classify runner
|
|
211
231
|
// credentials — the single-user OSS posture admits them (the #564
|
|
212
232
|
// narrowing is CLOUD policy, shipped in the cloud capability).
|
|
213
233
|
const step = newGuardMemoryCaptureStep<typeof MemorySchema>(providerWith());
|
|
214
|
-
step.execute(memoryCtx(SANDBOX_CALLER));
|
|
234
|
+
await step.execute(memoryCtx(SANDBOX_CALLER));
|
|
215
235
|
});
|
|
216
236
|
});
|
|
@@ -59,7 +59,7 @@ export function newGuardMemoryCaptureStep<Desc extends DescMessage>(
|
|
|
59
59
|
): PipelineStep<Desc> {
|
|
60
60
|
return {
|
|
61
61
|
name: "GuardMemoryCapture",
|
|
62
|
-
execute(ctx: RequestContext<Desc>): void {
|
|
62
|
+
async execute(ctx: RequestContext<Desc>): Promise<void> {
|
|
63
63
|
const caller = ctx.callerIdentity;
|
|
64
64
|
if (caller.callerClass === "internal" || caller.origin === "in-process") {
|
|
65
65
|
// Server-composed traversals are not capture requests from a
|
|
@@ -72,9 +72,11 @@ export function newGuardMemoryCaptureStep<Desc extends DescMessage>(
|
|
|
72
72
|
// arm checks it against the token's own claim (and throws its
|
|
73
73
|
// byte-pinned refusal itself). Empty when the request carries
|
|
74
74
|
// none; Java requires metadata.org before its org-match arm, so
|
|
75
|
-
// an empty value can only ever narrow.
|
|
75
|
+
// an empty value can only ever narrow. Awaited because the
|
|
76
|
+
// built-in provider reads the bound execution's row for the
|
|
77
|
+
// session and org the cloud's token carries as claims.
|
|
76
78
|
const org = metadataOf(ctx.newState)?.org ?? "";
|
|
77
|
-
const decision = decide.call(provider, caller, org);
|
|
79
|
+
const decision = await decide.call(provider, caller, org);
|
|
78
80
|
switch (decision.verdict) {
|
|
79
81
|
case "admit":
|
|
80
82
|
ctx.set(MEMORY_CAPTURE_CREDENTIAL_KEY, {
|