@steerable/agent-shell 0.6.18 → 0.6.20
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.
|
@@ -2917,9 +2917,14 @@ export class LocalBackendRouter {
|
|
|
2917
2917
|
onNotice: (kind, notice) => {
|
|
2918
2918
|
if (kind === 'budget_exhausted') {
|
|
2919
2919
|
// 预算维度(rounds/tokens)在 notice.budget;notice.kind 是信封
|
|
2920
|
-
// 类型(loop 事件 data 里的 budget 键经 sidecar 透传)。
|
|
2920
|
+
// 类型(loop 事件 data 里的 budget 键经 sidecar 透传)。message
|
|
2921
|
+
// 是客户端读取的人类可读原因(chat-transport 的 reason 字段)。
|
|
2921
2922
|
const budgetKind = typeof notice?.budget === 'string' ? notice.budget : undefined;
|
|
2922
|
-
emit(this.sseData({
|
|
2923
|
+
emit(this.sseData({
|
|
2924
|
+
type: 'budget_exhausted',
|
|
2925
|
+
budget: { kind: budgetKind },
|
|
2926
|
+
message: budgetKind ? `budget_exhausted: ${budgetKind}` : 'budget_exhausted',
|
|
2927
|
+
}));
|
|
2923
2928
|
}
|
|
2924
2929
|
},
|
|
2925
2930
|
onChildEvent: (event) => {
|
package/dist/preload.cjs
CHANGED
|
@@ -122,14 +122,14 @@ function buildPreloadApi(packContributions = {}) {
|
|
|
122
122
|
},
|
|
123
123
|
/**
|
|
124
124
|
* 4.6a 后台任务状态推送:任务到达终态(completed/failed)或 worktree
|
|
125
|
-
* 合并/丢弃完成时,主进程广播 `task-updated`。载荷只带 chatId/taskId
|
|
126
|
-
*
|
|
125
|
+
* 合并/丢弃完成时,主进程广播 `task-updated`。载荷只带 chatId/taskId
|
|
126
|
+
* ——renderer 收到后重新拉任务列表(GET /chats/:id/tasks),
|
|
127
127
|
* 不把整条任务记录塞进 IPC(避免与 SQLite 双写漂移)。
|
|
128
128
|
*/
|
|
129
129
|
onTaskUpdated: (callback) => {
|
|
130
130
|
const handler = (_event, payload) => {
|
|
131
|
-
if (typeof payload?.chatId === "string" && typeof payload?.taskId === "string"
|
|
132
|
-
callback({ chatId: payload.chatId, taskId: payload.taskId
|
|
131
|
+
if (typeof payload?.chatId === "string" && typeof payload?.taskId === "string") {
|
|
132
|
+
callback({ chatId: payload.chatId, taskId: payload.taskId });
|
|
133
133
|
}
|
|
134
134
|
};
|
|
135
135
|
import_electron.ipcRenderer.on("task-updated", handler);
|
package/dist/preload.js
CHANGED
|
@@ -138,16 +138,14 @@ export function buildPreloadApi(packContributions = {}) {
|
|
|
138
138
|
},
|
|
139
139
|
/**
|
|
140
140
|
* 4.6a 后台任务状态推送:任务到达终态(completed/failed)或 worktree
|
|
141
|
-
* 合并/丢弃完成时,主进程广播 `task-updated`。载荷只带 chatId/taskId
|
|
142
|
-
*
|
|
141
|
+
* 合并/丢弃完成时,主进程广播 `task-updated`。载荷只带 chatId/taskId
|
|
142
|
+
* ——renderer 收到后重新拉任务列表(GET /chats/:id/tasks),
|
|
143
143
|
* 不把整条任务记录塞进 IPC(避免与 SQLite 双写漂移)。
|
|
144
144
|
*/
|
|
145
145
|
onTaskUpdated: (callback) => {
|
|
146
146
|
const handler = (_event, payload) => {
|
|
147
|
-
if (typeof payload?.chatId === 'string' &&
|
|
148
|
-
|
|
149
|
-
typeof payload?.status === 'string') {
|
|
150
|
-
callback({ chatId: payload.chatId, taskId: payload.taskId, status: payload.status });
|
|
147
|
+
if (typeof payload?.chatId === 'string' && typeof payload?.taskId === 'string') {
|
|
148
|
+
callback({ chatId: payload.chatId, taskId: payload.taskId });
|
|
151
149
|
}
|
|
152
150
|
};
|
|
153
151
|
ipcRenderer.on('task-updated', handler);
|
|
@@ -12,9 +12,16 @@
|
|
|
12
12
|
* (terminal / local / approval 为宿主自带;
|
|
13
13
|
* 场景包路由经 host/http-routes 注册表匹配)。
|
|
14
14
|
*
|
|
15
|
-
* 安全约定:默认绑定 127.0.0.1
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* 安全约定:默认绑定 127.0.0.1。仅绑回环挡不住 DNS rebinding(恶意网站
|
|
16
|
+
* 重绑定后受害者浏览器即同源驱动本服务),所以有两道门:
|
|
17
|
+
* 1. Host 白名单——所有请求(含静态)的 Host 必须是 127.0.0.1 /
|
|
18
|
+
* localhost / [::1],浏览器无法伪造 Host,rebinding 在此被拒;
|
|
19
|
+
* 2. Bearer token——每次启动随机生成,经 index.html 注入
|
|
20
|
+
* `window.__DEEPPATH_BS__.token`;`/api/v2/*` 与 `/host/*` 全部要求
|
|
21
|
+
* `Authorization: Bearer <token>`(EventSource 不能设头,SSE 走
|
|
22
|
+
* `?token=` query)。静态资源不验 token(浏览器要靠它拿到 token)。
|
|
23
|
+
* 不要绑到非回环地址,那会把 shell / 文件读写 / 场景包工具控制暴露给整个
|
|
24
|
+
* 网络(token 只能挡没有 token 的人)。
|
|
18
25
|
*/
|
|
19
26
|
import { type Server } from 'node:http';
|
|
20
27
|
import type { LocalBackendRouter } from '../local-backend/router.js';
|
|
@@ -35,5 +42,7 @@ export interface BsServerDeps {
|
|
|
35
42
|
bus: SseBus;
|
|
36
43
|
/** apps/web/dist 的绝对路径。 */
|
|
37
44
|
webDistDir: string;
|
|
45
|
+
/** Bearer token;缺省每次启动随机生成。测试注入固定值。 */
|
|
46
|
+
authToken?: string;
|
|
38
47
|
}
|
|
39
48
|
export declare function createBsServer(deps: BsServerDeps): Server;
|
|
@@ -12,11 +12,19 @@
|
|
|
12
12
|
* (terminal / local / approval 为宿主自带;
|
|
13
13
|
* 场景包路由经 host/http-routes 注册表匹配)。
|
|
14
14
|
*
|
|
15
|
-
* 安全约定:默认绑定 127.0.0.1
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* 安全约定:默认绑定 127.0.0.1。仅绑回环挡不住 DNS rebinding(恶意网站
|
|
16
|
+
* 重绑定后受害者浏览器即同源驱动本服务),所以有两道门:
|
|
17
|
+
* 1. Host 白名单——所有请求(含静态)的 Host 必须是 127.0.0.1 /
|
|
18
|
+
* localhost / [::1],浏览器无法伪造 Host,rebinding 在此被拒;
|
|
19
|
+
* 2. Bearer token——每次启动随机生成,经 index.html 注入
|
|
20
|
+
* `window.__DEEPPATH_BS__.token`;`/api/v2/*` 与 `/host/*` 全部要求
|
|
21
|
+
* `Authorization: Bearer <token>`(EventSource 不能设头,SSE 走
|
|
22
|
+
* `?token=` query)。静态资源不验 token(浏览器要靠它拿到 token)。
|
|
23
|
+
* 不要绑到非回环地址,那会把 shell / 文件读写 / 场景包工具控制暴露给整个
|
|
24
|
+
* 网络(token 只能挡没有 token 的人)。
|
|
18
25
|
*/
|
|
19
26
|
import { createServer } from 'node:http';
|
|
27
|
+
import { randomBytes } from 'node:crypto';
|
|
20
28
|
import { readFile } from 'node:fs/promises';
|
|
21
29
|
import { existsSync } from 'node:fs';
|
|
22
30
|
import path from 'node:path';
|
|
@@ -52,6 +60,8 @@ const MIME = {
|
|
|
52
60
|
'.ttf': 'font/ttf',
|
|
53
61
|
'.txt': 'text/plain; charset=utf-8',
|
|
54
62
|
};
|
|
63
|
+
/** Host 白名单:仅回环(可带端口)。浏览器禁止伪造 Host,rebinding 到此为止。 */
|
|
64
|
+
const LOOPBACK_HOST = /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/;
|
|
55
65
|
function sendJson(res, status, data) {
|
|
56
66
|
const body = JSON.stringify(data ?? null);
|
|
57
67
|
res.writeHead(status, {
|
|
@@ -81,6 +91,13 @@ function asRecord(body) {
|
|
|
81
91
|
}
|
|
82
92
|
export function createBsServer(deps) {
|
|
83
93
|
const { localBackendRouter, localExecutor, localScriptRegistry, terminalManager, approvalBridge, askUserBridge, maybeExecInTerminal, bus, webDistDir, } = deps;
|
|
94
|
+
const authToken = deps.authToken ?? randomBytes(24).toString('base64url');
|
|
95
|
+
/** `/api/v2/*` 与 `/host/*` 的 Bearer 门;EventSource 不能设头,收 query token。 */
|
|
96
|
+
function isAuthorized(req, url) {
|
|
97
|
+
if (req.headers.authorization === `Bearer ${authToken}`)
|
|
98
|
+
return true;
|
|
99
|
+
return url.searchParams.get('token') === authToken;
|
|
100
|
+
}
|
|
84
101
|
async function handleApi(req, res, pathname) {
|
|
85
102
|
const isStream = req.method === 'POST' && STREAM_PATH_PATTERNS.some((p) => p.test(pathname));
|
|
86
103
|
const body = req.method === 'POST' || req.method === 'PATCH' || req.method === 'PUT'
|
|
@@ -315,6 +332,7 @@ export function createBsServer(deps) {
|
|
|
315
332
|
platform: process.platform,
|
|
316
333
|
flavor: brand.flavor,
|
|
317
334
|
brandName: brand.displayName,
|
|
335
|
+
token: authToken,
|
|
318
336
|
})};</script>`;
|
|
319
337
|
content = content.toString('utf8').replace('<head>', `<head>${bootstrap}`);
|
|
320
338
|
}
|
|
@@ -331,8 +349,21 @@ export function createBsServer(deps) {
|
|
|
331
349
|
}
|
|
332
350
|
return createServer((req, res) => {
|
|
333
351
|
void (async () => {
|
|
352
|
+
// 门 1:Host 白名单(含静态——rebound 浏览器连 index.html 里的
|
|
353
|
+
// token 都不许拿到)。
|
|
354
|
+
if (!LOOPBACK_HOST.test(req.headers.host ?? '')) {
|
|
355
|
+
sendJson(res, 403, { detail: 'forbidden host' });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
334
358
|
const url = new URL(req.url ?? '/', 'http://bs.local');
|
|
335
359
|
const pathname = url.pathname;
|
|
360
|
+
// 门 2:API 与宿主能力要 Bearer token;静态资源不验(浏览器靠它
|
|
361
|
+
// 拿 token),静态面不暴露任何能力。
|
|
362
|
+
const needsAuth = pathname.startsWith('/api/v2/') || pathname.startsWith('/host/');
|
|
363
|
+
if (needsAuth && !isAuthorized(req, url)) {
|
|
364
|
+
sendJson(res, 401, { detail: 'unauthorized' });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
336
367
|
if (pathname === '/api/v2/events' && req.method === 'GET') {
|
|
337
368
|
bus.attach(res);
|
|
338
369
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.20",
|
|
4
4
|
"description": "Steerable framework — product-neutral desktop/headless agent host (Tier 5). Electron main + preload + headless HTTP server (BS) sharing one storage/sidecar/tooling core; scenario packs extend it through the @steerable/pack-sdk contract and are composed at build time by the consuming product.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"electron-store": "^8.1.0",
|
|
42
42
|
"he": "^1.2.0",
|
|
43
43
|
"node-pty": "^1.1.0",
|
|
44
|
-
"@steerable/
|
|
45
|
-
"@steerable/agent-harness": "0.6.
|
|
46
|
-
"@steerable/
|
|
44
|
+
"@steerable/agent-protocol": "0.6.20",
|
|
45
|
+
"@steerable/agent-harness": "0.6.20",
|
|
46
|
+
"@steerable/pack-sdk": "0.6.20"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/better-sqlite3": "^7.6.13",
|