@steerable/agent-shell 0.6.47 → 0.6.48
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/main.js +21 -1
- package/dist/preload.cjs +10 -0
- package/dist/preload.js +10 -0
- package/dist/server/start.js +7 -1
- package/dist/sidecar/boot.js +19 -2
- package/dist/sidecar/supervisor.d.ts +4 -3
- package/dist/sidecar/supervisor.js +34 -8
- package/package.json +4 -4
package/dist/app-update.d.ts
CHANGED
|
@@ -11,6 +11,16 @@ export interface AppUpdateState {
|
|
|
11
11
|
percent?: number;
|
|
12
12
|
message?: string;
|
|
13
13
|
}
|
|
14
|
+
/** 渲染层看到的版本与更新状态。`version` 是当前安装版本。 */
|
|
15
|
+
export interface AppReleaseSnapshot {
|
|
16
|
+
version: string;
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
phase: AppUpdatePhase;
|
|
19
|
+
availableVersion?: string;
|
|
20
|
+
percent?: number;
|
|
21
|
+
message?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function toAppReleaseSnapshot(currentVersion: string, state: AppUpdateState): AppReleaseSnapshot;
|
|
14
24
|
export interface AppUpdateCheckResult {
|
|
15
25
|
updateInfo: {
|
|
16
26
|
version: string;
|
package/dist/app-update.js
CHANGED
|
@@ -6,6 +6,20 @@
|
|
|
6
6
|
import { appendFileSync } from 'node:fs';
|
|
7
7
|
import { gt, valid } from 'semver';
|
|
8
8
|
export const APP_UPDATE_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
9
|
+
export function toAppReleaseSnapshot(currentVersion, state) {
|
|
10
|
+
const snapshot = {
|
|
11
|
+
version: currentVersion,
|
|
12
|
+
enabled: state.phase !== 'disabled',
|
|
13
|
+
phase: state.phase,
|
|
14
|
+
};
|
|
15
|
+
if (state.version)
|
|
16
|
+
snapshot.availableVersion = state.version;
|
|
17
|
+
if (state.percent !== undefined)
|
|
18
|
+
snapshot.percent = state.percent;
|
|
19
|
+
if (state.message)
|
|
20
|
+
snapshot.message = state.message;
|
|
21
|
+
return snapshot;
|
|
22
|
+
}
|
|
9
23
|
export function appUpdateMenuAction(state) {
|
|
10
24
|
if (state.phase === 'disabled')
|
|
11
25
|
return 'none';
|
package/dist/main.js
CHANGED
|
@@ -11,7 +11,7 @@ import log from 'electron-log';
|
|
|
11
11
|
import { getBrand } from './brand.js';
|
|
12
12
|
import { getPreloadPath } from './runtime.js';
|
|
13
13
|
import { getProductConfig } from './product-config.js';
|
|
14
|
-
import { AppUpdateController, appUpdateMenuAction, shouldNotifyUpdateReady, } from './app-update.js';
|
|
14
|
+
import { AppUpdateController, appUpdateMenuAction, shouldNotifyUpdateReady, toAppReleaseSnapshot, } from './app-update.js';
|
|
15
15
|
import { createElectronAppUpdater } from './app-update-electron.js';
|
|
16
16
|
import { assertHostIpcAllowed, getResolvedHostTools } from './host-tools-runtime.js';
|
|
17
17
|
import { createJsonStore } from './json-store.js';
|
|
@@ -513,7 +513,26 @@ function createMenu() {
|
|
|
513
513
|
];
|
|
514
514
|
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
515
515
|
}
|
|
516
|
+
function currentReleaseSnapshot(state) {
|
|
517
|
+
return toAppReleaseSnapshot(app.getVersion(), state ?? appUpdates?.state ?? { phase: 'disabled' });
|
|
518
|
+
}
|
|
519
|
+
function broadcastRelease(state) {
|
|
520
|
+
if (!mainWindow || mainWindow.isDestroyed())
|
|
521
|
+
return;
|
|
522
|
+
mainWindow.webContents.send('app-update-state', currentReleaseSnapshot(state));
|
|
523
|
+
}
|
|
516
524
|
function setupIpcHandlers() {
|
|
525
|
+
ipcMain.handle('app:release-snapshot', () => currentReleaseSnapshot());
|
|
526
|
+
ipcMain.handle('app:release-check', async () => {
|
|
527
|
+
if (!appUpdates)
|
|
528
|
+
return currentReleaseSnapshot({ phase: 'disabled' });
|
|
529
|
+
return currentReleaseSnapshot(await appUpdates.checkNow());
|
|
530
|
+
});
|
|
531
|
+
ipcMain.handle('app:release-install', () => {
|
|
532
|
+
if (!appUpdates)
|
|
533
|
+
return currentReleaseSnapshot({ phase: 'disabled' });
|
|
534
|
+
return currentReleaseSnapshot(appUpdates.installNow());
|
|
535
|
+
});
|
|
517
536
|
ipcMain.on('retry-connection', async () => {
|
|
518
537
|
retryCount = 0;
|
|
519
538
|
if (retryTimer) {
|
|
@@ -844,6 +863,7 @@ function startDesktopUpdates() {
|
|
|
844
863
|
updater: createElectronAppUpdater(),
|
|
845
864
|
onState: state => {
|
|
846
865
|
log.info('[app-update]', state.phase, state.version ?? '', state.message ?? '');
|
|
866
|
+
broadcastRelease(state);
|
|
847
867
|
if (shouldNotifyUpdateReady(previous, state) && state.version) {
|
|
848
868
|
const notification = new ElectronNotification({
|
|
849
869
|
title: getBrand().displayName,
|
package/dist/preload.cjs
CHANGED
|
@@ -406,6 +406,16 @@ function buildPreloadApi(packContributions = {}) {
|
|
|
406
406
|
import_electron.ipcRenderer.on("terminal:spawned", fn);
|
|
407
407
|
return () => import_electron.ipcRenderer.removeListener("terminal:spawned", fn);
|
|
408
408
|
}
|
|
409
|
+
},
|
|
410
|
+
app: {
|
|
411
|
+
snapshot: () => import_electron.ipcRenderer.invoke("app:release-snapshot"),
|
|
412
|
+
check: () => import_electron.ipcRenderer.invoke("app:release-check"),
|
|
413
|
+
install: () => import_electron.ipcRenderer.invoke("app:release-install"),
|
|
414
|
+
onState: (callback) => {
|
|
415
|
+
const fn = (_event, snapshot) => callback(snapshot);
|
|
416
|
+
import_electron.ipcRenderer.on("app-update-state", fn);
|
|
417
|
+
return () => import_electron.ipcRenderer.removeListener("app-update-state", fn);
|
|
418
|
+
}
|
|
409
419
|
}
|
|
410
420
|
};
|
|
411
421
|
import_electron.contextBridge.exposeInMainWorld("electron", { ...electronAPI, ...packContributions });
|
package/dist/preload.js
CHANGED
|
@@ -427,6 +427,16 @@ export function buildPreloadApi(packContributions = {}) {
|
|
|
427
427
|
return () => ipcRenderer.removeListener('terminal:spawned', fn);
|
|
428
428
|
},
|
|
429
429
|
},
|
|
430
|
+
app: {
|
|
431
|
+
snapshot: () => ipcRenderer.invoke('app:release-snapshot'),
|
|
432
|
+
check: () => ipcRenderer.invoke('app:release-check'),
|
|
433
|
+
install: () => ipcRenderer.invoke('app:release-install'),
|
|
434
|
+
onState: (callback) => {
|
|
435
|
+
const fn = (_event, snapshot) => callback(snapshot);
|
|
436
|
+
ipcRenderer.on('app-update-state', fn);
|
|
437
|
+
return () => ipcRenderer.removeListener('app-update-state', fn);
|
|
438
|
+
},
|
|
439
|
+
},
|
|
430
440
|
};
|
|
431
441
|
contextBridge.exposeInMainWorld('electron', { ...electronAPI, ...packContributions });
|
|
432
442
|
}
|
package/dist/server/start.js
CHANGED
|
@@ -92,7 +92,7 @@ export async function startBsHost(options = {}) {
|
|
|
92
92
|
shutdown() {
|
|
93
93
|
shutdownPromise ??= (async () => {
|
|
94
94
|
try {
|
|
95
|
-
|
|
95
|
+
const closed = new Promise((resolve, reject) => {
|
|
96
96
|
server.close((error) => {
|
|
97
97
|
if (error)
|
|
98
98
|
reject(error);
|
|
@@ -100,6 +100,12 @@ export async function startBsHost(options = {}) {
|
|
|
100
100
|
resolve();
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
|
+
// The Tauri webview keeps HTTP/SSE connections alive while its
|
|
104
|
+
// ExitRequested handler waits for this promise. Force those
|
|
105
|
+
// connections closed so Cmd+Q does not deadlock until the outer
|
|
106
|
+
// five-second process timeout.
|
|
107
|
+
server.closeAllConnections();
|
|
108
|
+
await closed;
|
|
103
109
|
}
|
|
104
110
|
finally {
|
|
105
111
|
await runtime.shutdown();
|
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 { rustSidecarEnabled, 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(',')
|
|
@@ -166,9 +167,16 @@ async function startEgressProxyIfEnabled(store) {
|
|
|
166
167
|
export async function startHostSidecar(deps) {
|
|
167
168
|
if (process.env.STEERABLE_USE_SIDECAR === '0')
|
|
168
169
|
return;
|
|
170
|
+
const generation = ++sidecarGeneration;
|
|
171
|
+
const pythonRunner = process.env.STEERABLE_PYTHON?.trim();
|
|
172
|
+
const runCodeEnabled = !rustSidecarEnabled() || Boolean(pythonRunner);
|
|
169
173
|
// ready 后的完整接线:注册全局 handle + reverse channels + web 工具握手。
|
|
170
174
|
// 正常 boot 路径与「boot 失败后后台 restart 迟到就绪」路径共用。
|
|
171
175
|
const wireSupervisor = async (supervisor) => {
|
|
176
|
+
if (generation !== sidecarGeneration) {
|
|
177
|
+
await supervisor.shutdown();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
172
180
|
supervisorRef = supervisor;
|
|
173
181
|
setSidecarSupervisor(supervisor);
|
|
174
182
|
// A4 reverse channel: the sidecar-hosted CoreLoop asks this process to
|
|
@@ -302,7 +310,8 @@ export async function startHostSidecar(deps) {
|
|
|
302
310
|
// P1: offer the sidecar's run_code (programmatic tool calls) to the
|
|
303
311
|
// model. The sidecar registers + advertises it; the desktop's
|
|
304
312
|
// tool-router forwards the call back over the reverse channel.
|
|
305
|
-
STEERABLE_RUN_CODE: '1',
|
|
313
|
+
STEERABLE_RUN_CODE: runCodeEnabled ? '1' : '0',
|
|
314
|
+
...(pythonRunner ? { STEERABLE_PYTHON: pythonRunner } : {}),
|
|
306
315
|
// P3: conversational JS PTC (run_js/wait_js). The sidecar spawns a
|
|
307
316
|
// long-lived Node worker. Browser and Tauri desktop hosts both run
|
|
308
317
|
// under the pinned Node runtime, so process.execPath is directly
|
|
@@ -311,6 +320,13 @@ export async function startHostSidecar(deps) {
|
|
|
311
320
|
STEERABLE_PTC_NODE: process.execPath,
|
|
312
321
|
},
|
|
313
322
|
onLogLine: (line) => (deps.onLogLine ?? ((l) => log.info('[sidecar]', l)))(line),
|
|
323
|
+
}, (created) => {
|
|
324
|
+
if (generation === sidecarGeneration) {
|
|
325
|
+
supervisorRef = created;
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
void created.shutdown();
|
|
329
|
+
}
|
|
314
330
|
});
|
|
315
331
|
await wireSupervisor(supervisor);
|
|
316
332
|
return supervisor;
|
|
@@ -336,6 +352,7 @@ export async function startHostSidecar(deps) {
|
|
|
336
352
|
}
|
|
337
353
|
/** 宿主退出钩子:停 supervisor + egress proxy。幂等。 */
|
|
338
354
|
export async function shutdownHostSidecar() {
|
|
355
|
+
sidecarGeneration += 1;
|
|
339
356
|
if (supervisorRef) {
|
|
340
357
|
const supervisor = supervisorRef;
|
|
341
358
|
supervisorRef = null;
|
|
@@ -48,7 +48,7 @@ export declare class SidecarSupervisor extends EventEmitter {
|
|
|
48
48
|
private reverseHandlers;
|
|
49
49
|
private constructor();
|
|
50
50
|
/** Spawn the sidecar and wait for the ready marker. */
|
|
51
|
-
static start(options?: SidecarStartOptions): Promise<SidecarSupervisor>;
|
|
51
|
+
static start(options?: SidecarStartOptions, onCreate?: (supervisor: SidecarSupervisor) => void): Promise<SidecarSupervisor>;
|
|
52
52
|
/** Round-trip a JSON-RPC method call. */
|
|
53
53
|
call<T = unknown>(method: string, params?: unknown, options?: SidecarMethodOptions): Promise<T>;
|
|
54
54
|
/** Convenience: list tools registered on the sidecar. */
|
|
@@ -220,7 +220,8 @@ export declare class SidecarSupervisor extends EventEmitter {
|
|
|
220
220
|
export declare function resolveSidecarPython(pythonExecutable?: string): string;
|
|
221
221
|
export declare function rustSidecarEnabled(): boolean;
|
|
222
222
|
/**
|
|
223
|
-
* Resolve the
|
|
224
|
-
*
|
|
223
|
+
* Resolve the Rust sidecar binary from an explicit path, a packaged engine,
|
|
224
|
+
* or a framework source checkout. Boot fails when Rust was explicitly
|
|
225
|
+
* enabled but no binary resolves.
|
|
225
226
|
*/
|
|
226
227
|
export declare function resolveRustSidecarBin(explicit?: string): string | undefined;
|
|
@@ -77,8 +77,9 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
77
77
|
this.options = options;
|
|
78
78
|
}
|
|
79
79
|
/** Spawn the sidecar and wait for the ready marker. */
|
|
80
|
-
static async start(options = {}) {
|
|
80
|
+
static async start(options = {}, onCreate) {
|
|
81
81
|
const supervisor = new SidecarSupervisor(options);
|
|
82
|
+
onCreate?.(supervisor);
|
|
82
83
|
try {
|
|
83
84
|
await supervisor.boot();
|
|
84
85
|
SidecarSupervisor.lastSpawnRefusal = null;
|
|
@@ -383,19 +384,28 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
383
384
|
// Internal: boot
|
|
384
385
|
// ------------------------------------------------------------------
|
|
385
386
|
async boot() {
|
|
386
|
-
const
|
|
387
|
+
const useRustSidecar = rustSidecarEnabled();
|
|
388
|
+
const rustBin = useRustSidecar
|
|
387
389
|
? resolveRustSidecarBin(this.options.rustSidecarBin)
|
|
388
390
|
: undefined;
|
|
389
391
|
let spawnPlan;
|
|
390
392
|
if (rustBin) {
|
|
391
393
|
spawnPlan = await this.resolveSandboxedSpawn(rustBin, this.options.args ?? []);
|
|
392
394
|
}
|
|
395
|
+
else if (useRustSidecar) {
|
|
396
|
+
throw new Error('STEERABLE_RUST_SIDECAR is enabled but the Rust sidecar binary is missing');
|
|
397
|
+
}
|
|
393
398
|
else {
|
|
394
399
|
const py = this.resolvePythonBinary();
|
|
395
400
|
const entry = this.options.entryModule ?? 'steerable_sidecar';
|
|
396
401
|
const args = ['-m', entry, ...(this.options.args ?? [])];
|
|
397
402
|
spawnPlan = await this.resolveSandboxedSpawn(py, args);
|
|
398
403
|
}
|
|
404
|
+
// Profile generation is asynchronous. A quit can begin while it runs;
|
|
405
|
+
// never spawn a new child after shutdown has claimed the supervisor.
|
|
406
|
+
if (this.shuttingDown) {
|
|
407
|
+
throw new SidecarShutdownError('sidecar startup cancelled by shutdown');
|
|
408
|
+
}
|
|
399
409
|
const child = spawn(spawnPlan.command, spawnPlan.args, {
|
|
400
410
|
cwd: this.options.cwd,
|
|
401
411
|
env: { ...process.env, ...this.options.env, ...spawnPlan.env },
|
|
@@ -508,9 +518,6 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
508
518
|
(allowResolver ? ' + resolver only (web tools egress via the per-host proxy)' : '') +
|
|
509
519
|
')');
|
|
510
520
|
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
521
|
const confinedTmp = join(writableRoots[0], 'tmp');
|
|
515
522
|
mkdirSync(confinedTmp, { recursive: true });
|
|
516
523
|
return {
|
|
@@ -646,6 +653,15 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
646
653
|
child.stderr.setEncoding('utf-8');
|
|
647
654
|
child.stdout.on('data', (chunk) => this.handleStdoutChunk(chunk));
|
|
648
655
|
child.stderr.on('data', (chunk) => this.handleStderrChunk(chunk));
|
|
656
|
+
child.stdin.on('error', (error) => {
|
|
657
|
+
// A child may close stdin between shutdown's requireChild() and write().
|
|
658
|
+
// Keep the stream error handled; the write callback/pending failure
|
|
659
|
+
// carries the transport error to callers.
|
|
660
|
+
this.failPending(new SidecarShutdownError(`sidecar stdin closed: ${error.message}`));
|
|
661
|
+
if (!this.shuttingDown) {
|
|
662
|
+
this.options.onLogLine?.(`sidecar stdin error: ${error.message}`);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
649
665
|
child.on('exit', (code, signal) => {
|
|
650
666
|
this.emit('exit', { code, signal });
|
|
651
667
|
this.failPending(new SidecarShutdownError(`sidecar exited (code=${code ?? 'null'}, signal=${signal ?? 'null'})`));
|
|
@@ -968,11 +984,15 @@ function envFlag(name) {
|
|
|
968
984
|
return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
|
|
969
985
|
}
|
|
970
986
|
export function rustSidecarEnabled() {
|
|
971
|
-
|
|
987
|
+
if ((process.env[RUST_SIDECAR_ENV] ?? '').trim()) {
|
|
988
|
+
return envFlag(RUST_SIDECAR_ENV);
|
|
989
|
+
}
|
|
990
|
+
return resolveRustSidecarBin() !== undefined;
|
|
972
991
|
}
|
|
973
992
|
/**
|
|
974
|
-
* Resolve the
|
|
975
|
-
*
|
|
993
|
+
* Resolve the Rust sidecar binary from an explicit path, a packaged engine,
|
|
994
|
+
* or a framework source checkout. Boot fails when Rust was explicitly
|
|
995
|
+
* enabled but no binary resolves.
|
|
976
996
|
*/
|
|
977
997
|
export function resolveRustSidecarBin(explicit) {
|
|
978
998
|
if (explicit) {
|
|
@@ -983,6 +1003,12 @@ export function resolveRustSidecarBin(explicit) {
|
|
|
983
1003
|
return existsSync(fromEnv) ? fromEnv : undefined;
|
|
984
1004
|
}
|
|
985
1005
|
const exe = process.platform === 'win32' ? 'steerable-sidecar.exe' : 'steerable-sidecar';
|
|
1006
|
+
const resourcesPath = process.resourcesPath;
|
|
1007
|
+
if (resourcesPath) {
|
|
1008
|
+
const packaged = join(resourcesPath, 'engine', exe);
|
|
1009
|
+
if (existsSync(packaged))
|
|
1010
|
+
return packaged;
|
|
1011
|
+
}
|
|
986
1012
|
const relatives = [
|
|
987
1013
|
join('..', '..', '..', '..', 'sidecar', 'rs', 'target', 'debug', exe),
|
|
988
1014
|
join('..', '..', '..', '..', '..', 'sidecar', 'rs', 'target', 'debug', exe),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.48",
|
|
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/
|
|
59
|
-
"@steerable/
|
|
60
|
-
"@steerable/agent-protocol": "0.6.
|
|
58
|
+
"@steerable/agent-harness": "0.6.48",
|
|
59
|
+
"@steerable/pack-sdk": "0.6.48",
|
|
60
|
+
"@steerable/agent-protocol": "0.6.48"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/better-sqlite3": "^7.6.13",
|