@steerable/agent-shell 0.6.21 → 0.6.22
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 +34 -0
- package/dist/auth/index.d.ts +38 -0
- package/dist/auth/index.js +20 -0
- package/dist/host/pack-assembly.d.ts +7 -7
- package/dist/host/runtime.d.ts +6 -2
- package/dist/host/runtime.js +54 -14
- package/dist/insights/flush.d.ts +5 -4
- package/dist/insights/flush.js +14 -15
- package/dist/insights/record.d.ts +6 -5
- package/dist/insights/record.js +9 -10
- package/dist/llm/index.d.ts +6 -2
- package/dist/llm/index.js +19 -9
- package/dist/local-backend/prompt-builder.d.ts +10 -4
- package/dist/local-backend/prompt-builder.js +13 -12
- package/dist/local-backend/router.d.ts +18 -4
- package/dist/local-backend/router.js +133 -98
- package/dist/local-backend/skills/00-identity/SKILL.md +1 -1
- package/dist/local-backend/task-service.d.ts +14 -13
- package/dist/local-backend/task-service.js +32 -29
- package/dist/local-backend/worktree-service.d.ts +1 -1
- package/dist/local-backend/worktree-service.js +6 -6
- package/dist/main.js +43 -26
- package/dist/server/http-server.d.ts +13 -1
- package/dist/server/http-server.js +37 -8
- package/dist/server/index.js +4 -90
- package/dist/server/start.d.ts +26 -0
- package/dist/server/start.js +111 -0
- package/dist/sidecar/boot.d.ts +3 -1
- package/dist/sidecar/boot.js +6 -7
- package/dist/sidecar/reverse-spawn.js +12 -4
- package/dist/sidecar/reverse-tools.d.ts +1 -1
- package/dist/sidecar/reverse-tools.js +1 -1
- package/dist/sidecar/storage-path.js +2 -3
- package/dist/storage/driver.d.ts +57 -0
- package/dist/storage/driver.js +104 -0
- package/dist/storage/index.d.ts +75 -68
- package/dist/storage/index.js +506 -301
- package/dist/storage/pack-migrations.d.ts +2 -4
- package/dist/storage/pack-migrations.js +2 -4
- package/dist/storage/scoped-store.d.ts +125 -0
- package/dist/storage/scoped-store.js +1 -0
- package/dist/storage/sqlite-driver.d.ts +16 -0
- package/dist/storage/sqlite-driver.js +135 -0
- package/dist/storage/write-lease-error.d.ts +11 -0
- package/dist/storage/write-lease-error.js +18 -0
- package/package.json +14 -5
- package/dist/storage/local-store-singleton.d.ts +0 -28
- package/dist/storage/local-store-singleton.js +0 -38
package/dist/server/index.js
CHANGED
|
@@ -1,100 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* 装配与 Electron 主进程(main.ts)相同的一组服务——同一份装配住在
|
|
5
|
-
* src/host/runtime.ts(HostRuntime),这里只保留 BS 特有的部分:SSE
|
|
6
|
-
* 总线广播、HTTP 出口(http-server.ts)、SIGINT/SIGTERM 生命周期。
|
|
7
|
-
* 数据目录、sidecar、工具链、审批流全部共用;两种模式因此操作同一份
|
|
8
|
-
* SQLite / JSON 配置(不要同时启动两个宿主——write-lease 会拒绝第二个写者)。
|
|
9
|
-
*
|
|
10
|
-
* 默认绑定 127.0.0.1:4787(DEEPPATH_BS_HOST / DEEPPATH_BS_PORT 可改)。
|
|
11
|
-
* 无认证:与桌面版同一信任模型,不要暴露到非回环地址。
|
|
12
|
-
*/
|
|
13
|
-
// 产品组装根必须是第一个 import:包迁移注册要先于 storage 单例构造(0.3b)。
|
|
14
|
-
// 产品组装根(products/<id>/server.ts 或 devtools/dev-server.ts)必须先于
|
|
15
|
-
// 本模块完成 import:包迁移注册要先于 storage 单例构造(0.3b / 2.3)。
|
|
16
|
-
import { existsSync } from 'node:fs';
|
|
17
|
-
import path from 'node:path';
|
|
18
|
-
import { fileURLToPath } from 'node:url';
|
|
19
|
-
import dotenv from 'dotenv';
|
|
20
|
-
import log from 'electron-log';
|
|
21
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
// 与 main.ts 相同的 .env 加载顺序(.env.local 优先;已有环境变量胜出)。
|
|
23
|
-
(function loadDotenvFiles() {
|
|
24
|
-
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
25
|
-
for (const file of ['.env.local', '.env']) {
|
|
26
|
-
const fullPath = path.join(repoRoot, file);
|
|
27
|
-
if (existsSync(fullPath))
|
|
28
|
-
dotenv.config({ path: fullPath, override: false });
|
|
29
|
-
}
|
|
30
|
-
})();
|
|
31
|
-
import { getBrand } from '../brand.js';
|
|
32
|
-
import { getUserDataDir } from '../runtime.js';
|
|
33
|
-
import { createHostRuntime } from '../host/runtime.js';
|
|
34
|
-
import { registerPackHttpRoutes } from '../host/http-routes.js';
|
|
35
|
-
import { SseBus } from './sse-bus.js';
|
|
36
|
-
import { createBsServer } from './http-server.js';
|
|
37
|
-
// BS 模式没有 Electron 的 userData 概念,electron-log 默认会写
|
|
38
|
-
// ~/.config/<name>/logs;在 HOME 只读(容器/只读环境)或用户显式指定
|
|
39
|
-
// DEEPPATH_USER_DATA_DIR 时,日志应跟随应用数据目录走。
|
|
40
|
-
log.transports.file.resolvePathFn = () => path.join(getUserDataDir(), 'logs', 'main.log');
|
|
1
|
+
/** Executable BS entry; reusable assembly lives in `start.ts`. */
|
|
2
|
+
import { startBsHost } from './start.js';
|
|
41
3
|
async function main() {
|
|
42
|
-
const
|
|
43
|
-
const bus = new SseBus();
|
|
44
|
-
const broadcast = (channel, payload) => bus.broadcast(channel, payload);
|
|
45
|
-
// 与 CS 同一份服务装配;BS 的差异只有广播(SSE 总线)与"有浏览器连着
|
|
46
|
-
// 事件总线 = 有人能应答审批/提问"的 hasWindow 判定。
|
|
47
|
-
const runtime = createHostRuntime({
|
|
48
|
-
broadcast,
|
|
49
|
-
hasWindow: () => bus.size > 0,
|
|
50
|
-
onLog: (line) => log.info('[sidecar]', line),
|
|
51
|
-
taskSweepReason: '服务重启,任务流已中断',
|
|
52
|
-
});
|
|
53
|
-
const { localExecutor, localScriptRegistry, terminalManager, packHandles, localBackendRouter, approvalBridge, askUserBridge, maybeExecInTerminal, } = runtime;
|
|
54
|
-
// 2.3 起 web 产物按产品构建:产品入口注入 DEEPPATH_WEB_DIST
|
|
55
|
-
//(products/<id>/web/dist);缺省回退到 shell 包自带的 web/dist
|
|
56
|
-
//(packages/agent-shell/web/dist),仅用于给出可读的报错。
|
|
57
|
-
const webDistDir = process.env.DEEPPATH_WEB_DIST ??
|
|
58
|
-
path.join(__dirname, '..', '..', 'web', 'dist');
|
|
59
|
-
if (!existsSync(path.join(webDistDir, 'index.html'))) {
|
|
60
|
-
console.error(`[bs] web build not found at ${webDistDir} — run \`pnpm build\` first.`);
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
// 场景包 HTTP 路由(/host/<packId>/*)由装配产物给出,宿主循环注册
|
|
64
|
-
//(2.3;路由路径命名空间由 registerPackHttpRoutes 校验)。
|
|
65
|
-
for (const [packId, handle] of packHandles) {
|
|
66
|
-
const routes = handle.httpRoutes?.();
|
|
67
|
-
if (routes)
|
|
68
|
-
registerPackHttpRoutes(packId, routes);
|
|
69
|
-
}
|
|
70
|
-
const server = createBsServer({
|
|
71
|
-
localBackendRouter,
|
|
72
|
-
localExecutor,
|
|
73
|
-
localScriptRegistry,
|
|
74
|
-
terminalManager,
|
|
75
|
-
approvalBridge,
|
|
76
|
-
askUserBridge,
|
|
77
|
-
maybeExecInTerminal,
|
|
78
|
-
bus,
|
|
79
|
-
webDistDir,
|
|
80
|
-
});
|
|
81
|
-
const host = process.env.DEEPPATH_BS_HOST || '127.0.0.1';
|
|
82
|
-
const port = Number(process.env.DEEPPATH_BS_PORT || 4787);
|
|
83
|
-
await new Promise((resolve, reject) => {
|
|
84
|
-
server.once('error', reject);
|
|
85
|
-
server.listen(port, host, () => resolve());
|
|
86
|
-
});
|
|
87
|
-
console.log(`[bs] ${brand.displayName} server listening at http://${host}:${port} (flavor=${brand.flavor})`);
|
|
88
|
-
// 任务清扫 / mock 广播 / sidecar 启动 / MCP 刷新 / 终端预热。不阻塞
|
|
89
|
-
// HTTP 就绪;竞速的回合由 router 回退处理(同 main.ts)。
|
|
90
|
-
runtime.start();
|
|
4
|
+
const handle = await startBsHost();
|
|
91
5
|
let shuttingDown = false;
|
|
92
6
|
const shutdown = () => {
|
|
93
7
|
if (shuttingDown)
|
|
94
8
|
return;
|
|
95
9
|
shuttingDown = true;
|
|
96
10
|
console.log('[bs] shutting down…');
|
|
97
|
-
void
|
|
11
|
+
void handle.shutdown().finally(() => process.exit(0));
|
|
98
12
|
// 兜底:sidecar 卡住也不拖住退出。
|
|
99
13
|
setTimeout(() => process.exit(0), 5_000).unref();
|
|
100
14
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Server } from 'node:http';
|
|
2
|
+
import type { TenantScope } from '../storage/driver.js';
|
|
3
|
+
import { type BsMiddleware } from './http-server.js';
|
|
4
|
+
export interface BsHostOptions {
|
|
5
|
+
host?: string;
|
|
6
|
+
port?: number;
|
|
7
|
+
authToken?: string;
|
|
8
|
+
middleware?: readonly BsMiddleware[];
|
|
9
|
+
/** Fixed storage owner for a per-user host process. */
|
|
10
|
+
scope?: TenantScope;
|
|
11
|
+
}
|
|
12
|
+
export interface BsHostHandle {
|
|
13
|
+
readonly server: Server;
|
|
14
|
+
readonly host: string;
|
|
15
|
+
readonly port: number;
|
|
16
|
+
/** Stops accepting requests and shuts down all host runtime services. */
|
|
17
|
+
shutdown(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Assembles and starts the BS HTTP host.
|
|
21
|
+
*
|
|
22
|
+
* Product composition roots may register providers, package contributions,
|
|
23
|
+
* and middleware before calling this function.
|
|
24
|
+
*/
|
|
25
|
+
export declare function startBsHost(options?: BsHostOptions): Promise<BsHostHandle>;
|
|
26
|
+
export type { BsMiddleware };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable BS host assembly for executable and embedded deployments.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import dotenv from 'dotenv';
|
|
8
|
+
import log from 'electron-log';
|
|
9
|
+
import { getBrand } from '../brand.js';
|
|
10
|
+
import { registerPackHttpRoutes } from '../host/http-routes.js';
|
|
11
|
+
import { createHostRuntime } from '../host/runtime.js';
|
|
12
|
+
import { getUserDataDir } from '../runtime.js';
|
|
13
|
+
import { createBsServer, } from './http-server.js';
|
|
14
|
+
import { SseBus } from './sse-bus.js';
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
// Match the Electron entry's dotenv precedence: local file first, existing
|
|
17
|
+
// process variables always win.
|
|
18
|
+
for (const file of ['.env.local', '.env']) {
|
|
19
|
+
const fullPath = path.join(path.resolve(__dirname, '..', '..'), file);
|
|
20
|
+
if (existsSync(fullPath))
|
|
21
|
+
dotenv.config({ path: fullPath, override: false });
|
|
22
|
+
}
|
|
23
|
+
log.transports.file.resolvePathFn = () => path.join(getUserDataDir(), 'logs', 'main.log');
|
|
24
|
+
/**
|
|
25
|
+
* Assembles and starts the BS HTTP host.
|
|
26
|
+
*
|
|
27
|
+
* Product composition roots may register providers, package contributions,
|
|
28
|
+
* and middleware before calling this function.
|
|
29
|
+
*/
|
|
30
|
+
export async function startBsHost(options = {}) {
|
|
31
|
+
const brand = getBrand();
|
|
32
|
+
const bus = new SseBus();
|
|
33
|
+
const broadcast = (channel, payload) => bus.broadcast(channel, payload);
|
|
34
|
+
const runtime = await createHostRuntime({
|
|
35
|
+
scope: options.scope,
|
|
36
|
+
broadcast,
|
|
37
|
+
hasWindow: () => bus.size > 0,
|
|
38
|
+
onLog: (line) => log.info('[sidecar]', line),
|
|
39
|
+
taskSweepReason: '服务重启,任务流已中断',
|
|
40
|
+
});
|
|
41
|
+
const { localExecutor, localScriptRegistry, terminalManager, packHandles, localBackendRouter, approvalBridge, askUserBridge, maybeExecInTerminal, } = runtime;
|
|
42
|
+
const webDistDir = process.env.DEEPPATH_WEB_DIST ??
|
|
43
|
+
path.join(__dirname, '..', '..', 'web', 'dist');
|
|
44
|
+
if (!existsSync(path.join(webDistDir, 'index.html'))) {
|
|
45
|
+
await runtime.shutdown();
|
|
46
|
+
throw new Error(`[bs] web build not found at ${webDistDir} — run \`pnpm build\` first.`);
|
|
47
|
+
}
|
|
48
|
+
for (const [packId, handle] of packHandles) {
|
|
49
|
+
const routes = handle.httpRoutes?.();
|
|
50
|
+
if (routes)
|
|
51
|
+
registerPackHttpRoutes(packId, routes);
|
|
52
|
+
}
|
|
53
|
+
const server = createBsServer({
|
|
54
|
+
store: runtime.store,
|
|
55
|
+
localBackendRouter,
|
|
56
|
+
localExecutor,
|
|
57
|
+
localScriptRegistry,
|
|
58
|
+
terminalManager,
|
|
59
|
+
approvalBridge,
|
|
60
|
+
askUserBridge,
|
|
61
|
+
maybeExecInTerminal,
|
|
62
|
+
bus,
|
|
63
|
+
webDistDir,
|
|
64
|
+
authToken: options.authToken,
|
|
65
|
+
middleware: options.middleware,
|
|
66
|
+
});
|
|
67
|
+
const host = options.host ?? (process.env.DEEPPATH_BS_HOST || '127.0.0.1');
|
|
68
|
+
const requestedPort = options.port ?? Number(process.env.DEEPPATH_BS_PORT || 4787);
|
|
69
|
+
try {
|
|
70
|
+
await new Promise((resolve, reject) => {
|
|
71
|
+
const onError = (error) => reject(error);
|
|
72
|
+
server.once('error', onError);
|
|
73
|
+
server.listen(requestedPort, host, () => {
|
|
74
|
+
server.off('error', onError);
|
|
75
|
+
resolve();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
await runtime.shutdown();
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
const address = server.address();
|
|
84
|
+
const port = address.port;
|
|
85
|
+
console.log(`[bs] ${brand.displayName} server listening at http://${host}:${port} (flavor=${brand.flavor})`);
|
|
86
|
+
await runtime.start();
|
|
87
|
+
let shutdownPromise = null;
|
|
88
|
+
return {
|
|
89
|
+
server,
|
|
90
|
+
host,
|
|
91
|
+
port,
|
|
92
|
+
shutdown() {
|
|
93
|
+
shutdownPromise ??= (async () => {
|
|
94
|
+
try {
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
server.close((error) => {
|
|
97
|
+
if (error)
|
|
98
|
+
reject(error);
|
|
99
|
+
else
|
|
100
|
+
resolve();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await runtime.shutdown();
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
108
|
+
return shutdownPromise;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
package/dist/sidecar/boot.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type { ScopedStore } from '../storage/scoped-store.js';
|
|
1
2
|
import { type ToolRouter } from '../tool-router.js';
|
|
2
3
|
import type { createApprovalBridge } from './reverse-approval.js';
|
|
3
4
|
import type { SidecarReverseHandler } from './types.js';
|
|
4
5
|
export interface HostSidecarDeps {
|
|
6
|
+
store: ScopedStore;
|
|
5
7
|
toolRouter: ToolRouter;
|
|
6
8
|
/** W4-2 项目模式围栏:chatId → 项目根(无项目对话返回 null)。 */
|
|
7
|
-
resolveProjectRoot: (chatId: string) => string | null
|
|
9
|
+
resolveProjectRoot: (chatId: string) => Promise<string | null>;
|
|
8
10
|
/**
|
|
9
11
|
* 项目模式之外额外放行的只读根(会话附件目录等)。文件写入仍只受
|
|
10
12
|
* projectRoot 围栏约束;这些根只放宽 local_read_file 的读取范围。
|
package/dist/sidecar/boot.js
CHANGED
|
@@ -10,7 +10,6 @@ import path from 'node:path';
|
|
|
10
10
|
import log from 'electron-log';
|
|
11
11
|
import { SidecarSupervisor, resolveSidecarPython } from './supervisor.js';
|
|
12
12
|
import { setSidecarSupervisor, setSidecarSupervisorPending, llmService } from '../llm/index.js';
|
|
13
|
-
import { localStore } from '../storage/index.js';
|
|
14
13
|
import { resolveSidecarStoragePath } from './storage-path.js';
|
|
15
14
|
import { deriveEgressAllowListFromBaseUrl, probeExecSandboxCapability, } from './exec-sandbox.js';
|
|
16
15
|
import { createToolInvokeHandler } from './reverse-tools.js';
|
|
@@ -58,7 +57,7 @@ async function deriveSidecarEgressAllowList() {
|
|
|
58
57
|
* ambient 端点并进允许列表,见 W4-8)。
|
|
59
58
|
* - 启动失败 / 派生不出任何代理条目。
|
|
60
59
|
*/
|
|
61
|
-
async function startEgressProxyIfEnabled() {
|
|
60
|
+
async function startEgressProxyIfEnabled(store) {
|
|
62
61
|
try {
|
|
63
62
|
const ambient = process.env.STEERABLE_EGRESS_PROXY === '0' ? [] : await collectAmbientProxyEndpoints();
|
|
64
63
|
const decision = decideEgressProxy({ env: process.env, ambientProxies: ambient });
|
|
@@ -72,7 +71,7 @@ async function startEgressProxyIfEnabled() {
|
|
|
72
71
|
return null;
|
|
73
72
|
}
|
|
74
73
|
const settings = llmService.getSettings();
|
|
75
|
-
const searchSettings =
|
|
74
|
+
const searchSettings = await store.getWebSearchSettings();
|
|
76
75
|
const searchEnv = sidecarWebSearchEnv({
|
|
77
76
|
processEnv: process.env,
|
|
78
77
|
storedApiKey: searchSettings?.apiKey,
|
|
@@ -214,7 +213,7 @@ export async function startHostSidecar(deps) {
|
|
|
214
213
|
consentGranted: true,
|
|
215
214
|
timeoutMs: WEB_TOOL_RPC_TIMEOUT_MS,
|
|
216
215
|
}), webNames);
|
|
217
|
-
const searchSettings =
|
|
216
|
+
const searchSettings = await deps.store.getWebSearchSettings();
|
|
218
217
|
const searchEnv = sidecarWebSearchEnv({
|
|
219
218
|
processEnv: process.env,
|
|
220
219
|
storedApiKey: searchSettings?.apiKey,
|
|
@@ -254,7 +253,7 @@ export async function startHostSidecar(deps) {
|
|
|
254
253
|
};
|
|
255
254
|
const boot = (async () => {
|
|
256
255
|
try {
|
|
257
|
-
const egressProxy = await startEgressProxyIfEnabled();
|
|
256
|
+
const egressProxy = await startEgressProxyIfEnabled(deps.store);
|
|
258
257
|
// W2.6.1: durable sessions/traces/history in a zero-dependency
|
|
259
258
|
// sqlite database under ~/.steerable — the one root the Seatbelt
|
|
260
259
|
// profile already allows writes to; userData would be denied.
|
|
@@ -286,8 +285,8 @@ export async function startHostSidecar(deps) {
|
|
|
286
285
|
...egressProxy?.env,
|
|
287
286
|
...sidecarWebSearchEnv({
|
|
288
287
|
processEnv: process.env,
|
|
289
|
-
storedApiKey:
|
|
290
|
-
storedProvider:
|
|
288
|
+
storedApiKey: (await deps.store.getWebSearchSettings())?.apiKey,
|
|
289
|
+
storedProvider: (await deps.store.getWebSearchSettings())?.provider,
|
|
291
290
|
llmBaseUrl: llmService.getSettings().baseUrl,
|
|
292
291
|
}),
|
|
293
292
|
// P1: offer the sidecar's run_code (programmatic tool calls) to the
|
|
@@ -29,17 +29,25 @@ export function resolveWinSpawnHelperPath() {
|
|
|
29
29
|
// 显式覆盖(测试/调试逃生门):设置后只认这一条路径。指向不存在的
|
|
30
30
|
// 路径即可强制走"helper 缺失"的失败分支,与 dev checkout 是否已构建
|
|
31
31
|
// Rust 二进制无关,保证失败分支的测试确定性。
|
|
32
|
-
const override = process.env.DEEPPATH_WIN_SPAWN_HELPER;
|
|
32
|
+
const override = process.env.DEEPPATH_WIN_SPAWN_HELPER ?? process.env.STEERABLE_WIN_SPAWN_HELPER;
|
|
33
33
|
if (override !== undefined) {
|
|
34
34
|
return override && existsSync(override) ? override : null;
|
|
35
35
|
}
|
|
36
|
-
const
|
|
36
|
+
const electron = process;
|
|
37
|
+
const resourcesPath = electron.resourcesPath;
|
|
37
38
|
const candidates = [];
|
|
38
39
|
if (resourcesPath) {
|
|
39
40
|
candidates.push(join(resourcesPath, 'win-spawn-helper', binary));
|
|
40
41
|
}
|
|
41
|
-
//
|
|
42
|
-
|
|
42
|
+
// Packaged extraResources live under resourcesPath. cwd is the user's
|
|
43
|
+
// launch directory and must not supply the confinement binary.
|
|
44
|
+
// `electron .` sets defaultApp and does not copy extraResources there;
|
|
45
|
+
// npm-published shell also cannot see the host checkout via __dirname.
|
|
46
|
+
const packagedElectron = Boolean(resourcesPath) && electron.defaultApp !== true;
|
|
47
|
+
if (!packagedElectron) {
|
|
48
|
+
candidates.push(join(process.cwd(), 'resources', 'windows-spawn-helper', binary));
|
|
49
|
+
candidates.push(join(__dirname, '..', '..', 'resources', 'windows-spawn-helper', binary));
|
|
50
|
+
}
|
|
43
51
|
for (const candidate of candidates) {
|
|
44
52
|
if (existsSync(candidate))
|
|
45
53
|
return candidate;
|
|
@@ -19,7 +19,7 @@ export interface ReverseToolDeps {
|
|
|
19
19
|
* main.ts 注入 LocalBackendRouter.resolveChatProject 的适配;未注入时
|
|
20
20
|
* 项目围栏不生效(CLI/test 场景)。
|
|
21
21
|
*/
|
|
22
|
-
resolveProjectRoot?: (chatId: string) => string | null
|
|
22
|
+
resolveProjectRoot?: (chatId: string) => Promise<string | null>;
|
|
23
23
|
/**
|
|
24
24
|
* 额外放行的只读根(会话附件目录等)。写入仍只受 projectRoot 围栏约束,
|
|
25
25
|
* 这里只放宽 local_read_file 的读取范围。
|
|
@@ -53,7 +53,7 @@ export function createToolInvokeHandler(deps) {
|
|
|
53
53
|
const projectRoot = typeof p.context?.workspaceRoot === 'string' && p.context.workspaceRoot
|
|
54
54
|
? p.context.workspaceRoot
|
|
55
55
|
: p.context?.chatId
|
|
56
|
-
? (deps.resolveProjectRoot?.(p.context.chatId) ?? null)
|
|
56
|
+
? (await deps.resolveProjectRoot?.(p.context.chatId) ?? null)
|
|
57
57
|
: null;
|
|
58
58
|
const additionalReadRoots = p.context?.chatId
|
|
59
59
|
? (deps.resolveAdditionalReadRoots?.(p.context.chatId) ?? [])
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sidecar 持久化库(sessions/traces/history)路径解析。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 路径付这个代价。
|
|
4
|
+
* Kept outside boot.ts so history readers can resolve this separate database
|
|
5
|
+
* without initializing host storage.
|
|
7
6
|
*/
|
|
8
7
|
import path from 'node:path';
|
|
9
8
|
import { homedir } from 'node:os';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ScopedStore } from './scoped-store.js';
|
|
2
|
+
/** Ownership context applied to every host and scenario-pack database operation. */
|
|
3
|
+
export interface TenantScope {
|
|
4
|
+
tenantId: string;
|
|
5
|
+
userId: string;
|
|
6
|
+
}
|
|
7
|
+
/** Ownership used by the personal, single-user product. */
|
|
8
|
+
export declare const LOCAL_SCOPE: TenantScope;
|
|
9
|
+
export type PackDbParams = readonly unknown[] | Readonly<Record<string, unknown>>;
|
|
10
|
+
export interface PackDbRunResult {
|
|
11
|
+
changes: number;
|
|
12
|
+
lastInsertRowid: number | bigint;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Driver-neutral SQL access for scenario-pack repositories.
|
|
16
|
+
*
|
|
17
|
+
* The access object carries scope but cannot safely rewrite arbitrary SQL.
|
|
18
|
+
* Every pack statement must include tenant_id and user_id predicates or values;
|
|
19
|
+
* drivers and source gates reject missing or mismatched ownership. The driver
|
|
20
|
+
* connection is never exposed.
|
|
21
|
+
*/
|
|
22
|
+
export interface PackDbAccess {
|
|
23
|
+
readonly scope: TenantScope;
|
|
24
|
+
get<T extends Record<string, unknown>>(sql: string, params?: PackDbParams): Promise<T | undefined>;
|
|
25
|
+
all<T extends Record<string, unknown>>(sql: string, params?: PackDbParams): Promise<T[]>;
|
|
26
|
+
run(sql: string, params?: PackDbParams): Promise<PackDbRunResult>;
|
|
27
|
+
exec(sql: string): Promise<void>;
|
|
28
|
+
transaction<T>(operation: (db: PackDbAccess) => Promise<T>): Promise<T>;
|
|
29
|
+
}
|
|
30
|
+
export type { ScopedStore } from './scoped-store.js';
|
|
31
|
+
/** Product-selectable host storage implementation. */
|
|
32
|
+
export interface StorageDriver {
|
|
33
|
+
initialize(): Promise<void>;
|
|
34
|
+
scoped(scope: TenantScope): ScopedStore;
|
|
35
|
+
packAccess(scope: TenantScope): PackDbAccess;
|
|
36
|
+
applyPackMigrations(): Promise<void>;
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export type StorageDriverFactory = () => StorageDriver | Promise<StorageDriver>;
|
|
40
|
+
/** Registers a storage driver. Product packs call this during composition. */
|
|
41
|
+
export declare function registerStorageDriver(id: string, factory: StorageDriverFactory, options?: {
|
|
42
|
+
default?: boolean;
|
|
43
|
+
}): () => void;
|
|
44
|
+
/** Selects a registered driver before storage initialization. */
|
|
45
|
+
export declare function selectStorageDriver(id: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Initializes storage after all product-pack registrations have run.
|
|
48
|
+
*
|
|
49
|
+
* Calls are idempotent and share one initialization promise.
|
|
50
|
+
*/
|
|
51
|
+
export declare function initializeStorage(): Promise<StorageDriver>;
|
|
52
|
+
/** Returns a scoped view after explicit storage initialization. */
|
|
53
|
+
export declare function getScopedStore(scope: TenantScope): ScopedStore;
|
|
54
|
+
/** Returns scope-bound scenario-pack access after initialization. */
|
|
55
|
+
export declare function getPackDbAccess(scope: TenantScope): PackDbAccess;
|
|
56
|
+
/** Closes the active driver. Mainly used by host shutdown and tests. */
|
|
57
|
+
export declare function closeStorage(): Promise<void>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Ownership used by the personal, single-user product. */
|
|
2
|
+
export const LOCAL_SCOPE = Object.freeze({
|
|
3
|
+
tenantId: 'local',
|
|
4
|
+
userId: 'local',
|
|
5
|
+
});
|
|
6
|
+
const DEFAULT_DRIVER_ID = 'sqlite';
|
|
7
|
+
const factories = new Map();
|
|
8
|
+
let selectedDriverId = DEFAULT_DRIVER_ID;
|
|
9
|
+
let activeDriver = null;
|
|
10
|
+
let initialization = null;
|
|
11
|
+
/** Registers a storage driver. Product packs call this during composition. */
|
|
12
|
+
export function registerStorageDriver(id, factory, options = {}) {
|
|
13
|
+
if (factories.has(id)) {
|
|
14
|
+
throw new Error(`[storage] duplicate driver registration: ${id}`);
|
|
15
|
+
}
|
|
16
|
+
if (activeDriver || initialization) {
|
|
17
|
+
throw new Error('[storage] drivers must be registered before initialization');
|
|
18
|
+
}
|
|
19
|
+
factories.set(id, factory);
|
|
20
|
+
if (options.default)
|
|
21
|
+
selectedDriverId = id;
|
|
22
|
+
return () => {
|
|
23
|
+
if (!activeDriver && !initialization) {
|
|
24
|
+
factories.delete(id);
|
|
25
|
+
if (selectedDriverId === id)
|
|
26
|
+
selectedDriverId = DEFAULT_DRIVER_ID;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Selects a registered driver before storage initialization. */
|
|
31
|
+
export function selectStorageDriver(id) {
|
|
32
|
+
if (activeDriver || initialization) {
|
|
33
|
+
throw new Error('[storage] driver selection is closed after initialization');
|
|
34
|
+
}
|
|
35
|
+
selectedDriverId = id;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Initializes storage after all product-pack registrations have run.
|
|
39
|
+
*
|
|
40
|
+
* Calls are idempotent and share one initialization promise.
|
|
41
|
+
*/
|
|
42
|
+
export function initializeStorage() {
|
|
43
|
+
if (activeDriver)
|
|
44
|
+
return Promise.resolve(activeDriver);
|
|
45
|
+
if (initialization)
|
|
46
|
+
return initialization;
|
|
47
|
+
const attempt = (async () => {
|
|
48
|
+
const factory = factories.get(selectedDriverId);
|
|
49
|
+
if (!factory) {
|
|
50
|
+
throw new Error(`[storage] unknown storage driver: ${selectedDriverId}`);
|
|
51
|
+
}
|
|
52
|
+
let driver = null;
|
|
53
|
+
try {
|
|
54
|
+
driver = await factory();
|
|
55
|
+
await driver.initialize();
|
|
56
|
+
await driver.applyPackMigrations();
|
|
57
|
+
activeDriver = driver;
|
|
58
|
+
return driver;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (driver) {
|
|
62
|
+
try {
|
|
63
|
+
await driver.close();
|
|
64
|
+
}
|
|
65
|
+
catch (_closeError) {
|
|
66
|
+
// Initialization error remains the actionable startup failure.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
initialization = attempt;
|
|
73
|
+
void attempt.catch(() => {
|
|
74
|
+
if (initialization === attempt)
|
|
75
|
+
initialization = null;
|
|
76
|
+
});
|
|
77
|
+
return attempt;
|
|
78
|
+
}
|
|
79
|
+
/** Returns a scoped view after explicit storage initialization. */
|
|
80
|
+
export function getScopedStore(scope) {
|
|
81
|
+
if (!activeDriver) {
|
|
82
|
+
throw new Error('[storage] initializeStorage() must complete before use');
|
|
83
|
+
}
|
|
84
|
+
return activeDriver.scoped(scope);
|
|
85
|
+
}
|
|
86
|
+
/** Returns scope-bound scenario-pack access after initialization. */
|
|
87
|
+
export function getPackDbAccess(scope) {
|
|
88
|
+
if (!activeDriver) {
|
|
89
|
+
throw new Error('[storage] initializeStorage() must complete before use');
|
|
90
|
+
}
|
|
91
|
+
return activeDriver.packAccess(scope);
|
|
92
|
+
}
|
|
93
|
+
/** Closes the active driver. Mainly used by host shutdown and tests. */
|
|
94
|
+
export async function closeStorage() {
|
|
95
|
+
const driver = activeDriver;
|
|
96
|
+
activeDriver = null;
|
|
97
|
+
initialization = null;
|
|
98
|
+
if (driver)
|
|
99
|
+
await driver.close();
|
|
100
|
+
}
|
|
101
|
+
registerStorageDriver(DEFAULT_DRIVER_ID, async () => {
|
|
102
|
+
const { SqliteStorageDriver } = await import('./sqlite-driver.js');
|
|
103
|
+
return new SqliteStorageDriver();
|
|
104
|
+
});
|