@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({ type: 'budget_exhausted', budget: { kind: budgetKind } }));
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
- * status——renderer 收到后重新拉任务列表(GET /chats/:id/tasks),
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" && typeof payload?.status === "string") {
132
- callback({ chatId: payload.chatId, taskId: payload.taskId, status: payload.status });
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
- * status——renderer 收到后重新拉任务列表(GET /chats/:id/tasks),
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
- typeof payload?.taskId === 'string' &&
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、无认证——本机自用定位(与 Electron 版相同
16
- * 的信任模型:能到端口 = 本机用户)。不要绑到非回环地址,那会把 shell /
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、无认证——本机自用定位(与 Electron 版相同
16
- * 的信任模型:能到端口 = 本机用户)。不要绑到非回环地址,那会把 shell /
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.18",
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/pack-sdk": "0.6.18",
45
- "@steerable/agent-harness": "0.6.18",
46
- "@steerable/agent-protocol": "0.6.18"
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",