@stigmer/runner 3.1.7 → 3.1.8
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/README.md +1 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/discover-mcp-server.js +1 -0
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +5 -2
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +58 -10
- package/dist/activities/execute-cursor/approval-state.js +37 -8
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/env-resolver.js +7 -1
- package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/hook-script.d.ts +16 -3
- package/dist/activities/execute-cursor/hook-script.js +66 -21
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +59 -0
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +52 -0
- package/dist/activities/execute-cursor/message-translator.js +107 -1
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/turn-boundary.d.ts +25 -3
- package/dist/activities/execute-cursor/turn-boundary.js +46 -8
- package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +6 -2
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-setup.d.ts +16 -2
- package/dist/activities/execute-cursor/workspace-setup.js +22 -12
- package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/environment.js +7 -1
- package/dist/activities/execute-deep-agent/environment.js.map +1 -1
- package/dist/activities/execute-deep-agent/hitl.d.ts +23 -14
- package/dist/activities/execute-deep-agent/hitl.js +42 -37
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +35 -26
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +7 -0
- package/dist/activities/execute-deep-agent/setup.js +9 -2
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +8 -1
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +69 -1
- package/dist/client/stigmer-client.js +108 -4
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/client/token-claims.d.ts +22 -0
- package/dist/client/token-claims.js +40 -0
- package/dist/client/token-claims.js.map +1 -0
- package/dist/config.d.ts +11 -1
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/middleware/approval-gate.js +9 -2
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/runner-manager.d.ts +4 -2
- package/dist/runner-manager.js +20 -7
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +2 -2
- package/dist/runner.js +1 -1
- package/dist/shared/checkpointer/factory.d.ts +11 -7
- package/dist/shared/checkpointer/factory.js +21 -8
- package/dist/shared/checkpointer/factory.js.map +1 -1
- package/dist/shared/checkpointer/sqlite-saver.d.ts +54 -0
- package/dist/shared/checkpointer/sqlite-saver.js +272 -0
- package/dist/shared/checkpointer/sqlite-saver.js.map +1 -0
- package/dist/shared/checkpointer/types.d.ts +8 -4
- package/dist/shared/tool-row.js +14 -1
- package/dist/shared/tool-row.js.map +1 -1
- package/dist/shared/workspace/platform-dir.d.ts +18 -0
- package/dist/shared/workspace/platform-dir.js +23 -0
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/package.json +3 -3
- package/src/__test-utils__/mock-client.ts +3 -0
- package/src/activities/__tests__/hydrate-workflow-execution.test.ts +2 -0
- package/src/activities/discover-mcp-server.ts +1 -0
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +5 -3
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +272 -14
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +72 -16
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +65 -0
- package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +128 -1
- package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +95 -0
- package/src/activities/execute-cursor/approval-state.ts +75 -11
- package/src/activities/execute-cursor/env-resolver.ts +8 -1
- package/src/activities/execute-cursor/hook-script.ts +66 -21
- package/src/activities/execute-cursor/index.ts +61 -0
- package/src/activities/execute-cursor/message-translator.ts +124 -1
- package/src/activities/execute-cursor/turn-boundary.ts +79 -6
- package/src/activities/execute-cursor/turn-stream.ts +6 -2
- package/src/activities/execute-cursor/workspace-setup.ts +42 -14
- package/src/activities/execute-deep-agent/__tests__/environment.test.ts +5 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +314 -0
- package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +64 -46
- package/src/activities/execute-deep-agent/environment.ts +8 -1
- package/src/activities/execute-deep-agent/hitl.ts +43 -45
- package/src/activities/execute-deep-agent/index.ts +36 -27
- package/src/activities/execute-deep-agent/setup.ts +15 -2
- package/src/activities/hydrate-workflow-execution.ts +9 -1
- package/src/client/__tests__/stigmer-client.test.ts +239 -2
- package/src/client/__tests__/token-claims.test.ts +45 -0
- package/src/client/stigmer-client.ts +152 -3
- package/src/client/token-claims.ts +42 -0
- package/src/config.ts +16 -3
- package/src/middleware/__tests__/approval-gate.test.ts +8 -1
- package/src/middleware/approval-gate.ts +9 -2
- package/src/runner-manager.ts +22 -8
- package/src/runner.ts +3 -3
- package/src/shared/__tests__/tool-row.test.ts +35 -0
- package/src/shared/checkpointer/__tests__/factory.test.ts +27 -1
- package/src/shared/checkpointer/__tests__/sqlite-saver.test.ts +212 -0
- package/src/shared/checkpointer/factory.ts +25 -8
- package/src/shared/checkpointer/sqlite-saver.ts +384 -0
- package/src/shared/checkpointer/types.ts +8 -4
- package/src/shared/tool-row.ts +14 -1
- package/src/shared/workspace/platform-dir.ts +25 -0
|
@@ -48,7 +48,8 @@ import { WorkflowQueryController } from "@stigmer/protos/ai/stigmer/agentic/work
|
|
|
48
48
|
import type { Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
49
49
|
import { WorkflowInstanceQueryController } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/query_pb";
|
|
50
50
|
import type { WorkflowInstance } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/api_pb";
|
|
51
|
-
import { PlatformQueryController } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
|
|
51
|
+
import { PlatformQueryController, GetRunnerScopedTokenInputSchema } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
|
|
52
|
+
import { isEmbeddedRunnerToken } from "./token-claims.js";
|
|
52
53
|
import { assertCreateRequirements, assertReferenceRequirements } from "./server-contracts.js";
|
|
53
54
|
|
|
54
55
|
/**
|
|
@@ -69,6 +70,28 @@ export interface RunnerBootstrapConfig {
|
|
|
69
70
|
runnerAccessTokenExpiresInSeconds?: number;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* A runner token scoped to one unit of dispatched work (issue #156).
|
|
75
|
+
*
|
|
76
|
+
* Minted on demand by the control plane when the runner exchanges its
|
|
77
|
+
* bootstrap credential at task start; presented for the ExecutionContext
|
|
78
|
+
* fetch of exactly that execution. Absent when the server does not mint
|
|
79
|
+
* (OSS, or no signing key) — the runner keeps its existing credential.
|
|
80
|
+
*/
|
|
81
|
+
export interface RunnerScopedToken {
|
|
82
|
+
token: string;
|
|
83
|
+
/** Lifetime in seconds from issuance; absent/0 when the server omitted it. */
|
|
84
|
+
expiresInSeconds?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Names the unit of dispatched work a scoped runner token should serve.
|
|
89
|
+
* Exactly one id must be set — mirrors the proto oneof.
|
|
90
|
+
*/
|
|
91
|
+
export type RunnerScopedTokenScope =
|
|
92
|
+
| { agentExecutionId: string }
|
|
93
|
+
| { workflowExecutionId: string };
|
|
94
|
+
|
|
72
95
|
/**
|
|
73
96
|
* A shared mutable token reference. When provided, the interceptor
|
|
74
97
|
* reads from this on every request, enabling token updates to
|
|
@@ -82,6 +105,13 @@ export interface StigmerClientOptions {
|
|
|
82
105
|
endpoint: string;
|
|
83
106
|
token: string | null;
|
|
84
107
|
tokenRef?: TokenRef;
|
|
108
|
+
/**
|
|
109
|
+
* Optional runner credential (a server-minted token with a runner-class
|
|
110
|
+
* `token_type` claim). When populated, ExecutionContext reads authenticate
|
|
111
|
+
* with this instead of the control-plane token — see the interceptor in the
|
|
112
|
+
* constructor for why the credential differs per service.
|
|
113
|
+
*/
|
|
114
|
+
runnerTokenRef?: TokenRef;
|
|
85
115
|
}
|
|
86
116
|
|
|
87
117
|
export class StigmerClient {
|
|
@@ -106,15 +136,49 @@ export class StigmerClient {
|
|
|
106
136
|
private readonly platformQuery: Client<typeof PlatformQueryController>;
|
|
107
137
|
|
|
108
138
|
private readonly tokenRef: TokenRef | null;
|
|
139
|
+
private readonly runnerTokenRef: TokenRef | null;
|
|
109
140
|
|
|
110
141
|
constructor(options: StigmerClientOptions) {
|
|
111
142
|
this.currentToken = options.token;
|
|
112
143
|
this.tokenRef = options.tokenRef ?? null;
|
|
144
|
+
this.runnerTokenRef = options.runnerTokenRef ?? null;
|
|
113
145
|
this.transport = createGrpcTransport({
|
|
114
146
|
baseUrl: options.endpoint,
|
|
115
147
|
interceptors: [
|
|
148
|
+
// Credential selection lives here — one tested decision point — rather
|
|
149
|
+
// than at call sites. Precedence:
|
|
150
|
+
//
|
|
151
|
+
// 1. An explicit per-call credential (an authorization header set via
|
|
152
|
+
// CallOptions) always wins. The scoped-token flow (issue #156)
|
|
153
|
+
// authenticates each ExecutionContext read with a token minted for
|
|
154
|
+
// that specific execution; concurrent sessions in one runner process
|
|
155
|
+
// make a shared mutable ref unusable for this — session A's read
|
|
156
|
+
// must never go out with session B's token.
|
|
157
|
+
//
|
|
158
|
+
// 2. The runner credential (runnerTokenRef) authenticates the services
|
|
159
|
+
// that require a runner-class token_type claim: ExecutionContext
|
|
160
|
+
// reads carry decrypted secrets on cloud, and the server gates that
|
|
161
|
+
// decrypt on runner class + scope (stigmer-cloud#152/#155); the
|
|
162
|
+
// scoped-token exchange itself requires the embedded_runner
|
|
163
|
+
// bootstrap credential (a desktop runner's control-plane token is
|
|
164
|
+
// the user's own Auth0 token, which the server correctly treats as
|
|
165
|
+
// a browsing user).
|
|
166
|
+
//
|
|
167
|
+
// 3. Everything else uses the control-plane token. Falls through
|
|
168
|
+
// unchanged when no runner token exists (OSS/local, where the
|
|
169
|
+
// server enforces no auth).
|
|
116
170
|
(next) => async (req) => {
|
|
117
|
-
|
|
171
|
+
if (req.header.has("authorization")) {
|
|
172
|
+
return next(req);
|
|
173
|
+
}
|
|
174
|
+
const usesRunnerCredential =
|
|
175
|
+
req.service.typeName === ExecutionContextQueryController.typeName ||
|
|
176
|
+
(req.service.typeName === PlatformQueryController.typeName &&
|
|
177
|
+
req.method.name === PlatformQueryController.method.getRunnerScopedToken.name);
|
|
178
|
+
const token =
|
|
179
|
+
(usesRunnerCredential ? this.runnerTokenRef?.current : null)
|
|
180
|
+
?? this.tokenRef?.current
|
|
181
|
+
?? this.currentToken;
|
|
118
182
|
if (token) {
|
|
119
183
|
req.header.set("authorization", `Bearer ${token}`);
|
|
120
184
|
}
|
|
@@ -180,12 +244,97 @@ export class StigmerClient {
|
|
|
180
244
|
return this.executionCommand.updateStatus(input);
|
|
181
245
|
}
|
|
182
246
|
|
|
183
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Fetch the ExecutionContext for an execution.
|
|
249
|
+
*
|
|
250
|
+
* When a scoped runner token is supplied (issue #156), the read
|
|
251
|
+
* authenticates with it per-call instead of the process-wide credential:
|
|
252
|
+
* on cloud the decrypt gate releases secrets only to a runner token whose
|
|
253
|
+
* scope binds it to this very execution, and one desktop runner process
|
|
254
|
+
* serves many sessions concurrently, so the credential cannot live in a
|
|
255
|
+
* shared ref.
|
|
256
|
+
*/
|
|
257
|
+
async getExecutionContextByExecutionId(
|
|
258
|
+
executionId: string,
|
|
259
|
+
scopedToken?: string,
|
|
260
|
+
): Promise<ExecutionContext> {
|
|
184
261
|
return this.executionContextQuery.getByExecutionId(
|
|
185
262
|
create(ExecutionContextExecutionIdInputSchema, { executionId }),
|
|
263
|
+
scopedToken
|
|
264
|
+
? { headers: { authorization: `Bearer ${scopedToken}` } }
|
|
265
|
+
: undefined,
|
|
186
266
|
);
|
|
187
267
|
}
|
|
188
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Exchange this runner's bootstrap credential for a token scoped to one
|
|
271
|
+
* unit of dispatched work (issue #156).
|
|
272
|
+
*
|
|
273
|
+
* The call authenticates with the runner credential (see the interceptor);
|
|
274
|
+
* the server verifies it is an embedded_runner token and that its identity
|
|
275
|
+
* can view the named execution, then mints the same session/execution-scoped
|
|
276
|
+
* sandbox token a cloud sandbox runner receives at provisioning. Returns
|
|
277
|
+
* undefined when the server does not mint (OSS, or no signing key) —
|
|
278
|
+
* presence-based, like the bootstrap token fields.
|
|
279
|
+
*/
|
|
280
|
+
async getRunnerScopedToken(
|
|
281
|
+
scope: RunnerScopedTokenScope,
|
|
282
|
+
): Promise<RunnerScopedToken | undefined> {
|
|
283
|
+
const input = create(GetRunnerScopedTokenInputSchema,
|
|
284
|
+
"agentExecutionId" in scope
|
|
285
|
+
? { scope: { case: "agentExecutionId", value: scope.agentExecutionId } }
|
|
286
|
+
: { scope: { case: "workflowExecutionId", value: scope.workflowExecutionId } });
|
|
287
|
+
const res = await this.platformQuery.getRunnerScopedToken(input);
|
|
288
|
+
if (!res.runnerScopedToken) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
token: res.runnerScopedToken,
|
|
293
|
+
expiresInSeconds: res.expiresInSeconds || undefined,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Acquire a scoped runner token for an ExecutionContext read, if this
|
|
299
|
+
* runner's credential situation calls for one.
|
|
300
|
+
*
|
|
301
|
+
* The gate is the credential itself: only an unscoped embedded_runner
|
|
302
|
+
* bootstrap token needs exchanging. A cloud sandbox runner's credential is
|
|
303
|
+
* already scoped (skip — the exchange would rightly refuse it), and an
|
|
304
|
+
* OSS/local runner holds no runner-class credential at all (skip — the
|
|
305
|
+
* server neither mints nor redacts).
|
|
306
|
+
*
|
|
307
|
+
* Failure falls back rather than failing the execution: returning undefined
|
|
308
|
+
* makes the read authenticate with the bootstrap credential, which remains
|
|
309
|
+
* decrypt-eligible until issue #156 item 3 removes it — at that point this
|
|
310
|
+
* fallback stops yielding secrets and executions surface the warning below.
|
|
311
|
+
*/
|
|
312
|
+
async acquireScopedRunnerToken(
|
|
313
|
+
scope: RunnerScopedTokenScope,
|
|
314
|
+
): Promise<string | undefined> {
|
|
315
|
+
if (!isEmbeddedRunnerToken(this.runnerTokenRef?.current)) {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const scoped = await this.getRunnerScopedToken(scope);
|
|
320
|
+
if (!scoped) {
|
|
321
|
+
console.warn(
|
|
322
|
+
"[stigmer-client] Server minted no scoped runner token; " +
|
|
323
|
+
"falling back to the bootstrap credential for the ExecutionContext read",
|
|
324
|
+
);
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
return scoped.token;
|
|
328
|
+
} catch (err) {
|
|
329
|
+
console.warn(
|
|
330
|
+
"[stigmer-client] Scoped runner token exchange failed; " +
|
|
331
|
+
"falling back to the bootstrap credential for the ExecutionContext read: " +
|
|
332
|
+
`${err instanceof Error ? err.message : err}`,
|
|
333
|
+
);
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
189
338
|
async getSession(sessionId: string): Promise<Session> {
|
|
190
339
|
return this.sessionQuery.get({ value: sessionId });
|
|
191
340
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only claim inspection for the runner's own credentials.
|
|
3
|
+
*
|
|
4
|
+
* The runner never verifies signatures — the server does that. It only needs
|
|
5
|
+
* to look at its own token's `token_type` claim to decide which credential
|
|
6
|
+
* flow applies (e.g. "do I hold an unscoped bootstrap credential that must be
|
|
7
|
+
* exchanged for a scoped one before an ExecutionContext read?"). Claim names
|
|
8
|
+
* and values mirror the server's single source of truth,
|
|
9
|
+
* `StigmerTokenType` (stigmer-cloud, api-authentication).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The `token_type` claim value of a bootstrap-minted embedded runner token. */
|
|
13
|
+
export const TOKEN_TYPE_EMBEDDED_RUNNER = "embedded_runner";
|
|
14
|
+
|
|
15
|
+
const CLAIM_TOKEN_TYPE = "token_type";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract the `token_type` claim from a JWT without verifying it.
|
|
19
|
+
*
|
|
20
|
+
* Returns undefined for null/malformed tokens and for tokens without the
|
|
21
|
+
* claim (e.g. a user's Auth0 token) — callers branch on the specific value,
|
|
22
|
+
* so "unknown" and "absent" collapse to the same answer.
|
|
23
|
+
*/
|
|
24
|
+
export function tokenTypeOf(token: string | null | undefined): string | undefined {
|
|
25
|
+
if (!token) return undefined;
|
|
26
|
+
const parts = token.split(".");
|
|
27
|
+
if (parts.length !== 3) return undefined;
|
|
28
|
+
try {
|
|
29
|
+
const payload = JSON.parse(
|
|
30
|
+
Buffer.from(parts[1]!, "base64url").toString("utf8"),
|
|
31
|
+
) as Record<string, unknown>;
|
|
32
|
+
const tokenType = payload[CLAIM_TOKEN_TYPE];
|
|
33
|
+
return typeof tokenType === "string" ? tokenType : undefined;
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether the token is a bootstrap-minted `embedded_runner` credential. */
|
|
40
|
+
export function isEmbeddedRunnerToken(token: string | null | undefined): boolean {
|
|
41
|
+
return tokenTypeOf(token) === TOKEN_TYPE_EMBEDDED_RUNNER;
|
|
42
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -73,7 +73,7 @@ export interface Config {
|
|
|
73
73
|
readonly maxConcurrentActivities: number;
|
|
74
74
|
readonly idleTimeoutSeconds: number | null;
|
|
75
75
|
readonly cloudModeEnabled: boolean;
|
|
76
|
-
readonly checkpointerType: "memory" | "http";
|
|
76
|
+
readonly checkpointerType: "memory" | "http" | "sqlite";
|
|
77
77
|
readonly checkpointerProxyEndpoint: string | null;
|
|
78
78
|
readonly primaryModel: string;
|
|
79
79
|
/**
|
|
@@ -97,6 +97,14 @@ export interface Config {
|
|
|
97
97
|
readonly workspaceLockTimeoutMs: number;
|
|
98
98
|
/** Shared mutable token reference for dynamic token updates (manager mode). */
|
|
99
99
|
readonly stigmerTokenRef?: { current: string | null };
|
|
100
|
+
/**
|
|
101
|
+
* Shared mutable reference to the runner credential (manager mode): the
|
|
102
|
+
* server-minted runner token once adopted, tracking the control-plane token
|
|
103
|
+
* in lockstep before that. StigmerClient authenticates ExecutionContext
|
|
104
|
+
* reads with it so the server's runner-class decrypt gate recognizes a
|
|
105
|
+
* desktop runner (see stigmer-client.ts).
|
|
106
|
+
*/
|
|
107
|
+
readonly stigmerRunnerTokenRef?: { current: string | null };
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
export function loadConfig(): Config {
|
|
@@ -146,8 +154,13 @@ export function loadConfig(): Config {
|
|
|
146
154
|
const idleTimeoutRaw = process.env.STIGMER_IDLE_TIMEOUT_SECONDS;
|
|
147
155
|
const idleTimeoutSeconds = idleTimeoutRaw ? parseInt(idleTimeoutRaw, 10) : null;
|
|
148
156
|
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
// Local/OSS default is the durable SQLite saver so HITL/pause/transient-recovery
|
|
158
|
+
// resume across ExecuteDeepAgent invocations (stigmer/stigmer#204); cloud stays
|
|
159
|
+
// on the proxy-backed http saver. STIGMER_CHECKPOINTER_TYPE overrides explicitly
|
|
160
|
+
// (e.g. "memory" for ephemeral test runs).
|
|
161
|
+
const checkpointerType =
|
|
162
|
+
(process.env.STIGMER_CHECKPOINTER_TYPE as "memory" | "http" | "sqlite" | undefined)
|
|
163
|
+
?? (mode === "cloud" ? "http" : "sqlite");
|
|
151
164
|
const checkpointerProxyEndpoint = process.env.STIGMER_CHECKPOINTER_PROXY_ENDPOINT
|
|
152
165
|
?? proxyEndpoint;
|
|
153
166
|
|
|
@@ -168,12 +168,19 @@ describe("ApprovalGateMiddleware", () => {
|
|
|
168
168
|
|
|
169
169
|
mockedInterrupt.mockReturnValue({ action: "reject", comment: "too dangerous" });
|
|
170
170
|
|
|
171
|
+
// Spy handler so we can prove the tool never executes on reject.
|
|
172
|
+
const handler = vi.fn(passthrough);
|
|
171
173
|
const req = makeRequest({ name: "tool_b" });
|
|
172
|
-
const result = await mw.wrapToolCall!(req,
|
|
174
|
+
const result = await mw.wrapToolCall!(req, handler);
|
|
173
175
|
|
|
174
176
|
expect(result).toBeInstanceOf(ToolMessage);
|
|
175
177
|
expect((result as ToolMessage).content).toContain("rejected");
|
|
176
178
|
expect((result as ToolMessage).content).toContain("too dangerous");
|
|
179
|
+
// The tool is denied — the handler (execution) must NOT run.
|
|
180
|
+
expect(handler).not.toHaveBeenCalled();
|
|
181
|
+
// REJECT denies one tool and continues the run; it does not terminate the
|
|
182
|
+
// execution (issue #197) — the old "Execution will be terminated" copy is gone.
|
|
183
|
+
expect((result as ToolMessage).content).not.toContain("terminated");
|
|
177
184
|
});
|
|
178
185
|
|
|
179
186
|
describe("platform tool defaults", () => {
|
|
@@ -288,9 +288,16 @@ export function createApprovalGateMiddleware(
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
if (action === "reject") {
|
|
291
|
-
|
|
291
|
+
// REJECT denies THIS tool call and continues the run (it does not
|
|
292
|
+
// terminate the execution — see APPROVAL_ACTION_REJECT in enum.proto).
|
|
293
|
+
// Feed the user's objection back so the model adapts rather than
|
|
294
|
+
// retrying. Distinct from SKIP only by the strength of the signal.
|
|
295
|
+
const comment = response.comment ?? "";
|
|
296
|
+
const rejectMessage = comment
|
|
297
|
+
? `Tool '${toolName}' was rejected by the user: ${comment}. Do not retry it; proceed by taking their objection into account.`
|
|
298
|
+
: `Tool '${toolName}' was rejected by the user. Do not retry it; proceed by taking their objection into account.`;
|
|
292
299
|
return new ToolMessage({
|
|
293
|
-
content:
|
|
300
|
+
content: rejectMessage,
|
|
294
301
|
tool_call_id: toolCall.id,
|
|
295
302
|
name: toolName,
|
|
296
303
|
});
|
package/src/runner-manager.ts
CHANGED
|
@@ -94,8 +94,8 @@ export interface RunnerManagerOptions {
|
|
|
94
94
|
/** Max wait for the per-workspace turn lock (ms). @default 900000 */
|
|
95
95
|
readonly workspaceLockTimeoutMs?: number;
|
|
96
96
|
|
|
97
|
-
/** Checkpointer type for LangGraph agent state. @default "
|
|
98
|
-
readonly checkpointerType?: "memory" | "http";
|
|
97
|
+
/** Checkpointer type for LangGraph agent state. @default "sqlite" (or "http" if proxyEndpoint is set) */
|
|
98
|
+
readonly checkpointerType?: "memory" | "http" | "sqlite";
|
|
99
99
|
|
|
100
100
|
/** Checkpointer proxy endpoint. Falls back to proxyEndpoint. */
|
|
101
101
|
readonly checkpointerProxyEndpoint?: string;
|
|
@@ -202,7 +202,16 @@ export async function createStigmerRunnerManager(
|
|
|
202
202
|
// installed) — see that module for the two-writer rationale and the staleness
|
|
203
203
|
// history it guards.
|
|
204
204
|
const tokenRef = { current: options.stigmerToken ?? null };
|
|
205
|
-
|
|
205
|
+
// `runnerTokenRef` mirrors the coordinator's proxy credential for gRPC use:
|
|
206
|
+
// the minted runner token (token_type=embedded_runner) once adopted, tracking
|
|
207
|
+
// the control-plane token in lockstep before that. StigmerClient authenticates
|
|
208
|
+
// ExecutionContext reads with it so cloud's runner-class decrypt gate
|
|
209
|
+
// (stigmer-cloud#152) recognizes the desktop runner — its control-plane token
|
|
210
|
+
// is the user's own Auth0 token, which the server treats as a browsing user
|
|
211
|
+
// and answers with redacted secrets. Pre-mint the ref equals the control-plane
|
|
212
|
+
// token, which is exactly what the client would fall back to anyway.
|
|
213
|
+
const runnerTokenRef = { current: options.stigmerToken ?? null };
|
|
214
|
+
const baseConfig = mapManagerOptionsToConfig(options, tokenRef, runnerTokenRef);
|
|
206
215
|
|
|
207
216
|
// Install the Cursor SDK interceptors BEFORE resolving Temporal coordinates.
|
|
208
217
|
// Coordinate discovery dials the control plane via StigmerClient, which loads
|
|
@@ -235,14 +244,17 @@ export async function createStigmerRunnerManager(
|
|
|
235
244
|
await assertHttp2ConnectPatched();
|
|
236
245
|
|
|
237
246
|
// The token coordinator owns the proxy credential (x-stigmer-auth) and its
|
|
238
|
-
// refresh lifecycle. It writes ONLY the interceptors
|
|
239
|
-
//
|
|
240
|
-
// expired) minted token itself, so
|
|
241
|
-
// restart.
|
|
247
|
+
// refresh lifecycle. It writes ONLY its sinks (the interceptors plus the gRPC
|
|
248
|
+
// runner-credential ref) and re-mints using the always-fresh control-plane
|
|
249
|
+
// token in `tokenRef` — never the (possibly expired) minted token itself, so
|
|
250
|
+
// a slept-past-TTL runner recovers without a restart.
|
|
242
251
|
const tokenCoordinator = createRunnerTokenCoordinator({
|
|
243
252
|
applyProxyToken: (token) => {
|
|
244
253
|
updateInterceptorToken(token);
|
|
245
254
|
updateHttp2InterceptorToken(token);
|
|
255
|
+
// Third sink: keeps the ExecutionContext-read credential in lockstep with
|
|
256
|
+
// the proxy credential across mint and refresh (see runnerTokenRef above).
|
|
257
|
+
runnerTokenRef.current = token;
|
|
246
258
|
},
|
|
247
259
|
reMint: () =>
|
|
248
260
|
refreshRunnerAccessToken({
|
|
@@ -545,6 +557,7 @@ function validateManagerOptions(options: RunnerManagerOptions): void {
|
|
|
545
557
|
export function mapManagerOptionsToConfig(
|
|
546
558
|
options: RunnerManagerOptions,
|
|
547
559
|
tokenRef?: { current: string | null },
|
|
560
|
+
runnerTokenRef?: { current: string | null },
|
|
548
561
|
): Config {
|
|
549
562
|
const proxyActive = !!options.proxyEndpoint;
|
|
550
563
|
|
|
@@ -563,6 +576,7 @@ export function mapManagerOptionsToConfig(
|
|
|
563
576
|
stigmerBackendEndpoint: normalizeEndpoint(options.stigmerEndpoint),
|
|
564
577
|
stigmerToken: options.stigmerToken ?? null,
|
|
565
578
|
stigmerTokenRef: tokenRef,
|
|
579
|
+
stigmerRunnerTokenRef: runnerTokenRef,
|
|
566
580
|
cursorApiKey: proxyActive
|
|
567
581
|
? (options.cursorApiKey ?? "proxy-managed")
|
|
568
582
|
: (options.cursorApiKey ?? ""),
|
|
@@ -573,7 +587,7 @@ export function mapManagerOptionsToConfig(
|
|
|
573
587
|
idleTimeoutSeconds: null,
|
|
574
588
|
cloudModeEnabled: options.cloudModeEnabled ?? false,
|
|
575
589
|
checkpointerType:
|
|
576
|
-
options.checkpointerType ?? (proxyActive ? "http" : "
|
|
590
|
+
options.checkpointerType ?? (proxyActive ? "http" : "sqlite"),
|
|
577
591
|
checkpointerProxyEndpoint:
|
|
578
592
|
options.checkpointerProxyEndpoint ?? options.proxyEndpoint ?? null,
|
|
579
593
|
primaryModel: options.primaryModel ?? "gpt-4.1",
|
package/src/runner.ts
CHANGED
|
@@ -72,8 +72,8 @@ export interface StigmerRunnerOptions {
|
|
|
72
72
|
/** Max wait for the per-workspace turn lock (ms). @default 900000 */
|
|
73
73
|
readonly workspaceLockTimeoutMs?: number;
|
|
74
74
|
|
|
75
|
-
/** Checkpointer type for LangGraph agent state. @default "
|
|
76
|
-
readonly checkpointerType?: "memory" | "http";
|
|
75
|
+
/** Checkpointer type for LangGraph agent state. @default "sqlite" (or "http" if proxyEndpoint is set) */
|
|
76
|
+
readonly checkpointerType?: "memory" | "http" | "sqlite";
|
|
77
77
|
|
|
78
78
|
/** Checkpointer proxy endpoint. Falls back to proxyEndpoint. */
|
|
79
79
|
readonly checkpointerProxyEndpoint?: string;
|
|
@@ -270,7 +270,7 @@ export function mapOptionsToConfig(options: StigmerRunnerOptions): Config {
|
|
|
270
270
|
idleTimeoutSeconds: null,
|
|
271
271
|
cloudModeEnabled: options.cloudModeEnabled ?? false,
|
|
272
272
|
checkpointerType: options.checkpointerType
|
|
273
|
-
?? (proxyActive ? "http" : "
|
|
273
|
+
?? (proxyActive ? "http" : "sqlite"),
|
|
274
274
|
checkpointerProxyEndpoint: options.checkpointerProxyEndpoint
|
|
275
275
|
?? options.proxyEndpoint
|
|
276
276
|
?? null,
|
|
@@ -11,6 +11,7 @@ import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/a
|
|
|
11
11
|
import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
|
|
12
12
|
import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
13
13
|
import {
|
|
14
|
+
collectSettledToolCallIds,
|
|
14
15
|
collectSubAgentToolCallIds,
|
|
15
16
|
hideToolCallRow,
|
|
16
17
|
isToolCallRowHidden,
|
|
@@ -354,3 +355,37 @@ describe("collectSubAgentToolCallIds", () => {
|
|
|
354
355
|
expect([...collectSubAgentToolCallIds([sa]).values()].sort()).toEqual(["tc-a", "tc-b"]);
|
|
355
356
|
});
|
|
356
357
|
});
|
|
358
|
+
|
|
359
|
+
describe("collectSettledToolCallIds", () => {
|
|
360
|
+
function msgWith(id: string, status: ToolCallStatus) {
|
|
361
|
+
return create(AgentMessageSchema, {
|
|
362
|
+
type: 1, // MESSAGE_AI
|
|
363
|
+
toolCalls: [create(ToolCallSchema, { id, name: "Shell", status })],
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
it("collects every terminal status and skips every live one", () => {
|
|
368
|
+
const ids = collectSettledToolCallIds([
|
|
369
|
+
msgWith("tc-completed", ToolCallStatus.TOOL_CALL_COMPLETED),
|
|
370
|
+
msgWith("tc-failed", ToolCallStatus.TOOL_CALL_FAILED),
|
|
371
|
+
msgWith("tc-skipped", ToolCallStatus.TOOL_CALL_SKIPPED),
|
|
372
|
+
msgWith("tc-pending", ToolCallStatus.TOOL_CALL_PENDING),
|
|
373
|
+
msgWith("tc-running", ToolCallStatus.TOOL_CALL_RUNNING),
|
|
374
|
+
msgWith("tc-gated", ToolCallStatus.TOOL_CALL_WAITING_APPROVAL),
|
|
375
|
+
]);
|
|
376
|
+
expect([...ids].sort()).toEqual(["tc-completed", "tc-failed", "tc-skipped"]);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("treats a server-settled INTERRUPTED row as settled (recovery provenance scoping)", () => {
|
|
380
|
+
// A FAILED-then-recovered execution seeds INTERRUPTED rows from the prior
|
|
381
|
+
// invocation. For turn-boundary provenance those are PRIOR-turn history —
|
|
382
|
+
// scoping them as this-turn would mis-attribute a dead call to the
|
|
383
|
+
// resuming turn's change set. (The Cursor monotonic guard makes the
|
|
384
|
+
// opposite call for the same status so a replayed event can still advance
|
|
385
|
+
// the row; see isTerminalToolStatus in message-translator.ts.)
|
|
386
|
+
const ids = collectSettledToolCallIds([
|
|
387
|
+
msgWith("tc-interrupted", ToolCallStatus.TOOL_CALL_INTERRUPTED),
|
|
388
|
+
]);
|
|
389
|
+
expect(ids.has("tc-interrupted")).toBe(true);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
@@ -1,14 +1,40 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import { MemorySaver } from "@langchain/langgraph-checkpoint";
|
|
3
6
|
import { createCheckpointer, CheckpointerCreationError } from "../factory.js";
|
|
4
7
|
import { HttpCheckpointSaver } from "../http-saver.js";
|
|
8
|
+
import { SqliteCheckpointSaver } from "../sqlite-saver.js";
|
|
5
9
|
|
|
6
10
|
describe("createCheckpointer", () => {
|
|
11
|
+
const tempDirs: string[] = [];
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
for (const d of tempDirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
7
16
|
it("returns MemorySaver for type=memory", async () => {
|
|
8
17
|
const saver = await createCheckpointer({ type: "memory" });
|
|
9
18
|
expect(saver).toBeInstanceOf(MemorySaver);
|
|
10
19
|
});
|
|
11
20
|
|
|
21
|
+
it("returns SqliteCheckpointSaver for type=sqlite", async () => {
|
|
22
|
+
const dir = mkdtempSync(join(tmpdir(), "stigmer-cp-factory-"));
|
|
23
|
+
tempDirs.push(dir);
|
|
24
|
+
const saver = await createCheckpointer({
|
|
25
|
+
type: "sqlite",
|
|
26
|
+
sqlitePath: join(dir, "checkpoints.db"),
|
|
27
|
+
});
|
|
28
|
+
expect(saver).toBeInstanceOf(SqliteCheckpointSaver);
|
|
29
|
+
(saver as SqliteCheckpointSaver).close();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("throws CheckpointerCreationError when sqlite is missing sqlitePath", async () => {
|
|
33
|
+
await expect(
|
|
34
|
+
createCheckpointer({ type: "sqlite" }),
|
|
35
|
+
).rejects.toThrow(CheckpointerCreationError);
|
|
36
|
+
});
|
|
37
|
+
|
|
12
38
|
it("returns HttpCheckpointSaver for type=http", async () => {
|
|
13
39
|
const saver = await createCheckpointer({
|
|
14
40
|
type: "http",
|