@steerable/agent-shell 0.6.47 → 0.6.49
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/app-update.d.ts +10 -0
- package/dist/app-update.js +14 -0
- package/dist/host/runtime.js +3 -3
- package/dist/host-tools.d.ts +1 -1
- package/dist/host-tools.js +1 -0
- package/dist/llm/sidecar-provider.d.ts +0 -4
- package/dist/llm/sidecar-provider.js +0 -4
- package/dist/local-backend/coreloop-stream.d.ts +1 -3
- package/dist/local-backend/coreloop-stream.js +1 -3
- package/dist/local-backend/portable-bundle.d.ts +202 -0
- package/dist/local-backend/portable-bundle.js +318 -0
- package/dist/local-backend/portable-service.d.ts +23 -0
- package/dist/local-backend/portable-service.js +469 -0
- package/dist/local-backend/router.js +20 -0
- package/dist/local-backend/skills/85-local-exec/SKILL.md +2 -0
- package/dist/local-backend/skills/86-proactive-coding/SKILL.md +1 -1
- package/dist/local-backend/turn-files.d.ts +20 -0
- package/dist/local-backend/turn-files.js +162 -5
- package/dist/main.js +21 -1
- package/dist/preload.cjs +10 -0
- package/dist/preload.js +10 -0
- package/dist/present-files.d.ts +64 -0
- package/dist/present-files.js +136 -0
- package/dist/product-config.d.ts +6 -0
- package/dist/product-config.js +4 -0
- package/dist/server/start.js +7 -1
- package/dist/sidecar/boot.d.ts +19 -4
- package/dist/sidecar/boot.js +54 -20
- package/dist/sidecar/handle.d.ts +5 -8
- package/dist/sidecar/handle.js +6 -10
- package/dist/sidecar/supervisor.d.ts +1 -9
- package/dist/sidecar/supervisor.js +31 -81
- package/dist/sidecar/types.d.ts +0 -2
- package/dist/tool-router.d.ts +19 -2
- package/dist/tool-router.js +63 -2
- package/package.json +4 -4
package/dist/sidecar/boot.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import log from 'electron-log';
|
|
12
|
-
import { SidecarSupervisor } from './supervisor.js';
|
|
12
|
+
import { SidecarSupervisor, } from './supervisor.js';
|
|
13
13
|
import { setSidecarSupervisor, setSidecarSupervisorPending, llmService } from '../llm/index.js';
|
|
14
14
|
import { resolveSidecarStoragePath } from './storage-path.js';
|
|
15
15
|
import { deriveEgressAllowListFromBaseUrl, probeExecSandboxCapability, } from './exec-sandbox.js';
|
|
@@ -22,6 +22,7 @@ import { sidecarWebSearchEnv } from '../storage/web-search-settings.js';
|
|
|
22
22
|
import { executeHostedWebSearch } from '../hosted-web-search.js';
|
|
23
23
|
let egressProxyRef = null;
|
|
24
24
|
let supervisorRef = null;
|
|
25
|
+
let sidecarGeneration = 0;
|
|
25
26
|
async function deriveSidecarEgressAllowList() {
|
|
26
27
|
const envList = (process.env.STEERABLE_SIDECAR_SANDBOX_ALLOWED_HOSTS ?? '')
|
|
27
28
|
.split(',')
|
|
@@ -158,17 +159,49 @@ async function startEgressProxyIfEnabled(store) {
|
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* (
|
|
162
|
+
* Wire the sidecar plugin registry into the host tool router.
|
|
163
|
+
*
|
|
164
|
+
* The `plugin.list` probe doubles as the availability check: on failure the
|
|
165
|
+
* router is cleared (no plugin_* tools, no plugin-provided tools) and the
|
|
166
|
+
* error is rethrown for the caller to log. On success the router gets the
|
|
167
|
+
* plugin.* RPC seam plus a `tool.invoke` forwarder, then syncs the enabled
|
|
168
|
+
* plugins' tool descriptors from `plugin.tools.describe`. Plugin tools run
|
|
169
|
+
* only in the sidecar; approval already happened in its CoreLoop before a
|
|
170
|
+
* call is forwarded here, hence `consentGranted`.
|
|
171
|
+
*
|
|
172
|
+
* @param supervisor Running sidecar supervisor.
|
|
173
|
+
* @param toolRouter Host router that advertises and dispatches the tools.
|
|
174
|
+
*/
|
|
175
|
+
export async function wirePluginRegistry(supervisor, toolRouter) {
|
|
176
|
+
try {
|
|
177
|
+
await supervisor.call('plugin.list');
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
toolRouter.setPluginRpc(null);
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
toolRouter.setPluginRpc((method, params) => supervisor.call(method, params), (name, args) => supervisor.invokeTool(name, args, {
|
|
184
|
+
consentGranted: true,
|
|
185
|
+
timeoutMs: WEB_TOOL_RPC_TIMEOUT_MS,
|
|
186
|
+
}));
|
|
187
|
+
await toolRouter.refreshPluginTools();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The complete Python sidecar hosts the Rust CoreLoop and is mandatory.
|
|
191
|
+
* Boot is registered synchronously so early RPC thin clients (skill-loader
|
|
192
|
+
* et al.) can await it via whenSidecarSupervisor().
|
|
165
193
|
*/
|
|
166
194
|
export async function startHostSidecar(deps) {
|
|
167
|
-
|
|
168
|
-
|
|
195
|
+
const generation = ++sidecarGeneration;
|
|
196
|
+
const pythonRunner = process.env.STEERABLE_PYTHON?.trim();
|
|
197
|
+
const runCodeEnabled = true;
|
|
169
198
|
// ready 后的完整接线:注册全局 handle + reverse channels + web 工具握手。
|
|
170
199
|
// 正常 boot 路径与「boot 失败后后台 restart 迟到就绪」路径共用。
|
|
171
200
|
const wireSupervisor = async (supervisor) => {
|
|
201
|
+
if (generation !== sidecarGeneration) {
|
|
202
|
+
await supervisor.shutdown();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
172
205
|
supervisorRef = supervisor;
|
|
173
206
|
setSidecarSupervisor(supervisor);
|
|
174
207
|
// A4 reverse channel: the sidecar-hosted CoreLoop asks this process to
|
|
@@ -251,12 +284,10 @@ export async function startHostSidecar(deps) {
|
|
|
251
284
|
// 缺省接线)。模型面是 tool-router 的 plugin_* 工具(deferred 层,经
|
|
252
285
|
// tool_search 发现);这里注入直调缝。握手失败降级为工具缺席,不阻塞 boot。
|
|
253
286
|
try {
|
|
254
|
-
await supervisor.
|
|
255
|
-
deps.toolRouter.setPluginRpc((method, params) => supervisor.call(method, params));
|
|
287
|
+
await wirePluginRegistry(supervisor, deps.toolRouter);
|
|
256
288
|
log.info('[sidecar] plugin registry wired; plugin_* tools available');
|
|
257
289
|
}
|
|
258
290
|
catch (err) {
|
|
259
|
-
deps.toolRouter.setPluginRpc(null);
|
|
260
291
|
log.warn('[sidecar] plugin registry unavailable; plugin_* tools hidden this session', err);
|
|
261
292
|
}
|
|
262
293
|
log.info('[sidecar] ready', supervisor.getBootSnapshot());
|
|
@@ -302,7 +333,8 @@ export async function startHostSidecar(deps) {
|
|
|
302
333
|
// P1: offer the sidecar's run_code (programmatic tool calls) to the
|
|
303
334
|
// model. The sidecar registers + advertises it; the desktop's
|
|
304
335
|
// tool-router forwards the call back over the reverse channel.
|
|
305
|
-
STEERABLE_RUN_CODE: '1',
|
|
336
|
+
STEERABLE_RUN_CODE: runCodeEnabled ? '1' : '0',
|
|
337
|
+
...(pythonRunner ? { STEERABLE_PYTHON: pythonRunner } : {}),
|
|
306
338
|
// P3: conversational JS PTC (run_js/wait_js). The sidecar spawns a
|
|
307
339
|
// long-lived Node worker. Browser and Tauri desktop hosts both run
|
|
308
340
|
// under the pinned Node runtime, so process.execPath is directly
|
|
@@ -311,24 +343,25 @@ export async function startHostSidecar(deps) {
|
|
|
311
343
|
STEERABLE_PTC_NODE: process.execPath,
|
|
312
344
|
},
|
|
313
345
|
onLogLine: (line) => (deps.onLogLine ?? ((l) => log.info('[sidecar]', l)))(line),
|
|
346
|
+
}, (created) => {
|
|
347
|
+
if (generation === sidecarGeneration) {
|
|
348
|
+
supervisorRef = created;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
void created.shutdown();
|
|
352
|
+
}
|
|
314
353
|
});
|
|
315
354
|
await wireSupervisor(supervisor);
|
|
316
355
|
return supervisor;
|
|
317
356
|
}
|
|
318
357
|
catch (err) {
|
|
319
|
-
log.error('[sidecar] failed to start
|
|
358
|
+
log.error('[sidecar] failed to start; Python sidecar is required', err);
|
|
320
359
|
setSidecarSupervisor(null);
|
|
321
|
-
// boot 失败后 supervisor 的 exit→restart 循环仍在后台重试(另一个
|
|
322
|
-
// 宿主暂时持有 sessions.lock 这类瞬态冲突会自愈)。迟到就绪时补
|
|
323
|
-
// 接线,否则 sidecar 进程活着但 router 永远 503。
|
|
324
360
|
const late = err.supervisor;
|
|
325
361
|
if (late) {
|
|
326
|
-
late.
|
|
327
|
-
log.info('[sidecar] late ready after initial boot failure');
|
|
328
|
-
void wireSupervisor(late);
|
|
329
|
-
});
|
|
362
|
+
await late.shutdown();
|
|
330
363
|
}
|
|
331
|
-
|
|
364
|
+
throw err;
|
|
332
365
|
}
|
|
333
366
|
})();
|
|
334
367
|
setSidecarSupervisorPending(boot);
|
|
@@ -336,6 +369,7 @@ export async function startHostSidecar(deps) {
|
|
|
336
369
|
}
|
|
337
370
|
/** 宿主退出钩子:停 supervisor + egress proxy。幂等。 */
|
|
338
371
|
export async function shutdownHostSidecar() {
|
|
372
|
+
sidecarGeneration += 1;
|
|
339
373
|
if (supervisorRef) {
|
|
340
374
|
const supervisor = supervisorRef;
|
|
341
375
|
supervisorRef = null;
|
package/dist/sidecar/handle.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* sidecar has booted
|
|
4
|
-
*
|
|
5
|
-
* `STEERABLE_USE_SIDECAR=0` to fall back to the in-process providers — also
|
|
6
|
-
* the automatic behavior when the sidecar failed to start.
|
|
2
|
+
* Mandatory sidecar handle. The host calls `setSidecarSupervisor()` after the
|
|
3
|
+
* complete Python sidecar has booted; CoreLoop and provider traffic route
|
|
4
|
+
* through it.
|
|
7
5
|
*
|
|
8
6
|
* Kept in a dependency-light module (type-only import of the supervisor) so
|
|
9
7
|
* RPC thin clients (`local-edit`, `skill-loader`) can read the handle without
|
|
@@ -25,8 +23,7 @@ export declare function isSidecarEnabled(): boolean;
|
|
|
25
23
|
/**
|
|
26
24
|
* Await the sidecar handle through boot: returns the live handle immediately
|
|
27
25
|
* when already up, otherwise waits on the registered boot promise. Returns
|
|
28
|
-
* null when
|
|
29
|
-
*
|
|
30
|
-
* caller to its no-sidecar fallback instead of hanging the request).
|
|
26
|
+
* null when no boot is in flight, boot failed, or the wait exceeds
|
|
27
|
+
* `timeoutMs`; callers surface that as sidecar unavailable.
|
|
31
28
|
*/
|
|
32
29
|
export declare function whenSidecarSupervisor(timeoutMs?: number): Promise<SidecarSupervisor | null>;
|
package/dist/sidecar/handle.js
CHANGED
|
@@ -14,21 +14,18 @@ export function setSidecarSupervisorPending(pending) {
|
|
|
14
14
|
}
|
|
15
15
|
/** The supervised sidecar handle, when the sidecar path is active. */
|
|
16
16
|
export function getSidecarSupervisor() {
|
|
17
|
-
return
|
|
17
|
+
return sidecarSupervisor;
|
|
18
18
|
}
|
|
19
19
|
export function isSidecarEnabled() {
|
|
20
|
-
return
|
|
20
|
+
return sidecarSupervisor != null;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Await the sidecar handle through boot: returns the live handle immediately
|
|
24
24
|
* when already up, otherwise waits on the registered boot promise. Returns
|
|
25
|
-
* null when
|
|
26
|
-
*
|
|
27
|
-
* caller to its no-sidecar fallback instead of hanging the request).
|
|
25
|
+
* null when no boot is in flight, boot failed, or the wait exceeds
|
|
26
|
+
* `timeoutMs`; callers surface that as sidecar unavailable.
|
|
28
27
|
*/
|
|
29
28
|
export async function whenSidecarSupervisor(timeoutMs = 20_000) {
|
|
30
|
-
if (process.env.STEERABLE_USE_SIDECAR === '0')
|
|
31
|
-
return null;
|
|
32
29
|
const live = getSidecarSupervisor();
|
|
33
30
|
if (live)
|
|
34
31
|
return live;
|
|
@@ -39,9 +36,8 @@ export async function whenSidecarSupervisor(timeoutMs = 20_000) {
|
|
|
39
36
|
const timeout = new Promise((resolve) => {
|
|
40
37
|
timer = setTimeout(() => resolve(null), timeoutMs);
|
|
41
38
|
});
|
|
42
|
-
// A rejected boot promise
|
|
43
|
-
//
|
|
44
|
-
// crashing on someone else's boot failure.
|
|
39
|
+
// A rejected boot promise is surfaced by host startup; concurrent waiters
|
|
40
|
+
// receive null and return their explicit sidecar-unavailable error.
|
|
45
41
|
const settled = pending.catch(() => null);
|
|
46
42
|
try {
|
|
47
43
|
return await Promise.race([settled, timeout]);
|
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { EventEmitter } from 'node:events';
|
|
13
13
|
import type { SidecarApplyEditsResult, SidecarChatStreamHandlers, SidecarChatStreamRequest, SidecarHealthSnapshot, SidecarMethodOptions, SidecarModelCatalog, SidecarReverseHandler, SidecarSandboxPosture, SidecarSessionBranches, SidecarSessionForkOutcome, SidecarSessionMessages, SidecarSessionTree, SidecarSkillModule, SidecarStartOptions, SidecarToolResult } from './types.js';
|
|
14
|
-
export declare const RUST_SIDECAR_ENV = "STEERABLE_RUST_SIDECAR";
|
|
15
|
-
export declare const RUST_SIDECAR_BIN_ENV = "STEERABLE_RUST_SIDECAR_BIN";
|
|
16
14
|
/**
|
|
17
15
|
* Tool names carried by a `tool.list` reply.
|
|
18
16
|
*
|
|
@@ -48,7 +46,7 @@ export declare class SidecarSupervisor extends EventEmitter {
|
|
|
48
46
|
private reverseHandlers;
|
|
49
47
|
private constructor();
|
|
50
48
|
/** Spawn the sidecar and wait for the ready marker. */
|
|
51
|
-
static start(options?: SidecarStartOptions): Promise<SidecarSupervisor>;
|
|
49
|
+
static start(options?: SidecarStartOptions, onCreate?: (supervisor: SidecarSupervisor) => void): Promise<SidecarSupervisor>;
|
|
52
50
|
/** Round-trip a JSON-RPC method call. */
|
|
53
51
|
call<T = unknown>(method: string, params?: unknown, options?: SidecarMethodOptions): Promise<T>;
|
|
54
52
|
/** Convenience: list tools registered on the sidecar. */
|
|
@@ -218,9 +216,3 @@ export declare class SidecarSupervisor extends EventEmitter {
|
|
|
218
216
|
* launcher (W1.3.3) spawns the same interpreter the sidecar uses.
|
|
219
217
|
*/
|
|
220
218
|
export declare function resolveSidecarPython(pythonExecutable?: string): string;
|
|
221
|
-
export declare function rustSidecarEnabled(): boolean;
|
|
222
|
-
/**
|
|
223
|
-
* Resolve the optional Rust sidecar binary. Missing binary with the flag
|
|
224
|
-
* on is a silent fallback to Python (`python -m steerable_sidecar`).
|
|
225
|
-
*/
|
|
226
|
-
export declare function resolveRustSidecarBin(explicit?: string): string | undefined;
|
|
@@ -25,8 +25,6 @@ const READY_PREFIX = '__SIDECAR_READY__:';
|
|
|
25
25
|
const DEFAULT_BOOT_TIMEOUT_MS = 15_000;
|
|
26
26
|
const DEFAULT_HEALTH_INTERVAL_MS = 5_000;
|
|
27
27
|
const DEFAULT_RESTART_AFTER_FAILED_PINGS = 3;
|
|
28
|
-
export const RUST_SIDECAR_ENV = 'STEERABLE_RUST_SIDECAR';
|
|
29
|
-
export const RUST_SIDECAR_BIN_ENV = 'STEERABLE_RUST_SIDECAR_BIN';
|
|
30
28
|
// Only /usr/bin/sandbox-exec is trusted — a PATH-relative lookup could
|
|
31
29
|
// resolve to an attacker-planted binary (codex's rule).
|
|
32
30
|
const SEATBELT_EXECUTABLE = '/usr/bin/sandbox-exec';
|
|
@@ -77,8 +75,9 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
77
75
|
this.options = options;
|
|
78
76
|
}
|
|
79
77
|
/** Spawn the sidecar and wait for the ready marker. */
|
|
80
|
-
static async start(options = {}) {
|
|
78
|
+
static async start(options = {}, onCreate) {
|
|
81
79
|
const supervisor = new SidecarSupervisor(options);
|
|
80
|
+
onCreate?.(supervisor);
|
|
82
81
|
try {
|
|
83
82
|
await supervisor.boot();
|
|
84
83
|
SidecarSupervisor.lastSpawnRefusal = null;
|
|
@@ -383,18 +382,14 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
383
382
|
// Internal: boot
|
|
384
383
|
// ------------------------------------------------------------------
|
|
385
384
|
async boot() {
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const py = this.resolvePythonBinary();
|
|
395
|
-
const entry = this.options.entryModule ?? 'steerable_sidecar';
|
|
396
|
-
const args = ['-m', entry, ...(this.options.args ?? [])];
|
|
397
|
-
spawnPlan = await this.resolveSandboxedSpawn(py, args);
|
|
385
|
+
const py = this.resolvePythonBinary();
|
|
386
|
+
const entry = this.options.entryModule ?? 'steerable_sidecar';
|
|
387
|
+
const args = ['-m', entry, ...(this.options.args ?? [])];
|
|
388
|
+
const spawnPlan = await this.resolveSandboxedSpawn(py, args);
|
|
389
|
+
// Profile generation is asynchronous. A quit can begin while it runs;
|
|
390
|
+
// never spawn a new child after shutdown has claimed the supervisor.
|
|
391
|
+
if (this.shuttingDown) {
|
|
392
|
+
throw new SidecarShutdownError('sidecar startup cancelled by shutdown');
|
|
398
393
|
}
|
|
399
394
|
const child = spawn(spawnPlan.command, spawnPlan.args, {
|
|
400
395
|
cwd: this.options.cwd,
|
|
@@ -492,12 +487,7 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
492
487
|
...(webEgress ? ['--allow-web-egress'] : []),
|
|
493
488
|
...(allowResolver ? ['--allow-resolver'] : []),
|
|
494
489
|
];
|
|
495
|
-
const
|
|
496
|
-
? resolveRustSidecarBin(this.options.rustSidecarBin)
|
|
497
|
-
: undefined;
|
|
498
|
-
const { stdout } = rustBin
|
|
499
|
-
? await execFileAsync(rustBin, ['sandbox', 'profile', ...flags], { timeout: 10_000 })
|
|
500
|
-
: await execFileAsync(command, ['-m', 'steerable_sidecar.sandbox', 'profile', ...flags], { timeout: 10_000 });
|
|
490
|
+
const { stdout } = await execFileAsync(command, ['-m', 'steerable_sidecar.sandbox', 'profile', ...flags], { timeout: 10_000 });
|
|
501
491
|
const profile = stdout.trim();
|
|
502
492
|
if (!profile.includes('(deny default)')) {
|
|
503
493
|
throw new Error('generated profile is not a Seatbelt policy');
|
|
@@ -508,9 +498,6 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
508
498
|
(allowResolver ? ' + resolver only (web tools egress via the per-host proxy)' : '') +
|
|
509
499
|
')');
|
|
510
500
|
this.sandboxPosture = { backend: 'seatbelt', enforcement: 'partial', reason: 'active' };
|
|
511
|
-
// macOS TMPDIR is /var/folders, which the Seatbelt profile does not
|
|
512
|
-
// allow. run_code / run_js materialize their workers there and fail
|
|
513
|
-
// with EPERM, so the model cannot list or read local folders.
|
|
514
501
|
const confinedTmp = join(writableRoots[0], 'tmp');
|
|
515
502
|
mkdirSync(confinedTmp, { recursive: true });
|
|
516
503
|
return {
|
|
@@ -545,29 +532,17 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
545
532
|
// 都透传环境)+ sidecar 应用层域名名单强制,网络命名空间保持共享。
|
|
546
533
|
// 这里如实记录,不假装接上了实际不强制的参数。
|
|
547
534
|
const egressViaProxy = this.options.env?.STEERABLE_EGRESS_CONFINED === '1';
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
command,
|
|
558
|
-
...args,
|
|
559
|
-
]
|
|
560
|
-
: [
|
|
561
|
-
'-m',
|
|
562
|
-
'steerable_sidecar.sandbox',
|
|
563
|
-
'linux-wrap',
|
|
564
|
-
...writableRoots.flatMap((root) => ['--writable-root', root]),
|
|
565
|
-
'--',
|
|
566
|
-
command,
|
|
567
|
-
...args,
|
|
568
|
-
];
|
|
535
|
+
const wrapArgs = [
|
|
536
|
+
'-m',
|
|
537
|
+
'steerable_sidecar.sandbox',
|
|
538
|
+
'linux-wrap',
|
|
539
|
+
...writableRoots.flatMap((root) => ['--writable-root', root]),
|
|
540
|
+
'--',
|
|
541
|
+
command,
|
|
542
|
+
...args,
|
|
543
|
+
];
|
|
569
544
|
try {
|
|
570
|
-
const { stdout } = await execFileAsync(
|
|
545
|
+
const { stdout } = await execFileAsync(command, wrapArgs, { timeout: 15_000 });
|
|
571
546
|
const plan = JSON.parse(stdout.trim());
|
|
572
547
|
if (!Array.isArray(plan.argv) || plan.argv.length < 1 || typeof plan.argv[0] !== 'string') {
|
|
573
548
|
throw new Error('linux-wrap did not return an argv');
|
|
@@ -646,6 +621,15 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
646
621
|
child.stderr.setEncoding('utf-8');
|
|
647
622
|
child.stdout.on('data', (chunk) => this.handleStdoutChunk(chunk));
|
|
648
623
|
child.stderr.on('data', (chunk) => this.handleStderrChunk(chunk));
|
|
624
|
+
child.stdin.on('error', (error) => {
|
|
625
|
+
// A child may close stdin between shutdown's requireChild() and write().
|
|
626
|
+
// Keep the stream error handled; the write callback/pending failure
|
|
627
|
+
// carries the transport error to callers.
|
|
628
|
+
this.failPending(new SidecarShutdownError(`sidecar stdin closed: ${error.message}`));
|
|
629
|
+
if (!this.shuttingDown) {
|
|
630
|
+
this.options.onLogLine?.(`sidecar stdin error: ${error.message}`);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
649
633
|
child.on('exit', (code, signal) => {
|
|
650
634
|
this.emit('exit', { code, signal });
|
|
651
635
|
this.failPending(new SidecarShutdownError(`sidecar exited (code=${code ?? 'null'}, signal=${signal ?? 'null'})`));
|
|
@@ -960,39 +944,5 @@ export function resolveSidecarPython(pythonExecutable) {
|
|
|
960
944
|
if (existsSync(candidate))
|
|
961
945
|
return candidate;
|
|
962
946
|
}
|
|
963
|
-
|
|
964
|
-
return process.platform === 'win32' ? 'python' : 'python3';
|
|
965
|
-
}
|
|
966
|
-
function envFlag(name) {
|
|
967
|
-
const raw = (process.env[name] ?? '').trim().toLowerCase();
|
|
968
|
-
return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
|
|
969
|
-
}
|
|
970
|
-
export function rustSidecarEnabled() {
|
|
971
|
-
return envFlag(RUST_SIDECAR_ENV);
|
|
972
|
-
}
|
|
973
|
-
/**
|
|
974
|
-
* Resolve the optional Rust sidecar binary. Missing binary with the flag
|
|
975
|
-
* on is a silent fallback to Python (`python -m steerable_sidecar`).
|
|
976
|
-
*/
|
|
977
|
-
export function resolveRustSidecarBin(explicit) {
|
|
978
|
-
if (explicit) {
|
|
979
|
-
return existsSync(explicit) ? explicit : undefined;
|
|
980
|
-
}
|
|
981
|
-
const fromEnv = process.env[RUST_SIDECAR_BIN_ENV];
|
|
982
|
-
if (fromEnv) {
|
|
983
|
-
return existsSync(fromEnv) ? fromEnv : undefined;
|
|
984
|
-
}
|
|
985
|
-
const exe = process.platform === 'win32' ? 'steerable-sidecar.exe' : 'steerable-sidecar';
|
|
986
|
-
const relatives = [
|
|
987
|
-
join('..', '..', '..', '..', 'sidecar', 'rs', 'target', 'debug', exe),
|
|
988
|
-
join('..', '..', '..', '..', '..', 'sidecar', 'rs', 'target', 'debug', exe),
|
|
989
|
-
join('..', '..', '..', '..', 'sidecar', 'rs', 'target', 'release', exe),
|
|
990
|
-
join('..', '..', '..', '..', '..', 'sidecar', 'rs', 'target', 'release', exe),
|
|
991
|
-
];
|
|
992
|
-
for (const rel of relatives) {
|
|
993
|
-
const candidate = join(__dirname, rel);
|
|
994
|
-
if (existsSync(candidate))
|
|
995
|
-
return candidate;
|
|
996
|
-
}
|
|
997
|
-
return undefined;
|
|
947
|
+
throw new Error('portable Python sidecar runtime not found; prepare python-runtime or set STEERABLE_SIDECAR_PYTHON');
|
|
998
948
|
}
|
package/dist/sidecar/types.d.ts
CHANGED
|
@@ -21,8 +21,6 @@ export interface SidecarSandboxPosture {
|
|
|
21
21
|
export interface SidecarStartOptions {
|
|
22
22
|
/** Override the python binary; defaults to the bundled portable runtime. */
|
|
23
23
|
pythonExecutable?: string;
|
|
24
|
-
/** Optional Rust sidecar binary; used when STEERABLE_RUST_SIDECAR is on. */
|
|
25
|
-
rustSidecarBin?: string;
|
|
26
24
|
/** Override the entrypoint module; defaults to ``steerable_sidecar``. */
|
|
27
25
|
entryModule?: string;
|
|
28
26
|
/** Extra arguments appended after ``-m <entryModule>``. */
|
package/dist/tool-router.d.ts
CHANGED
|
@@ -76,7 +76,13 @@ export declare const WEB_TOOL_RPC_TIMEOUT_MS = 130000;
|
|
|
76
76
|
* 由 PluginRegistry 背书。宿主只持 schema 并经此 delegate 直调——与 web 工具
|
|
77
77
|
* 不同,这些是普通 RPC 而非 tool.invoke(不经过模型工具分发层)。
|
|
78
78
|
*/
|
|
79
|
-
export type PluginRpcDelegate = (method: 'plugin.list' | 'plugin.enable' | 'plugin.disable' | 'plugin.reload', params?: Record<string, unknown>) => Promise<unknown>;
|
|
79
|
+
export type PluginRpcDelegate = (method: 'plugin.list' | 'plugin.enable' | 'plugin.disable' | 'plugin.reload' | 'plugin.tools.describe', params?: Record<string, unknown>) => Promise<unknown>;
|
|
80
|
+
/**
|
|
81
|
+
* 插件自带工具的执行缝:唯一实现在 sidecar 的插件路由上,宿主只持
|
|
82
|
+
* `plugin.tools.describe` 给出的 schema,调用经 `tool.invoke` 前转(与 web
|
|
83
|
+
* 工具同一模式)。审批已在 sidecar CoreLoop 的 ApprovalExecutor 完成。
|
|
84
|
+
*/
|
|
85
|
+
export type PluginToolDelegate = (name: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
80
86
|
/**
|
|
81
87
|
* 本机是否提供网络读取工具(`STEERABLE_WEB_TOOLS=0` 关闭)。
|
|
82
88
|
*
|
|
@@ -142,7 +148,18 @@ export declare class ToolRouter {
|
|
|
142
148
|
* plugin_* 工具一个 schema 都不出——模型看不到不可用的工具。
|
|
143
149
|
*/
|
|
144
150
|
private pluginRpc;
|
|
145
|
-
|
|
151
|
+
private pluginToolInvoke;
|
|
152
|
+
/** 已启用插件的工具(名字 → schema),由 refreshPluginTools 从 sidecar 同步。 */
|
|
153
|
+
private pluginToolSchemas;
|
|
154
|
+
setPluginRpc(delegate: PluginRpcDelegate | null, invokeTool?: PluginToolDelegate | null): void;
|
|
155
|
+
private pluginToolsEnabled;
|
|
156
|
+
/**
|
|
157
|
+
* 从 sidecar 拉取已启用插件的工具描述,替换宿主侧的插件工具集。
|
|
158
|
+
* 启动握手后和每次 plugin_enable / plugin_disable / plugin_reload 成功后
|
|
159
|
+
* 调用,保证模型看到的集合与 sidecar 路由上可分发的集合一致。
|
|
160
|
+
* 与内置 / 场景包工具重名的插件工具被跳过并告警——内置工具优先。
|
|
161
|
+
*/
|
|
162
|
+
refreshPluginTools(): Promise<void>;
|
|
146
163
|
listSchemas(): ToolSchema[];
|
|
147
164
|
/**
|
|
148
165
|
* 插件生命周期工具(框架 plugin.* RPC 的模型面)。全在 deferred 层:
|
package/dist/tool-router.js
CHANGED
|
@@ -6,6 +6,7 @@ import { rankTools, resolveMaxResults, TOOL_SEARCH_DEFAULT_MAX_RESULTS, } from '
|
|
|
6
6
|
import { maybeAutoInstallWrittenSkill } from './local-backend/skill-install.js';
|
|
7
7
|
import { filterToolsByPolicy, isToolAllowed, } from './local-backend/agent-capability.js';
|
|
8
8
|
import { processViewImage } from './image-attachment.js';
|
|
9
|
+
import { PRESENT_FILES_SCHEMA, PRESENT_FILES_TOOL_NAME, executePresentFiles, } from './present-files.js';
|
|
9
10
|
import { isHostToolCapabilityEnabled } from './host-tools.js';
|
|
10
11
|
import { getResolvedHostTools } from './host-tools-runtime.js';
|
|
11
12
|
/** 已注册 MCP 服务的动态工具名前缀:`mcp__<serverKey>__<toolName>`。 */
|
|
@@ -22,6 +23,7 @@ export const MCP_DYNAMIC_TOOL_PREFIX = 'mcp__';
|
|
|
22
23
|
export const TOOL_SEARCH_TOOL_NAME = 'tool_search';
|
|
23
24
|
/** 前转 RPC 超时:sidecar 侧单次请求上限默认 30s(可配到 600s),这里给足余量。 */
|
|
24
25
|
export const WEB_TOOL_RPC_TIMEOUT_MS = 130_000;
|
|
26
|
+
const PLUGIN_TOOL_MODES = new Set(['read', 'safe_write', 'destructive']);
|
|
25
27
|
/**
|
|
26
28
|
* 本机是否提供网络读取工具(`STEERABLE_WEB_TOOLS=0` 关闭)。
|
|
27
29
|
*
|
|
@@ -107,8 +109,52 @@ export class ToolRouter {
|
|
|
107
109
|
* plugin_* 工具一个 schema 都不出——模型看不到不可用的工具。
|
|
108
110
|
*/
|
|
109
111
|
pluginRpc = null;
|
|
110
|
-
|
|
112
|
+
pluginToolInvoke = null;
|
|
113
|
+
/** 已启用插件的工具(名字 → schema),由 refreshPluginTools 从 sidecar 同步。 */
|
|
114
|
+
pluginToolSchemas = new Map();
|
|
115
|
+
setPluginRpc(delegate, invokeTool = null) {
|
|
111
116
|
this.pluginRpc = delegate;
|
|
117
|
+
this.pluginToolInvoke = delegate ? invokeTool : null;
|
|
118
|
+
if (!delegate)
|
|
119
|
+
this.pluginToolSchemas = new Map();
|
|
120
|
+
}
|
|
121
|
+
pluginToolsEnabled() {
|
|
122
|
+
return getResolvedHostTools().plugins.capability;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 从 sidecar 拉取已启用插件的工具描述,替换宿主侧的插件工具集。
|
|
126
|
+
* 启动握手后和每次 plugin_enable / plugin_disable / plugin_reload 成功后
|
|
127
|
+
* 调用,保证模型看到的集合与 sidecar 路由上可分发的集合一致。
|
|
128
|
+
* 与内置 / 场景包工具重名的插件工具被跳过并告警——内置工具优先。
|
|
129
|
+
*/
|
|
130
|
+
async refreshPluginTools() {
|
|
131
|
+
if (!this.pluginRpc || !this.pluginToolInvoke) {
|
|
132
|
+
this.pluginToolSchemas = new Map();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const result = (await this.pluginRpc('plugin.tools.describe'));
|
|
136
|
+
this.pluginToolSchemas = new Map();
|
|
137
|
+
const taken = new Set(this.listSchemas().map((s) => s.name));
|
|
138
|
+
const next = new Map();
|
|
139
|
+
for (const tool of result?.tools ?? []) {
|
|
140
|
+
if (!tool || typeof tool.name !== 'string' || !tool.name)
|
|
141
|
+
continue;
|
|
142
|
+
if (taken.has(tool.name)) {
|
|
143
|
+
console.warn(`[tool-router] plugin tool ${tool.name} (${tool.plugin ?? 'unknown plugin'}) shadows a host tool; skipped`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const owner = tool.plugin ? `[plugin:${tool.plugin}] ` : '';
|
|
147
|
+
next.set(tool.name, {
|
|
148
|
+
name: tool.name,
|
|
149
|
+
description: `${owner}${tool.description || tool.name}`,
|
|
150
|
+
inputSchema: tool.schema && typeof tool.schema === 'object'
|
|
151
|
+
? tool.schema
|
|
152
|
+
: { type: 'object', properties: {} },
|
|
153
|
+
mode: (tool.mode && PLUGIN_TOOL_MODES.has(tool.mode) ? tool.mode : 'external'),
|
|
154
|
+
exposure: 'deferred',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
this.pluginToolSchemas = next;
|
|
112
158
|
}
|
|
113
159
|
listSchemas() {
|
|
114
160
|
return [
|
|
@@ -240,6 +286,7 @@ export class ToolRouter {
|
|
|
240
286
|
required: ['path', 'edits'],
|
|
241
287
|
},
|
|
242
288
|
},
|
|
289
|
+
PRESENT_FILES_SCHEMA,
|
|
243
290
|
{
|
|
244
291
|
name: 'local_open_path',
|
|
245
292
|
description: 'Open local path or URL',
|
|
@@ -374,6 +421,9 @@ export class ToolRouter {
|
|
|
374
421
|
...this.listWebToolSchemas(),
|
|
375
422
|
...this.listRegisteredMcpToolSchemas(),
|
|
376
423
|
...this.listPluginToolSchemas(),
|
|
424
|
+
// 插件自带的工具:与 MCP 动态工具同在 deferred 层,经 tool_search 发现。
|
|
425
|
+
// 名字不带族前缀,按名归族会漏过 plugins 能力开关,这里显式判定。
|
|
426
|
+
...(this.pluginToolsEnabled() ? this.pluginToolSchemas.values() : []),
|
|
377
427
|
// ─── 场景包贡献的工具(0.3a 开槽) ─────────────────
|
|
378
428
|
...[...this.contributedTools.values()].map((c) => ({
|
|
379
429
|
name: c.name,
|
|
@@ -747,6 +797,8 @@ export class ToolRouter {
|
|
|
747
797
|
this.autoInstallSkillOnSuccess(edited, args.path);
|
|
748
798
|
return edited;
|
|
749
799
|
}
|
|
800
|
+
case PRESENT_FILES_TOOL_NAME:
|
|
801
|
+
return await executePresentFiles(args, projectRoot);
|
|
750
802
|
case 'local_open_path':
|
|
751
803
|
return await this.localExecutor.openLocalTarget({
|
|
752
804
|
target: String(args.target || ''),
|
|
@@ -802,7 +854,10 @@ export class ToolRouter {
|
|
|
802
854
|
}
|
|
803
855
|
const method = call.name.replace('_', '.');
|
|
804
856
|
const params = call.name === 'plugin_list' ? undefined : { name: String(args.name || '') };
|
|
805
|
-
|
|
857
|
+
const result = await this.pluginRpc(method, params);
|
|
858
|
+
if (call.name !== 'plugin_list')
|
|
859
|
+
await this.refreshPluginTools();
|
|
860
|
+
return result;
|
|
806
861
|
}
|
|
807
862
|
case 'task_run': {
|
|
808
863
|
const chatId = this.requireTaskContext(context);
|
|
@@ -857,6 +912,12 @@ export class ToolRouter {
|
|
|
857
912
|
if (contributed) {
|
|
858
913
|
return await contributed.handler(args, context ?? {});
|
|
859
914
|
}
|
|
915
|
+
if (this.pluginToolSchemas.has(call.name) && this.pluginToolInvoke) {
|
|
916
|
+
if (!this.pluginToolsEnabled()) {
|
|
917
|
+
throw new Error(`工具 ${call.name} 未在本产品引入`);
|
|
918
|
+
}
|
|
919
|
+
return await this.pluginToolInvoke(call.name, args);
|
|
920
|
+
}
|
|
860
921
|
return { success: false, error: `Unknown tool: ${call.name}` };
|
|
861
922
|
}
|
|
862
923
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.49",
|
|
4
4
|
"description": "Steerable framework — product-neutral Node HostRuntime and headless HTTP server shared by Tauri desktop and browser deployments; scenario packs extend it through @steerable/pack-sdk.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/",
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"he": "^1.2.0",
|
|
56
56
|
"node-pty": "^1.1.0",
|
|
57
57
|
"semver": "^7.8.0",
|
|
58
|
-
"@steerable/pack-sdk": "0.6.
|
|
59
|
-
"@steerable/agent-
|
|
60
|
-
"@steerable/agent-
|
|
58
|
+
"@steerable/pack-sdk": "0.6.49",
|
|
59
|
+
"@steerable/agent-protocol": "0.6.49",
|
|
60
|
+
"@steerable/agent-harness": "0.6.49"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/better-sqlite3": "^7.6.13",
|