@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
|
@@ -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, {
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the bound-execution reader (bound-execution.ts) in isolation, with
|
|
3
|
+
* the clock injected, so the liveness rule every lane shares is stated
|
|
4
|
+
* by enumeration here and the verifier's and the decrypt lane's tests can
|
|
5
|
+
* assert behavior rather than restate it:
|
|
6
|
+
*
|
|
7
|
+
* - the kind is read off the id alone: the two execution kinds by the
|
|
8
|
+
* contract's prefix table, the connect binding by its own predicate;
|
|
9
|
+
* anything else is `undefined` with no store read;
|
|
10
|
+
* - a connect binding resolves through the connect's ExecutionContext
|
|
11
|
+
* row (by `spec.executionId`), is live while that row exists, and is
|
|
12
|
+
* not a run (`bindsARun`) — the shape both no-`exp` lanes refuse;
|
|
13
|
+
* - live is "not terminal" for each kind's OWN terminal set (the agent
|
|
14
|
+
* side's TERMINATED and WAITING_FOR_APPROVAL, the workflow side's
|
|
15
|
+
* TERMINATED and PAUSED are the arms that differ from a naive set);
|
|
16
|
+
* - a terminal row is live within the grace of its `completed_at`, dead
|
|
17
|
+
* at the boundary and beyond, and dead at once with no timestamp or an
|
|
18
|
+
* unparsable one (fail closed);
|
|
19
|
+
* - a missing row is `undefined`; any other store failure propagates as
|
|
20
|
+
* the same error object.
|
|
21
|
+
*/
|
|
22
|
+
import { create } from "@bufbuild/protobuf";
|
|
23
|
+
import type { DescMessage, MessageShape } from "@bufbuild/protobuf";
|
|
24
|
+
import { describe, expect, it, vi } from "vitest";
|
|
25
|
+
|
|
26
|
+
import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
27
|
+
import { ExecutionPhase as AgentPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
28
|
+
import { ExecutionContextSchema } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/api_pb";
|
|
29
|
+
import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
|
|
30
|
+
import { ExecutionPhase as WorkflowPhase } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/enum_pb";
|
|
31
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
32
|
+
|
|
33
|
+
import { newConnectExecutionId } from "../../domain/mcpserver/connect-execution-id.js";
|
|
34
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
35
|
+
import {
|
|
36
|
+
bindsARun,
|
|
37
|
+
boundExecutionKindOf,
|
|
38
|
+
loadBoundExecution,
|
|
39
|
+
} from "../bound-execution.js";
|
|
40
|
+
import type { BoundExecutionStore } from "../bound-execution.js";
|
|
41
|
+
import { RUN_CREDENTIAL_GRACE_AFTER_TERMINAL_MS } from "../constants.js";
|
|
42
|
+
|
|
43
|
+
const NOW = Date.parse("2026-09-16T12:00:00Z");
|
|
44
|
+
const iso = (offsetMs: number): string =>
|
|
45
|
+
new Date(NOW + offsetMs).toISOString();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A store of rows by id, plus the connect ECs by the execution id they
|
|
49
|
+
* were created for (`findByField` on `spec.executionId`). `reads` records
|
|
50
|
+
* every lookup key, so an arm can assert "no store read" and which read.
|
|
51
|
+
*/
|
|
52
|
+
function storeOf(
|
|
53
|
+
rows: Record<string, unknown>,
|
|
54
|
+
executionContexts: Record<string, unknown> = {},
|
|
55
|
+
): BoundExecutionStore & {
|
|
56
|
+
reads: string[];
|
|
57
|
+
} {
|
|
58
|
+
const reads: string[] = [];
|
|
59
|
+
return {
|
|
60
|
+
reads,
|
|
61
|
+
getResource<Desc extends DescMessage>(
|
|
62
|
+
kind: ApiResourceKind,
|
|
63
|
+
id: string,
|
|
64
|
+
_schema: Desc,
|
|
65
|
+
): Promise<MessageShape<Desc>> {
|
|
66
|
+
reads.push(id);
|
|
67
|
+
const row = rows[id];
|
|
68
|
+
return row === undefined
|
|
69
|
+
? Promise.reject(new ResourceNotFoundError(`${kind}/${id}`))
|
|
70
|
+
: Promise.resolve(row as MessageShape<Desc>);
|
|
71
|
+
},
|
|
72
|
+
findByField<Desc extends DescMessage>(
|
|
73
|
+
kind: ApiResourceKind,
|
|
74
|
+
fieldPath: string,
|
|
75
|
+
value: string,
|
|
76
|
+
_schema: Desc,
|
|
77
|
+
): Promise<MessageShape<Desc>> {
|
|
78
|
+
reads.push(`${fieldPath}=${value}`);
|
|
79
|
+
const row =
|
|
80
|
+
kind === ApiResourceKind.execution_context &&
|
|
81
|
+
fieldPath === "spec.executionId"
|
|
82
|
+
? executionContexts[value]
|
|
83
|
+
: undefined;
|
|
84
|
+
return row === undefined
|
|
85
|
+
? Promise.reject(new ResourceNotFoundError(`${kind}/${value}`))
|
|
86
|
+
: Promise.resolve(row as MessageShape<Desc>);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function connectContext(executionId: string, createdBy: string, org = "acme") {
|
|
92
|
+
return create(ExecutionContextSchema, {
|
|
93
|
+
metadata: { id: "ectx_1", name: `exec-ctx-${executionId}`, org },
|
|
94
|
+
spec: { executionId },
|
|
95
|
+
status: { audit: { specAudit: { createdBy: { id: createdBy } } } },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function agentRun(
|
|
100
|
+
id: string,
|
|
101
|
+
phase: AgentPhase,
|
|
102
|
+
completedAt = "",
|
|
103
|
+
extra: { createdBy?: string; sessionId?: string; org?: string } = {},
|
|
104
|
+
) {
|
|
105
|
+
return create(AgentExecutionSchema, {
|
|
106
|
+
metadata: { id, name: id, org: extra.org ?? "acme" },
|
|
107
|
+
spec: { sessionId: extra.sessionId ?? "" },
|
|
108
|
+
status: {
|
|
109
|
+
phase,
|
|
110
|
+
completedAt,
|
|
111
|
+
audit: { specAudit: { createdBy: { id: extra.createdBy ?? "" } } },
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function workflowRun(id: string, phase: WorkflowPhase, completedAt = "") {
|
|
117
|
+
return create(WorkflowExecutionSchema, {
|
|
118
|
+
metadata: { id, name: id, org: "acme" },
|
|
119
|
+
status: { phase, completedAt },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe("boundExecutionKindOf", () => {
|
|
124
|
+
it.each([
|
|
125
|
+
["aex_1", "agent-execution"],
|
|
126
|
+
["wex_1", "workflow-execution"],
|
|
127
|
+
[newConnectExecutionId("mcps_1"), "mcp-connect"],
|
|
128
|
+
])("%s binds %s", (id, kind) => {
|
|
129
|
+
expect(boundExecutionKindOf(id)).toBe(kind);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it.each([["ses_1"], ["agt_1"], ["zzz_1"], [""], ["aex"], ["connect"]])(
|
|
133
|
+
"%s binds nothing",
|
|
134
|
+
(id) => {
|
|
135
|
+
expect(boundExecutionKindOf(id)).toBeUndefined();
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("bindsARun — what a no-`exp` credential may name", () => {
|
|
141
|
+
it.each([
|
|
142
|
+
["agent-execution", true],
|
|
143
|
+
["workflow-execution", true],
|
|
144
|
+
["mcp-connect", false],
|
|
145
|
+
] as const)("%s → %s", (kind, expected) => {
|
|
146
|
+
expect(bindsARun(kind)).toBe(expected);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("loadBoundExecution", () => {
|
|
151
|
+
it("reads the row's facts the lane needs and nothing else", async () => {
|
|
152
|
+
const store = storeOf({
|
|
153
|
+
aex_1: agentRun("aex_1", AgentPhase.EXECUTION_IN_PROGRESS, "", {
|
|
154
|
+
createdBy: "ida_carol",
|
|
155
|
+
sessionId: "ses_carol",
|
|
156
|
+
org: "acme",
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
expect(await loadBoundExecution(store, "aex_1", NOW)).toEqual({
|
|
160
|
+
kind: "agent-execution",
|
|
161
|
+
executionId: "aex_1",
|
|
162
|
+
org: "acme",
|
|
163
|
+
createdBy: "ida_carol",
|
|
164
|
+
sessionId: "ses_carol",
|
|
165
|
+
live: true,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("an id that binds no kind is undefined with NO store read", async () => {
|
|
170
|
+
const store = storeOf({});
|
|
171
|
+
expect(await loadBoundExecution(store, "ses_1", NOW)).toBeUndefined();
|
|
172
|
+
expect(store.reads).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("the connect binding — the ExecutionContext row by its execution id", () => {
|
|
176
|
+
const connectId = newConnectExecutionId("mcps_1");
|
|
177
|
+
|
|
178
|
+
it("resolves to the row's creator and org, no session, live while the row exists", async () => {
|
|
179
|
+
const store = storeOf(
|
|
180
|
+
{},
|
|
181
|
+
{ [connectId]: connectContext(connectId, "ida_member", "acme") },
|
|
182
|
+
);
|
|
183
|
+
expect(await loadBoundExecution(store, connectId, NOW)).toEqual({
|
|
184
|
+
kind: "mcp-connect",
|
|
185
|
+
executionId: connectId,
|
|
186
|
+
org: "acme",
|
|
187
|
+
createdBy: "ida_member",
|
|
188
|
+
sessionId: "",
|
|
189
|
+
live: true,
|
|
190
|
+
});
|
|
191
|
+
// The read getByExecutionId already makes, and no primary-key read.
|
|
192
|
+
expect(store.reads).toEqual([`spec.executionId=${connectId}`]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("a connect whose row is gone (the connect settled) is undefined — the credential names no row", async () => {
|
|
196
|
+
expect(
|
|
197
|
+
await loadBoundExecution(storeOf({}), connectId, NOW),
|
|
198
|
+
).toBeUndefined();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("a store fault on the EC read propagates as the same error object", async () => {
|
|
202
|
+
const fault = new Error("connection reset");
|
|
203
|
+
const store: BoundExecutionStore = {
|
|
204
|
+
getResource: vi.fn(() => Promise.reject(new Error("unreachable"))),
|
|
205
|
+
findByField: vi.fn(() => Promise.reject(fault)),
|
|
206
|
+
};
|
|
207
|
+
await expect(loadBoundExecution(store, connectId, NOW)).rejects.toBe(
|
|
208
|
+
fault,
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("a missing row is undefined — the typed not-found, and only that, is absorbed", async () => {
|
|
214
|
+
expect(
|
|
215
|
+
await loadBoundExecution(storeOf({}), "aex_missing", NOW),
|
|
216
|
+
).toBeUndefined();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("any other store failure propagates as the same error object", async () => {
|
|
220
|
+
const fault = new Error("connection reset");
|
|
221
|
+
const store: BoundExecutionStore = {
|
|
222
|
+
getResource: vi.fn(() => Promise.reject(fault)),
|
|
223
|
+
findByField: vi.fn(() => Promise.reject(new Error("unreachable"))),
|
|
224
|
+
};
|
|
225
|
+
await expect(loadBoundExecution(store, "aex_1", NOW)).rejects.toBe(fault);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("liveness — the agent execution's own terminal set", () => {
|
|
229
|
+
it.each([
|
|
230
|
+
["PENDING", AgentPhase.EXECUTION_PENDING],
|
|
231
|
+
["IN_PROGRESS", AgentPhase.EXECUTION_IN_PROGRESS],
|
|
232
|
+
["WAITING_FOR_APPROVAL", AgentPhase.EXECUTION_WAITING_FOR_APPROVAL],
|
|
233
|
+
["PAUSED", AgentPhase.EXECUTION_PAUSED],
|
|
234
|
+
])("%s is live", async (_name, phase) => {
|
|
235
|
+
const store = storeOf({ aex_1: agentRun("aex_1", phase) });
|
|
236
|
+
expect((await loadBoundExecution(store, "aex_1", NOW))?.live).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it.each([
|
|
240
|
+
["COMPLETED", AgentPhase.EXECUTION_COMPLETED],
|
|
241
|
+
["FAILED", AgentPhase.EXECUTION_FAILED],
|
|
242
|
+
["CANCELLED", AgentPhase.EXECUTION_CANCELLED],
|
|
243
|
+
["TERMINATED", AgentPhase.EXECUTION_TERMINATED],
|
|
244
|
+
])(
|
|
245
|
+
"%s with no completed_at is dead at once — no grace without a timestamp",
|
|
246
|
+
async (_name, phase) => {
|
|
247
|
+
const store = storeOf({ aex_1: agentRun("aex_1", phase) });
|
|
248
|
+
expect((await loadBoundExecution(store, "aex_1", NOW))?.live).toBe(
|
|
249
|
+
false,
|
|
250
|
+
);
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
it("a row with no status has not started, which is live", async () => {
|
|
255
|
+
const store = storeOf({
|
|
256
|
+
aex_1: create(AgentExecutionSchema, { metadata: { id: "aex_1" } }),
|
|
257
|
+
});
|
|
258
|
+
expect((await loadBoundExecution(store, "aex_1", NOW))?.live).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("liveness — the workflow execution's own terminal set", () => {
|
|
263
|
+
it.each([
|
|
264
|
+
["PENDING", WorkflowPhase.EXECUTION_PENDING],
|
|
265
|
+
["IN_PROGRESS", WorkflowPhase.EXECUTION_IN_PROGRESS],
|
|
266
|
+
["PAUSED", WorkflowPhase.EXECUTION_PAUSED],
|
|
267
|
+
])("%s is live", async (_name, phase) => {
|
|
268
|
+
const store = storeOf({ wex_1: workflowRun("wex_1", phase) });
|
|
269
|
+
expect((await loadBoundExecution(store, "wex_1", NOW))?.live).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it.each([
|
|
273
|
+
["COMPLETED", WorkflowPhase.EXECUTION_COMPLETED],
|
|
274
|
+
["FAILED", WorkflowPhase.EXECUTION_FAILED],
|
|
275
|
+
["CANCELLED", WorkflowPhase.EXECUTION_CANCELLED],
|
|
276
|
+
["TERMINATED", WorkflowPhase.EXECUTION_TERMINATED],
|
|
277
|
+
])("%s with no completed_at is dead at once", async (_name, phase) => {
|
|
278
|
+
const store = storeOf({ wex_1: workflowRun("wex_1", phase) });
|
|
279
|
+
expect((await loadBoundExecution(store, "wex_1", NOW))?.live).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("a workflow execution has no session — the field is empty, never a guess", async () => {
|
|
283
|
+
const store = storeOf({
|
|
284
|
+
wex_1: workflowRun("wex_1", WorkflowPhase.EXECUTION_IN_PROGRESS),
|
|
285
|
+
});
|
|
286
|
+
expect((await loadBoundExecution(store, "wex_1", NOW))?.sessionId).toBe(
|
|
287
|
+
"",
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("the grace after terminal, measured from completed_at", () => {
|
|
293
|
+
const G = RUN_CREDENTIAL_GRACE_AFTER_TERMINAL_MS;
|
|
294
|
+
|
|
295
|
+
it.each([
|
|
296
|
+
["the moment it ended", 0, true],
|
|
297
|
+
["one millisecond before the grace ends", -(G - 1), true],
|
|
298
|
+
["exactly at the grace boundary", -G, false],
|
|
299
|
+
["long after", -(G * 10), false],
|
|
300
|
+
])("a run that ended %s → live=%s", async (_label, offset, live) => {
|
|
301
|
+
const store = storeOf({
|
|
302
|
+
aex_1: agentRun("aex_1", AgentPhase.EXECUTION_COMPLETED, iso(offset)),
|
|
303
|
+
});
|
|
304
|
+
expect((await loadBoundExecution(store, "aex_1", NOW))?.live).toBe(live);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("a completed_at in the future (a skewed writer's clock) is inside the grace, not an error", async () => {
|
|
308
|
+
const store = storeOf({
|
|
309
|
+
aex_1: agentRun("aex_1", AgentPhase.EXECUTION_COMPLETED, iso(60_000)),
|
|
310
|
+
});
|
|
311
|
+
expect((await loadBoundExecution(store, "aex_1", NOW))?.live).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("an unparsable completed_at gets no grace — fail closed", async () => {
|
|
315
|
+
const store = storeOf({
|
|
316
|
+
aex_1: agentRun(
|
|
317
|
+
"aex_1",
|
|
318
|
+
AgentPhase.EXECUTION_COMPLETED,
|
|
319
|
+
"yesterday-ish",
|
|
320
|
+
),
|
|
321
|
+
});
|
|
322
|
+
expect((await loadBoundExecution(store, "aex_1", NOW))?.live).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("the same rule applies to a workflow execution", async () => {
|
|
326
|
+
const store = storeOf({
|
|
327
|
+
wex_1: workflowRun("wex_1", WorkflowPhase.EXECUTION_FAILED, iso(-1000)),
|
|
328
|
+
wex_2: workflowRun("wex_2", WorkflowPhase.EXECUTION_FAILED, iso(-G)),
|
|
329
|
+
});
|
|
330
|
+
expect((await loadBoundExecution(store, "wex_1", NOW))?.live).toBe(true);
|
|
331
|
+
expect((await loadBoundExecution(store, "wex_2", NOW))?.live).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|