@gajae-code/coding-agent 0.5.4 → 0.6.1
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/CHANGELOG.md +23 -0
- package/dist/types/cli/web-search-cli.d.ts +12 -0
- package/dist/types/commands/rlm.d.ts +10 -0
- package/dist/types/commands/web-search.d.ts +54 -0
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +61 -3
- package/dist/types/edit/notebook.d.ts +3 -0
- package/dist/types/eval/py/executor.d.ts +3 -0
- package/dist/types/eval/py/kernel.d.ts +3 -1
- package/dist/types/eval/py/runtime.d.ts +9 -1
- package/dist/types/exec/bash-executor.d.ts +4 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/custom-tools/wrapper.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +1 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +6 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +14 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +3 -3
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +18 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/goals/tools/goal-tool.d.ts +2 -0
- package/dist/types/main.d.ts +11 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -2
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +12 -0
- package/dist/types/modes/components/model-selector.d.ts +5 -2
- package/dist/types/modes/components/status-line.d.ts +4 -1
- package/dist/types/modes/controllers/input-controller.d.ts +3 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/print-mode.d.ts +6 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +21 -0
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +1 -0
- package/dist/types/rlm/artifacts.d.ts +9 -0
- package/dist/types/rlm/complete-research-tool.d.ts +35 -0
- package/dist/types/rlm/data-context.d.ts +6 -0
- package/dist/types/rlm/index.d.ts +35 -0
- package/dist/types/rlm/notebook.d.ts +12 -0
- package/dist/types/rlm/preset.d.ts +23 -0
- package/dist/types/rlm/python-tool.d.ts +16 -0
- package/dist/types/rlm/report.d.ts +14 -0
- package/dist/types/rlm/types.d.ts +37 -0
- package/dist/types/sdk.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -1
- package/dist/types/tools/browser/attach.d.ts +19 -3
- package/dist/types/tools/browser/registry.d.ts +15 -0
- package/dist/types/tools/browser/render.d.ts +3 -0
- package/dist/types/tools/browser.d.ts +18 -1
- package/dist/types/tools/computer/render.d.ts +17 -0
- package/dist/types/tools/computer.d.ts +465 -0
- package/dist/types/tools/index.d.ts +24 -1
- package/dist/types/tools/job.d.ts +13 -0
- package/dist/types/tools/tool-timeouts.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +32 -2
- package/dist/types/web/search/providers/base.d.ts +22 -0
- package/dist/types/web/search/providers/xai.d.ts +64 -0
- package/dist/types/web/search/types.d.ts +11 -3
- package/package.json +7 -7
- package/src/cli/web-search-cli.ts +123 -8
- package/src/cli.ts +2 -0
- package/src/commands/rlm.ts +19 -0
- package/src/commands/web-search.ts +66 -0
- package/src/config/keybindings.ts +11 -0
- package/src/config/model-profiles.ts +11 -3
- package/src/config/model-registry.ts +55 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +67 -1
- package/src/edit/notebook.ts +6 -2
- package/src/eval/py/executor.ts +8 -1
- package/src/eval/py/kernel.ts +9 -4
- package/src/eval/py/runtime.ts +153 -32
- package/src/exec/bash-executor.ts +10 -4
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/custom-tools/wrapper.ts +2 -0
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/extensions/wrapper.ts +1 -0
- package/src/gjc-runtime/launch-tmux.ts +129 -1
- package/src/gjc-runtime/session-state-sidecar.ts +61 -1
- package/src/gjc-runtime/tmux-common.ts +26 -2
- package/src/gjc-runtime/tmux-gc.ts +40 -27
- package/src/gjc-runtime/tmux-sessions.ts +13 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +340 -18
- package/src/goals/runtime.ts +4 -3
- package/src/goals/state.ts +1 -1
- package/src/goals/tools/goal-tool.ts +16 -3
- package/src/internal-urls/docs-index.generated.ts +13 -9
- package/src/main.ts +28 -3
- package/src/modes/components/custom-editor.ts +13 -4
- package/src/modes/components/custom-model-preset-wizard.ts +293 -0
- package/src/modes/components/hook-selector.ts +1 -1
- package/src/modes/components/model-selector.ts +72 -29
- package/src/modes/components/skill-message.ts +62 -8
- package/src/modes/components/status-line.ts +13 -1
- package/src/modes/controllers/input-controller.ts +60 -11
- package/src/modes/controllers/selector-controller.ts +39 -0
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/print-mode.ts +14 -4
- package/src/modes/rpc/rpc-client.ts +250 -80
- package/src/modes/rpc/rpc-mode.ts +6 -12
- package/src/modes/rpc/rpc-socket-security.ts +103 -0
- package/src/modes/rpc/rpc-types.ts +10 -0
- package/src/modes/shared/agent-wire/command-dispatch.ts +7 -0
- package/src/modes/shared/agent-wire/command-validation.ts +1 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +9 -0
- package/src/modes/utils/hotkeys-markdown.ts +4 -2
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/prompts/goals/goal-continuation.md +1 -0
- package/src/prompts/goals/goal-mode-active.md +1 -0
- package/src/prompts/system/rlm-report-command.md +1 -0
- package/src/prompts/system/rlm-research.md +23 -0
- package/src/prompts/tools/bash.md +23 -2
- package/src/prompts/tools/browser.md +7 -3
- package/src/prompts/tools/computer.md +74 -0
- package/src/prompts/tools/goal.md +3 -0
- package/src/prompts/tools/job.md +9 -1
- package/src/prompts/tools/web-search.md +7 -0
- package/src/rlm/artifacts.ts +60 -0
- package/src/rlm/complete-research-tool.ts +163 -0
- package/src/rlm/data-context.ts +26 -0
- package/src/rlm/index.ts +339 -0
- package/src/rlm/notebook.ts +108 -0
- package/src/rlm/preset.ts +76 -0
- package/src/rlm/python-tool.ts +68 -0
- package/src/rlm/report.ts +70 -0
- package/src/rlm/types.ts +40 -0
- package/src/sdk.ts +12 -0
- package/src/session/agent-session.ts +48 -3
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/tools/bash-allowed-prefixes.ts +84 -1
- package/src/tools/bash.ts +80 -13
- package/src/tools/browser/attach.ts +103 -3
- package/src/tools/browser/registry.ts +176 -2
- package/src/tools/browser/render.ts +9 -1
- package/src/tools/browser.ts +33 -0
- package/src/tools/computer/render.ts +78 -0
- package/src/tools/computer.ts +640 -0
- package/src/tools/index.ts +41 -1
- package/src/tools/job.ts +88 -5
- package/src/tools/json-tree.ts +42 -29
- package/src/tools/renderers.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/web/search/index.ts +27 -2
- package/src/web/search/provider.ts +16 -1
- package/src/web/search/providers/base.ts +22 -0
- package/src/web/search/providers/xai.ts +511 -0
- package/src/web/search/render.ts +7 -0
- package/src/web/search/types.ts +11 -1
|
@@ -35,6 +35,14 @@ type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : n
|
|
|
35
35
|
type RpcCommandBody = DistributiveOmit<RpcCommand, "id">;
|
|
36
36
|
|
|
37
37
|
export interface RpcClientOptions {
|
|
38
|
+
/** Dial an existing Unix-domain socket instead of spawning a stdio child. */
|
|
39
|
+
socketPath?: string;
|
|
40
|
+
/** Explicit transport selector; defaults to uds when socketPath is set, otherwise stdio. */
|
|
41
|
+
transport?: "stdio" | "uds";
|
|
42
|
+
/** Observe transport close/error state. */
|
|
43
|
+
onTransportClose?: (error?: Error) => void;
|
|
44
|
+
/** Alias for transport close/error observation. */
|
|
45
|
+
onTransportError?: (error: Error) => void;
|
|
38
46
|
/** Path to the CLI entry point (default: searches for dist/cli.js) */
|
|
39
47
|
cliPath?: string;
|
|
40
48
|
/** Working directory for the agent */
|
|
@@ -176,62 +184,43 @@ function normalizeToolResult<TDetails>(result: RpcClientToolResult<TDetails>): A
|
|
|
176
184
|
// RPC Client
|
|
177
185
|
// ============================================================================
|
|
178
186
|
|
|
179
|
-
export
|
|
187
|
+
export interface RpcTransport {
|
|
188
|
+
readonly kind: "stdio" | "uds";
|
|
189
|
+
start(onFrame: (frame: unknown) => void, onClose: (error?: Error) => void): Promise<void>;
|
|
190
|
+
write(frame: unknown): void;
|
|
191
|
+
stop(): void;
|
|
192
|
+
getStderr(): string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
class StdioRpcTransport implements RpcTransport {
|
|
196
|
+
readonly kind = "stdio" as const;
|
|
180
197
|
#process: ptree.ChildProcess | null = null;
|
|
181
|
-
#eventListeners: RpcEventListener[] = [];
|
|
182
|
-
#pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =
|
|
183
|
-
new Map();
|
|
184
|
-
#customTools: RpcClientCustomTool[] = [];
|
|
185
|
-
#pendingHostToolCalls = new Map<string, { controller: AbortController }>();
|
|
186
|
-
#requestId = 0;
|
|
187
|
-
#extensionUiListeners: Set<(req: RpcExtensionUIRequest) => void> = new Set();
|
|
188
|
-
#workflowGateListeners: Set<(gate: RpcWorkflowGate) => void> = new Set();
|
|
189
198
|
#abortController = new AbortController();
|
|
199
|
+
#startupStderrPromise: Promise<string> = Promise.resolve("");
|
|
190
200
|
|
|
191
|
-
constructor(private options: RpcClientOptions
|
|
192
|
-
this.#customTools = [...(options.customTools ?? [])];
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Start the RPC agent process.
|
|
197
|
-
*/
|
|
198
|
-
async start(): Promise<void> {
|
|
199
|
-
if (this.#process) {
|
|
200
|
-
throw new Error("Client already started");
|
|
201
|
-
}
|
|
201
|
+
constructor(private readonly options: RpcClientOptions) {}
|
|
202
202
|
|
|
203
|
+
async start(onFrame: (frame: unknown) => void, onClose: (error?: Error) => void): Promise<void> {
|
|
204
|
+
if (this.#process) throw new Error("Transport already started");
|
|
205
|
+
this.#abortController = new AbortController();
|
|
203
206
|
const cliPath = this.options.cliPath ?? "dist/cli.js";
|
|
204
207
|
const args = ["--mode", "rpc"];
|
|
205
|
-
|
|
206
|
-
if (this.options.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (this.options.model) {
|
|
210
|
-
args.push("--model", this.options.model);
|
|
211
|
-
}
|
|
212
|
-
if (this.options.sessionDir) {
|
|
213
|
-
args.push("--session-dir", this.options.sessionDir);
|
|
214
|
-
}
|
|
215
|
-
if (this.options.args) {
|
|
216
|
-
args.push(...this.options.args);
|
|
217
|
-
}
|
|
218
|
-
|
|
208
|
+
if (this.options.provider) args.push("--provider", this.options.provider);
|
|
209
|
+
if (this.options.model) args.push("--model", this.options.model);
|
|
210
|
+
if (this.options.sessionDir) args.push("--session-dir", this.options.sessionDir);
|
|
211
|
+
if (this.options.args) args.push(...this.options.args);
|
|
219
212
|
this.#process = ptree.spawn(["bun", cliPath, ...args], {
|
|
220
213
|
cwd: this.options.cwd,
|
|
221
214
|
env: { ...Bun.env, ...this.options.env },
|
|
222
215
|
stdin: "pipe",
|
|
223
216
|
stderr: "full",
|
|
224
217
|
});
|
|
225
|
-
|
|
218
|
+
this.#startupStderrPromise = this.#process.stderr
|
|
226
219
|
? new Response(this.#process.stderr).text().catch(() => "")
|
|
227
220
|
: Promise.resolve("");
|
|
228
|
-
const getStartupStderr = async () => this.#process?.peekStderr() || (await startupStderrPromise);
|
|
229
|
-
|
|
230
|
-
// Wait for the "ready" signal or process exit
|
|
221
|
+
const getStartupStderr = async () => this.#process?.peekStderr() || (await this.#startupStderrPromise);
|
|
231
222
|
const { promise: readyPromise, resolve: readyResolve, reject: readyReject } = Promise.withResolvers<void>();
|
|
232
223
|
let readySettled = false;
|
|
233
|
-
|
|
234
|
-
// Process lines in background, intercepting the ready signal
|
|
235
224
|
const lines = readJsonl(this.#process.stdout, this.#abortController.signal);
|
|
236
225
|
void (async () => {
|
|
237
226
|
for await (const line of lines) {
|
|
@@ -240,10 +229,8 @@ export class RpcClient {
|
|
|
240
229
|
readyResolve();
|
|
241
230
|
continue;
|
|
242
231
|
}
|
|
243
|
-
|
|
232
|
+
onFrame(line);
|
|
244
233
|
}
|
|
245
|
-
// Stream ended without ready signal — process exited. Wait for the
|
|
246
|
-
// managed process wrapper so stderr is fully drained before reporting.
|
|
247
234
|
if (!readySettled) {
|
|
248
235
|
const proc = this.#process;
|
|
249
236
|
const exitCode = proc ? await proc.exited.catch(() => proc.exitCode ?? -1) : undefined;
|
|
@@ -256,40 +243,179 @@ export class RpcClient {
|
|
|
256
243
|
),
|
|
257
244
|
);
|
|
258
245
|
}
|
|
246
|
+
} else {
|
|
247
|
+
onClose(new Error("RPC stdio transport closed"));
|
|
259
248
|
}
|
|
260
249
|
})().catch((err: Error) => {
|
|
261
250
|
if (!readySettled) {
|
|
262
251
|
readySettled = true;
|
|
263
252
|
readyReject(err);
|
|
264
|
-
}
|
|
253
|
+
} else onClose(err);
|
|
265
254
|
});
|
|
266
|
-
|
|
267
|
-
// Also race against process exit (in case stdout closes before we read it)
|
|
268
255
|
void this.#process.exited.then(async (exitCode: number) => {
|
|
269
256
|
if (!readySettled) {
|
|
270
257
|
const stderr = await getStartupStderr();
|
|
271
258
|
if (readySettled) return;
|
|
272
259
|
readySettled = true;
|
|
273
260
|
readyReject(new Error(`Agent process exited with code ${exitCode}. Stderr: ${stderr}`));
|
|
274
|
-
}
|
|
261
|
+
} else onClose(new Error(`RPC stdio transport exited with code ${exitCode}`));
|
|
275
262
|
});
|
|
276
|
-
|
|
277
|
-
// Timeout to prevent hanging forever
|
|
278
|
-
const readyTimeout = this.#startTimeout(30000, () => {
|
|
263
|
+
const readyTimeout = setTimeout(() => {
|
|
279
264
|
if (readySettled) return;
|
|
280
265
|
readySettled = true;
|
|
281
|
-
void getStartupStderr().then(stderr =>
|
|
282
|
-
readyReject(new Error(`Timeout waiting for agent to become ready. Stderr: ${stderr}`))
|
|
283
|
-
|
|
284
|
-
});
|
|
266
|
+
void getStartupStderr().then(stderr =>
|
|
267
|
+
readyReject(new Error(`Timeout waiting for agent to become ready. Stderr: ${stderr}`)),
|
|
268
|
+
);
|
|
269
|
+
}, 30_000);
|
|
270
|
+
readyTimeout.unref();
|
|
271
|
+
try {
|
|
272
|
+
await readyPromise;
|
|
273
|
+
} finally {
|
|
274
|
+
clearTimeout(readyTimeout);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
write(frame: unknown): void {
|
|
279
|
+
if (!this.#process?.stdin) throw new Error("Client not started");
|
|
280
|
+
const stdin = this.#process.stdin as import("bun").FileSink;
|
|
281
|
+
stdin.write(`${JSON.stringify(frame)}\n`);
|
|
282
|
+
const flushResult = stdin.flush();
|
|
283
|
+
if (flushResult instanceof Promise) void flushResult;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
stop(): void {
|
|
287
|
+
if (!this.#process) return;
|
|
288
|
+
this.#process.kill();
|
|
289
|
+
this.#abortController.abort();
|
|
290
|
+
this.#process = null;
|
|
291
|
+
}
|
|
285
292
|
|
|
293
|
+
getStderr(): string {
|
|
294
|
+
return this.#process?.peekStderr() ?? "";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
class UdsRpcTransport implements RpcTransport {
|
|
299
|
+
readonly kind = "uds" as const;
|
|
300
|
+
#socket: import("bun").Socket | null = null;
|
|
301
|
+
#buf = "";
|
|
302
|
+
#decoder = new TextDecoder("utf-8", { fatal: false });
|
|
303
|
+
constructor(private readonly socketPath: string) {}
|
|
304
|
+
|
|
305
|
+
async start(onFrame: (frame: unknown) => void, onClose: (error?: Error) => void): Promise<void> {
|
|
306
|
+
const { promise: readyPromise, resolve: readyResolve, reject: readyReject } = Promise.withResolvers<void>();
|
|
307
|
+
let readySettled = false;
|
|
308
|
+
this.#socket = await Bun.connect({
|
|
309
|
+
unix: this.socketPath,
|
|
310
|
+
socket: {
|
|
311
|
+
data: (_socket, data) => {
|
|
312
|
+
this.#buf += this.#decoder.decode(data);
|
|
313
|
+
while (true) {
|
|
314
|
+
const nl = this.#buf.indexOf("\n");
|
|
315
|
+
if (nl < 0) break;
|
|
316
|
+
const text = this.#buf.slice(0, nl).trim();
|
|
317
|
+
this.#buf = this.#buf.slice(nl + 1);
|
|
318
|
+
if (!text) continue;
|
|
319
|
+
let frame: unknown;
|
|
320
|
+
try {
|
|
321
|
+
frame = JSON.parse(text);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
onClose(err instanceof Error ? err : new Error(String(err)));
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (!readySettled && isRecord(frame) && frame.type === "ready") {
|
|
327
|
+
readySettled = true;
|
|
328
|
+
readyResolve();
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
onFrame(frame);
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
close: () => {
|
|
335
|
+
if (!readySettled) {
|
|
336
|
+
readySettled = true;
|
|
337
|
+
readyReject(new Error(`RPC UDS transport closed before ready: ${this.socketPath}`));
|
|
338
|
+
} else onClose(new Error(`RPC UDS transport closed: ${this.socketPath}`));
|
|
339
|
+
},
|
|
340
|
+
error: (_socket, error) => {
|
|
341
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
342
|
+
if (!readySettled) {
|
|
343
|
+
readySettled = true;
|
|
344
|
+
readyReject(err);
|
|
345
|
+
} else onClose(err);
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
const readyTimeout = setTimeout(() => {
|
|
350
|
+
if (readySettled) return;
|
|
351
|
+
readySettled = true;
|
|
352
|
+
readyReject(new Error(`Timeout waiting for RPC UDS ready frame: ${this.socketPath}`));
|
|
353
|
+
}, 30_000);
|
|
354
|
+
readyTimeout.unref();
|
|
286
355
|
try {
|
|
287
356
|
await readyPromise;
|
|
357
|
+
} finally {
|
|
358
|
+
clearTimeout(readyTimeout);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
write(frame: unknown): void {
|
|
363
|
+
if (!this.#socket) throw new Error("Client not started");
|
|
364
|
+
this.#socket.write(`${JSON.stringify(frame)}\n`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
stop(): void {
|
|
368
|
+
this.#socket?.end();
|
|
369
|
+
this.#socket = null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
getStderr(): string {
|
|
373
|
+
return "";
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export class RpcClient {
|
|
378
|
+
#transport: RpcTransport | null = null;
|
|
379
|
+
#transportClosed = false;
|
|
380
|
+
#eventListeners: RpcEventListener[] = [];
|
|
381
|
+
#pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =
|
|
382
|
+
new Map();
|
|
383
|
+
#customTools: RpcClientCustomTool[] = [];
|
|
384
|
+
#pendingHostToolCalls = new Map<string, { controller: AbortController }>();
|
|
385
|
+
#requestId = 0;
|
|
386
|
+
#extensionUiListeners: Set<(req: RpcExtensionUIRequest) => void> = new Set();
|
|
387
|
+
#workflowGateListeners: Set<(gate: RpcWorkflowGate) => void> = new Set();
|
|
388
|
+
|
|
389
|
+
constructor(private options: RpcClientOptions = {}) {
|
|
390
|
+
this.#customTools = [...(options.customTools ?? [])];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Start the RPC agent process.
|
|
395
|
+
*/
|
|
396
|
+
async start(): Promise<void> {
|
|
397
|
+
if (this.#transport) {
|
|
398
|
+
throw new Error("Client already started");
|
|
399
|
+
}
|
|
400
|
+
this.#transportClosed = false;
|
|
401
|
+
const transportKind = this.options.transport ?? (this.options.socketPath ? "uds" : "stdio");
|
|
402
|
+
if (transportKind === "uds") {
|
|
403
|
+
if (!this.options.socketPath) throw new Error("socketPath is required for uds transport");
|
|
404
|
+
this.#transport = new UdsRpcTransport(this.options.socketPath);
|
|
405
|
+
} else {
|
|
406
|
+
this.#transport = new StdioRpcTransport(this.options);
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
await this.#transport.start(
|
|
410
|
+
line => this.#handleLine(line),
|
|
411
|
+
error => this.#handleTransportClose(error),
|
|
412
|
+
);
|
|
288
413
|
if (this.#customTools.length > 0) {
|
|
289
414
|
await this.setCustomTools(this.#customTools);
|
|
290
415
|
}
|
|
291
|
-
}
|
|
292
|
-
|
|
416
|
+
} catch (error) {
|
|
417
|
+
this.#transport = null;
|
|
418
|
+
throw error;
|
|
293
419
|
}
|
|
294
420
|
}
|
|
295
421
|
|
|
@@ -297,16 +423,11 @@ export class RpcClient {
|
|
|
297
423
|
* Stop the RPC agent process.
|
|
298
424
|
*/
|
|
299
425
|
stop() {
|
|
300
|
-
if (!this.#
|
|
426
|
+
if (!this.#transport) return;
|
|
301
427
|
|
|
302
|
-
this.#
|
|
303
|
-
this.#
|
|
304
|
-
this.#
|
|
305
|
-
this.#pendingRequests.clear();
|
|
306
|
-
for (const pendingCall of this.#pendingHostToolCalls.values()) {
|
|
307
|
-
pendingCall.controller.abort();
|
|
308
|
-
}
|
|
309
|
-
this.#pendingHostToolCalls.clear();
|
|
428
|
+
this.#transport.stop();
|
|
429
|
+
this.#transport = null;
|
|
430
|
+
this.#rejectAllPending(new Error("RPC client stopped"));
|
|
310
431
|
}
|
|
311
432
|
|
|
312
433
|
/**
|
|
@@ -367,6 +488,29 @@ export class RpcClient {
|
|
|
367
488
|
};
|
|
368
489
|
}
|
|
369
490
|
|
|
491
|
+
/** Respond to an extension UI request over the live transport. */
|
|
492
|
+
respondExtensionUi(response: import("./rpc-types").RpcExtensionUIResponse): void {
|
|
493
|
+
this.#writeFrame(response);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Observe transport close/error notifications. */
|
|
497
|
+
onTransportError(listener: (error: Error) => void): () => void {
|
|
498
|
+
const previousClose = this.options.onTransportClose;
|
|
499
|
+
const previousError = this.options.onTransportError;
|
|
500
|
+
this.options.onTransportClose = error => {
|
|
501
|
+
previousClose?.(error);
|
|
502
|
+
listener(error ?? new Error("RPC transport closed"));
|
|
503
|
+
};
|
|
504
|
+
this.options.onTransportError = error => {
|
|
505
|
+
previousError?.(error);
|
|
506
|
+
listener(error);
|
|
507
|
+
};
|
|
508
|
+
return () => {
|
|
509
|
+
this.options.onTransportClose = previousClose;
|
|
510
|
+
this.options.onTransportError = previousError;
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
370
514
|
/**
|
|
371
515
|
* Enter unattended mode by declaring budget + scopes + action allowlist.
|
|
372
516
|
* Returns the accepted declaration, or rejects (fail-closed) on refusal.
|
|
@@ -380,7 +524,7 @@ export class RpcClient {
|
|
|
380
524
|
* Get collected stderr output (useful for debugging).
|
|
381
525
|
*/
|
|
382
526
|
getStderr(): string {
|
|
383
|
-
return this.#
|
|
527
|
+
return this.#transport?.getStderr() ?? "";
|
|
384
528
|
}
|
|
385
529
|
|
|
386
530
|
#startTimeout(timeoutMs: number, onTimeout: () => void): NodeJS.Timeout {
|
|
@@ -448,6 +592,12 @@ export class RpcClient {
|
|
|
448
592
|
return this.#getData(response);
|
|
449
593
|
}
|
|
450
594
|
|
|
595
|
+
/** Return unresolved workflow gates persisted by the RPC session. */
|
|
596
|
+
async getPendingWorkflowGates(): Promise<RpcWorkflowGate[]> {
|
|
597
|
+
const response = await this.#send({ type: "get_pending_workflow_gates" });
|
|
598
|
+
return this.#getData<{ gates: RpcWorkflowGate[] }>(response).gates;
|
|
599
|
+
}
|
|
600
|
+
|
|
451
601
|
/**
|
|
452
602
|
* Set model by provider and ID.
|
|
453
603
|
*/
|
|
@@ -658,7 +808,7 @@ export class RpcClient {
|
|
|
658
808
|
*/
|
|
659
809
|
async setCustomTools(tools: RpcClientCustomTool[]): Promise<string[]> {
|
|
660
810
|
this.#customTools = [...tools];
|
|
661
|
-
if (!this.#
|
|
811
|
+
if (!this.#transport) {
|
|
662
812
|
return this.#customTools.map(tool => tool.name);
|
|
663
813
|
}
|
|
664
814
|
const definitions: RpcHostToolDefinition[] = this.#customTools.map(tool => ({
|
|
@@ -696,7 +846,7 @@ export class RpcClient {
|
|
|
696
846
|
if (settled) return;
|
|
697
847
|
settled = true;
|
|
698
848
|
unsubscribe();
|
|
699
|
-
reject(new Error(`Timeout waiting for agent to become idle. Stderr: ${this.#
|
|
849
|
+
reject(new Error(`Timeout waiting for agent to become idle. Stderr: ${this.#transport?.getStderr() ?? ""}`));
|
|
700
850
|
});
|
|
701
851
|
return promise;
|
|
702
852
|
}
|
|
@@ -722,7 +872,7 @@ export class RpcClient {
|
|
|
722
872
|
if (settled) return;
|
|
723
873
|
settled = true;
|
|
724
874
|
unsubscribe();
|
|
725
|
-
reject(new Error(`Timeout collecting events. Stderr: ${this.#
|
|
875
|
+
reject(new Error(`Timeout collecting events. Stderr: ${this.#transport?.getStderr() ?? ""}`));
|
|
726
876
|
});
|
|
727
877
|
return promise;
|
|
728
878
|
}
|
|
@@ -786,7 +936,7 @@ export class RpcClient {
|
|
|
786
936
|
}
|
|
787
937
|
|
|
788
938
|
#send(command: RpcCommandBody, timeoutMs = 30_000): Promise<RpcResponse> {
|
|
789
|
-
if (!this.#
|
|
939
|
+
if (!this.#transport || this.#transportClosed) {
|
|
790
940
|
throw new Error("Client not started");
|
|
791
941
|
}
|
|
792
942
|
|
|
@@ -799,7 +949,7 @@ export class RpcClient {
|
|
|
799
949
|
this.#pendingRequests.delete(id);
|
|
800
950
|
settled = true;
|
|
801
951
|
reject(
|
|
802
|
-
new Error(`Timeout waiting for response to ${command.type}. Stderr: ${this.#
|
|
952
|
+
new Error(`Timeout waiting for response to ${command.type}. Stderr: ${this.#transport?.getStderr() ?? ""}`),
|
|
803
953
|
);
|
|
804
954
|
});
|
|
805
955
|
|
|
@@ -884,22 +1034,42 @@ export class RpcClient {
|
|
|
884
1034
|
}
|
|
885
1035
|
|
|
886
1036
|
#writeFrame(
|
|
887
|
-
frame:
|
|
1037
|
+
frame:
|
|
1038
|
+
| RpcCommand
|
|
1039
|
+
| RpcWorkflowGateResponse
|
|
1040
|
+
| RpcHostToolResult
|
|
1041
|
+
| RpcHostToolUpdate
|
|
1042
|
+
| import("./rpc-types").RpcExtensionUIResponse,
|
|
888
1043
|
onError?: (error: Error) => void,
|
|
889
1044
|
): void {
|
|
890
|
-
if (!this.#
|
|
1045
|
+
if (!this.#transport || this.#transportClosed) {
|
|
891
1046
|
throw new Error("Client not started");
|
|
892
1047
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
});
|
|
1048
|
+
try {
|
|
1049
|
+
this.#transport.write(frame);
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1052
|
+
onError?.(error);
|
|
1053
|
+
this.#handleTransportClose(error);
|
|
900
1054
|
}
|
|
901
1055
|
}
|
|
902
1056
|
|
|
1057
|
+
#handleTransportClose(error?: Error): void {
|
|
1058
|
+
if (this.#transportClosed) return;
|
|
1059
|
+
this.#transportClosed = true;
|
|
1060
|
+
const closeError = error ?? new Error("RPC transport closed");
|
|
1061
|
+
this.options.onTransportClose?.(closeError);
|
|
1062
|
+
this.options.onTransportError?.(closeError);
|
|
1063
|
+
this.#rejectAllPending(closeError);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
#rejectAllPending(error: Error): void {
|
|
1067
|
+
for (const pending of this.#pendingRequests.values()) pending.reject(error);
|
|
1068
|
+
this.#pendingRequests.clear();
|
|
1069
|
+
for (const pendingCall of this.#pendingHostToolCalls.values()) pendingCall.controller.abort();
|
|
1070
|
+
this.#pendingHostToolCalls.clear();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
903
1073
|
#getData<T>(response: RpcResponse): T {
|
|
904
1074
|
if (!response.success) {
|
|
905
1075
|
const errorResponse = response as Extract<RpcResponse, { success: false }>;
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import * as fs from "node:fs/promises";
|
|
15
14
|
import * as path from "node:path";
|
|
16
15
|
import { $pickenv, logger, readLines, Snowflake } from "@gajae-code/utils";
|
|
17
16
|
import type {
|
|
@@ -31,6 +30,7 @@ import { modelSupportsTokenCostMetrics, UnattendedSessionControlPlane } from "..
|
|
|
31
30
|
import { FileGateStore } from "../shared/agent-wire/workflow-gate-broker";
|
|
32
31
|
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
|
33
32
|
import { isRpcHostUriResult, RpcHostUriBridge } from "./host-uris";
|
|
33
|
+
import { prepareRpcSocketPath, verifyRpcSocketAfterListen } from "./rpc-socket-security";
|
|
34
34
|
import type {
|
|
35
35
|
RpcCommand,
|
|
36
36
|
RpcExtensionUIRequest,
|
|
@@ -128,6 +128,7 @@ export const RPC_SAFE_READ_CONTROL_COMMANDS: ReadonlySet<RpcCommand["type"]> = n
|
|
|
128
128
|
"get_last_assistant_text",
|
|
129
129
|
"get_messages",
|
|
130
130
|
"get_login_providers",
|
|
131
|
+
"get_pending_workflow_gates",
|
|
131
132
|
]);
|
|
132
133
|
|
|
133
134
|
/** True when a command may bypass the ordered serial chain and run immediately. */
|
|
@@ -725,16 +726,7 @@ export async function runRpcMode(
|
|
|
725
726
|
// get_state/get_messages on reconnect).
|
|
726
727
|
if (options?.listen) {
|
|
727
728
|
const socketPath = options.listen;
|
|
728
|
-
await
|
|
729
|
-
// Refuse to clobber a live previous owner: probe the path first and only
|
|
730
|
-
// unlink a stale endpoint. A second `--listen` on the same path must not
|
|
731
|
-
// remove the socket another running server is still serving (#606).
|
|
732
|
-
// Unexpected probe failures are treated as alive, so this also refuses
|
|
733
|
-
// rather than unlinking a socket path we could not safely classify.
|
|
734
|
-
if (await isUnixSocketAlive(socketPath)) {
|
|
735
|
-
throw new RpcListenRefusedError(socketPath);
|
|
736
|
-
}
|
|
737
|
-
await fs.rm(socketPath, { force: true }).catch(() => {});
|
|
729
|
+
await prepareRpcSocketPath(socketPath);
|
|
738
730
|
await registerRpcSession({
|
|
739
731
|
sessionId: session.sessionId,
|
|
740
732
|
pid: process.pid,
|
|
@@ -748,7 +740,7 @@ export async function runRpcMode(
|
|
|
748
740
|
const noopSink = (_line: string): void => {};
|
|
749
741
|
let currentSocket: object | undefined;
|
|
750
742
|
let buf = "";
|
|
751
|
-
Bun.listen({
|
|
743
|
+
const server = Bun.listen({
|
|
752
744
|
unix: socketPath,
|
|
753
745
|
socket: {
|
|
754
746
|
open(socket) {
|
|
@@ -779,6 +771,8 @@ export async function runRpcMode(
|
|
|
779
771
|
error() {},
|
|
780
772
|
},
|
|
781
773
|
});
|
|
774
|
+
await verifyRpcSocketAfterListen(socketPath);
|
|
775
|
+
void server;
|
|
782
776
|
|
|
783
777
|
const onSignal = (): void => {
|
|
784
778
|
void shutdown(0, "RPC socket server signal");
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
export class RpcSocketSecurityError extends Error {
|
|
6
|
+
constructor(message: string) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "RpcSocketSecurityError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const unsafeBits = 0o077;
|
|
13
|
+
|
|
14
|
+
function currentUid(): number | undefined {
|
|
15
|
+
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function assertOwned(stat: { uid: number }, target: string): void {
|
|
19
|
+
const uid = currentUid();
|
|
20
|
+
if (uid !== undefined && stat.uid !== uid) {
|
|
21
|
+
throw new RpcSocketSecurityError(`${target} is owned by uid ${stat.uid}, expected ${uid}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertPrivateMode(mode: number, target: string): void {
|
|
26
|
+
if ((mode & unsafeBits) !== 0) throw new RpcSocketSecurityError(`${target} has group/other permissions`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function prepareRpcSocketPath(socketPath: string): Promise<void> {
|
|
30
|
+
const parent = path.dirname(socketPath);
|
|
31
|
+
let parentStat: import("node:fs").Stats;
|
|
32
|
+
try {
|
|
33
|
+
parentStat = await fs.lstat(parent);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
36
|
+
await fs.mkdir(parent, { recursive: true, mode: 0o700 });
|
|
37
|
+
parentStat = await fs.lstat(parent);
|
|
38
|
+
}
|
|
39
|
+
if (parentStat.isSymbolicLink()) throw new RpcSocketSecurityError(`RPC socket parent is a symlink: ${parent}`);
|
|
40
|
+
if (!parentStat.isDirectory()) throw new RpcSocketSecurityError(`RPC socket parent is not a directory: ${parent}`);
|
|
41
|
+
assertOwned(parentStat, parent);
|
|
42
|
+
assertPrivateMode(parentStat.mode, parent);
|
|
43
|
+
|
|
44
|
+
let existing: import("node:fs").Stats;
|
|
45
|
+
try {
|
|
46
|
+
existing = await fs.lstat(socketPath);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
if (existing.isSymbolicLink()) throw new RpcSocketSecurityError(`RPC socket path is a symlink: ${socketPath}`);
|
|
52
|
+
assertOwned(existing, socketPath);
|
|
53
|
+
if (!existing.isSocket()) throw new RpcSocketSecurityError(`RPC socket path is not a socket: ${socketPath}`);
|
|
54
|
+
assertPrivateMode(existing.mode, socketPath);
|
|
55
|
+
if (await probeUnixSocketAlive(socketPath)) {
|
|
56
|
+
throw new RpcSocketSecurityError(`RPC socket path is live: ${socketPath}`);
|
|
57
|
+
}
|
|
58
|
+
await fs.unlink(socketPath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function assertSafeClientSocket(socketPath: string): Promise<void> {
|
|
62
|
+
const parent = path.dirname(socketPath);
|
|
63
|
+
const parentStat = await fs.lstat(parent);
|
|
64
|
+
if (parentStat.isSymbolicLink()) throw new RpcSocketSecurityError(`RPC socket parent is a symlink: ${parent}`);
|
|
65
|
+
if (!parentStat.isDirectory()) throw new RpcSocketSecurityError(`RPC socket parent is not a directory: ${parent}`);
|
|
66
|
+
assertOwned(parentStat, parent);
|
|
67
|
+
assertPrivateMode(parentStat.mode, parent);
|
|
68
|
+
|
|
69
|
+
const socketStat = await fs.lstat(socketPath);
|
|
70
|
+
if (socketStat.isSymbolicLink()) throw new RpcSocketSecurityError(`RPC socket path is a symlink: ${socketPath}`);
|
|
71
|
+
assertOwned(socketStat, socketPath);
|
|
72
|
+
if (!socketStat.isSocket()) throw new RpcSocketSecurityError(`RPC socket path is not a socket: ${socketPath}`);
|
|
73
|
+
assertPrivateMode(socketStat.mode, socketPath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function verifyRpcSocketAfterListen(socketPath: string): Promise<void> {
|
|
77
|
+
await fs.chmod(socketPath, 0o600);
|
|
78
|
+
const st = await fs.lstat(socketPath);
|
|
79
|
+
if (st.isSymbolicLink()) throw new RpcSocketSecurityError(`RPC socket path became a symlink: ${socketPath}`);
|
|
80
|
+
if (!st.isSocket()) throw new RpcSocketSecurityError(`RPC socket path is not a socket after listen: ${socketPath}`);
|
|
81
|
+
assertOwned(st, socketPath);
|
|
82
|
+
assertPrivateMode(st.mode, socketPath);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function probeUnixSocketAlive(socketPath: string): Promise<boolean> {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const socket = net.createConnection({ path: socketPath });
|
|
88
|
+
let settled = false;
|
|
89
|
+
const settle = (value: boolean) => {
|
|
90
|
+
if (settled) return;
|
|
91
|
+
settled = true;
|
|
92
|
+
socket.destroy();
|
|
93
|
+
resolve(value);
|
|
94
|
+
};
|
|
95
|
+
socket.once("connect", () => settle(true));
|
|
96
|
+
socket.once("error", err => {
|
|
97
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
98
|
+
if (code === "ENOENT" || code === "ECONNREFUSED") settle(false);
|
|
99
|
+
else reject(err);
|
|
100
|
+
});
|
|
101
|
+
socket.setTimeout(1000, () => reject(new RpcSocketSecurityError(`timed out probing ${socketPath}`)));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -32,6 +32,7 @@ export type RpcCommand =
|
|
|
32
32
|
| { id?: string; type: "set_todos"; phases: TodoPhase[] }
|
|
33
33
|
| { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
|
|
34
34
|
| { id?: string; type: "set_host_uri_schemes"; schemes: RpcHostUriSchemeDefinition[] }
|
|
35
|
+
| { id?: string; type: "get_pending_workflow_gates" }
|
|
35
36
|
|
|
36
37
|
// Model
|
|
37
38
|
| { id?: string; type: "set_model"; provider: string; modelId: string }
|
|
@@ -132,6 +133,13 @@ export type RpcResponse =
|
|
|
132
133
|
| { id?: string; type: "response"; command: "set_todos"; success: true; data: { todoPhases: TodoPhase[] } }
|
|
133
134
|
| { id?: string; type: "response"; command: "set_host_tools"; success: true; data: { toolNames: string[] } }
|
|
134
135
|
| { id?: string; type: "response"; command: "set_host_uri_schemes"; success: true; data: { schemes: string[] } }
|
|
136
|
+
| {
|
|
137
|
+
id?: string;
|
|
138
|
+
type: "response";
|
|
139
|
+
command: "get_pending_workflow_gates";
|
|
140
|
+
success: true;
|
|
141
|
+
data: { gates: RpcWorkflowGate[] };
|
|
142
|
+
}
|
|
135
143
|
|
|
136
144
|
// Model
|
|
137
145
|
| {
|
|
@@ -446,6 +454,8 @@ export interface RpcWorkflowGateOption {
|
|
|
446
454
|
|
|
447
455
|
export interface RpcWorkflowGateContext {
|
|
448
456
|
title?: string;
|
|
457
|
+
plan?: string;
|
|
458
|
+
source?: string;
|
|
449
459
|
prompt?: string;
|
|
450
460
|
summary?: string;
|
|
451
461
|
stage_state?: Record<string, unknown>;
|
|
@@ -45,6 +45,8 @@ export interface RpcUnattendedControlPlane {
|
|
|
45
45
|
negotiate(declaration: RpcUnattendedDeclaration): RpcUnattendedAccepted;
|
|
46
46
|
/** Resolve a pending workflow gate with the agent's answer. */
|
|
47
47
|
resolveGate(response: RpcWorkflowGateResponse): Promise<RpcWorkflowGateResolution>;
|
|
48
|
+
/** List unresolved durable workflow gates for reconnect replay. */
|
|
49
|
+
listPendingGates?(): import("../../rpc/rpc-types").RpcWorkflowGate[];
|
|
48
50
|
isUnattended?(): boolean;
|
|
49
51
|
preflightCommand?(command: RpcCommand): void;
|
|
50
52
|
reconcileUsage?(phase?: string): void;
|
|
@@ -213,6 +215,11 @@ export async function dispatchRpcCommand(
|
|
|
213
215
|
return rpcSuccess(id, "set_host_tools", { toolNames: tools.map(tool => tool.name) });
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
case "get_pending_workflow_gates": {
|
|
219
|
+
const gates = unattendedControlPlane?.listPendingGates?.() ?? [];
|
|
220
|
+
return rpcSuccess(id, "get_pending_workflow_gates", { gates });
|
|
221
|
+
}
|
|
222
|
+
|
|
216
223
|
case "set_host_uri_schemes": {
|
|
217
224
|
try {
|
|
218
225
|
const schemes = hostUriRegistry.setSchemes(command.schemes);
|
|
@@ -118,6 +118,7 @@ export function isRpcCommand(value: unknown): value is RpcCommand {
|
|
|
118
118
|
case "get_last_assistant_text":
|
|
119
119
|
case "get_messages":
|
|
120
120
|
case "get_login_providers":
|
|
121
|
+
case "get_pending_workflow_gates":
|
|
121
122
|
return true;
|
|
122
123
|
case "abort_and_prompt":
|
|
123
124
|
return stringField(value, "message") && optionalArray(value.images);
|