@sema-agent/server 7.82.0 → 7.82.1
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/USAGE.md +17 -14
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.js +1 -1
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stage-10-http-server.js +2 -1
- package/dist/config-catalog.js +2 -2
- package/dist/http/routes/capabilities.js +2 -0
- package/dist/http/server.d.ts +8 -0
- package/dist/plugins/remote-env-adb.d.ts +62 -3
- package/dist/plugins/remote-env-adb.js +27 -15
- package/dist/plugins/remote-env-host.js +1 -1
- package/dist/plugins/remote-shell.d.ts +7 -11
- package/dist/plugins/remote-shell.js +3 -3
- package/dist/plugins/web-search.d.ts +50 -2
- package/dist/plugins/web-search.js +9 -2
- package/package.json +2 -2
package/USAGE.md
CHANGED
|
@@ -93,12 +93,12 @@ WEB_SEARCH_PROBE_ON_BOOT=true # boot 期一次性真出网探活,
|
|
|
93
93
|
|
|
94
94
|
| env 键 | 类型 | 默认 | 缺席行为 | 坏值行为 |
|
|
95
95
|
|---|---|---|---|---|
|
|
96
|
-
| `WEB_SEARCH_PROVIDER` | 闭集(`brave`\|`tavily`\|`searxng`) | 无 | WebSearch 工具整体不装配(功能缺席,无 warn) | 三词之外的任何值 = 同缺席处理,不装配、不 warn(
|
|
97
|
-
| `WEB_SEARCH_API_KEY` | secret string | 无 | brave/tavily:后端仍会装配(装配只看 `PROVIDER`),**首次真实工具调用**时抛 `WEB_SEARCH_API_KEY is required for the <provider> provider`(
|
|
98
|
-
| `WEB_SEARCH_ENDPOINT` | url string | brave/tavily → 各自官方 API;searxng → 无默认 | brave/tavily:落官方 endpoint;searxng:**首次真实工具调用**时抛 `WEB_SEARCH_ENDPOINT (the SearXNG instance URL) is required for the searxng provider`(
|
|
99
|
-
| `WEB_SEARCH_MAX_RESULTS` | number | `10` | 用默认 10 | 非数字/≤0 → 回落默认 10;是数字则向下取整;最终值再夹在 `[1,20]`(设 999 也被 clamp 到 20
|
|
100
|
-
| `WEB_SEARCH_TIMEOUT_MS` | number(ms) | `10000` | 用默认 10000 | 非数字/≤0 → 回落默认;最终值下限夹到 1000ms(`Math.max(1000
|
|
101
|
-
| `WEB_SEARCH_SEARXNG_PARAMS` | string(`k=v;k=v`) | 无(不铸键 = 不传 `extraParams`,交给 core adapter 自己的缺省) | 键整个不铸 | 一个 `k=v` 对都解析不出(没有 `=`,或 `k`/`v` 任一为空)→ 同缺席,整串忽略,不拒启不 warn(
|
|
96
|
+
| `WEB_SEARCH_PROVIDER` | 闭集(`brave`\|`tavily`\|`searxng`) | 无 | WebSearch 工具整体不装配(功能缺席,无 warn) | 三词之外的任何值 = 同缺席处理,不装配、不 warn(`webSearchConfigFromEnv` 的词表守卫 `isWebSearchProvider`)。**读面(S-382 起)**:`GET /v1/capabilities` 的 `webSearch.backend` 把这一格的生效值广告成闭集词(缺席/坏值都报 `"none"` —— 两者行为本就相同);端点/密钥/配额**不上 wire**,那些仍只在 operator 面 `GET /v1/config/catalog` |
|
|
97
|
+
| `WEB_SEARCH_API_KEY` | secret string | 无 | brave/tavily:后端仍会装配(装配只看 `PROVIDER`),**首次真实工具调用**时抛 `WEB_SEARCH_API_KEY is required for the <provider> provider`(`braveSearch` / `tavilySearch` 的首行守卫);searxng:本键无消费点 | 空字符串同缺席(`env.WEB_SEARCH_API_KEY ?` 只认真值,`webSearchConfigFromEnv` 的条件展开) |
|
|
98
|
+
| `WEB_SEARCH_ENDPOINT` | url string | brave/tavily → 各自官方 API;searxng → 无默认 | brave/tavily:落官方 endpoint;searxng:**首次真实工具调用**时抛 `WEB_SEARCH_ENDPOINT (the SearXNG instance URL) is required for the searxng provider`(`searxngSearch` 的首行守卫) | 不做 URL 形校验——写不成 URL 由 `new URL()` 抛出,同样落到"首次调用才现形"那条路径,不拒启 |
|
|
99
|
+
| `WEB_SEARCH_MAX_RESULTS` | number | `10` | 用默认 10 | 非数字/≤0 → 回落默认 10;是数字则向下取整;最终值再夹在 `[1,20]`(设 999 也被 clamp 到 20 —— `createWebSearchBackend` 里的 `maxResults` clamp) |
|
|
100
|
+
| `WEB_SEARCH_TIMEOUT_MS` | number(ms) | `10000` | 用默认 10000 | 非数字/≤0 → 回落默认;最终值下限夹到 1000ms(`createWebSearchBackend` 里的 `Math.max(1000, …)`) |
|
|
101
|
+
| `WEB_SEARCH_SEARXNG_PARAMS` | string(`k=v;k=v`) | 无(不铸键 = 不传 `extraParams`,交给 core adapter 自己的缺省) | 键整个不铸 | 一个 `k=v` 对都解析不出(没有 `=`,或 `k`/`v` 任一为空)→ 同缺席,整串忽略,不拒启不 warn(`parseSearxngParams`);env 侧值恒为字符串,不会触发下面 per-request 那个"数组被误当对象吸收"的边角(见 §9.5) |
|
|
102
102
|
| `WEB_SEARCH_PROBE_ON_BOOT` | boolean(仅认 `"true"`/`"1"`) | `false`(OFF) | 不探活 | trim+小写后不等于 `"true"`/`"1"` 的任何拼法(含 `"yes"`/`"on"`)一律当 `false`;探活失败(网络不通/后端拒绝)只 `warn`(`web_search_probe_failed`),**不拒启**——功能型能力缺席走降级,不是保护型旋钮(`main.ts:1028-1034`) |
|
|
103
103
|
|
|
104
104
|
**结论:七键无一在 boot 期拒启。** 与本仓其它安全轴旋钮(`MODEL_DEGRADE_ON`、思考档三键等,拼错即拒启)
|
|
@@ -1542,6 +1542,9 @@ const webSearch = reqWebSearch ? createWebSearchBackend(reqWebSearch) : deps.web
|
|
|
1542
1542
|
TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结构性够不着**——不是被拒、也不留任何
|
|
1543
1543
|
`warn`/`capabilities` 位说"你发的 webSearch 被忽略了"(对比 `mcpServers` 有 `capabilities.mcpInjection` +
|
|
1544
1544
|
`mcp_injection_dropped` 日志可读;`settings.webSearch` 没有对应的能力探测位,`src/http/wire-types.ts:320-323`)。
|
|
1545
|
+
🔴 **别把 S-382 的新位读成这个探测位**:`GET /v1/capabilities` 的 `webSearch.backend`(S-382)说的是
|
|
1546
|
+
**部署默认后端是哪一只**,**不是**「我发的 per-request `settings.webSearch` 会不会被采纳」——后者在多租户上
|
|
1547
|
+
依旧是结构性够不着、无 warn、无位,这一段的结论一字未变。
|
|
1545
1548
|
原因是安全边界(与多租户能力配置隔离规则同源):per-request `endpoint`/`apiKey` 是能力配置,多租户下一个租户把 `searxng`
|
|
1546
1549
|
指向内网地址就是 SSRF,所以多租户上只认部署 env 配的 backend。
|
|
1547
1550
|
|
|
@@ -1550,11 +1553,11 @@ TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结
|
|
|
1550
1553
|
|
|
1551
1554
|
| 键 | 类型 | 必填 | 默认 | 消费点 |
|
|
1552
1555
|
|---|---|---|---|---|
|
|
1553
|
-
| `provider` | `"brave"|"tavily"|"searxng"` | 是(缺席/非三词之一 ⇒ 整个 `settings.webSearch` 被丢,回落到 env 的 backend,**不报错**) | 无 |
|
|
1554
|
-
| `apiKey` | string(明文) | brave/tavily 建议带(缺了首次调用才报错,见下表);searxng 不读 | 无(不继承 env 的 `WEB_SEARCH_API_KEY`) |
|
|
1555
|
-
| `endpoint` | string | searxng 必需(缺了首次调用才报错);brave/tavily 可选覆盖 | 无(不继承 env 的 `WEB_SEARCH_ENDPOINT`) |
|
|
1556
|
-
| `searxngParams` | `Record<string,string>`(对象)或 `"k=v;k=v"`(字符串) | 否 | 无 |
|
|
1557
|
-
| `maxResults` | number | 否 | 10(与 env 同一 clamp,`[1,20]`,小数向下取整) |
|
|
1556
|
+
| `provider` | `"brave"|"tavily"|"searxng"` | 是(缺席/非三词之一 ⇒ 整个 `settings.webSearch` 被丢,回落到 env 的 backend,**不报错**) | 无 | `webSearchConfigFromSettings` 的词表守卫(与 env 腿**同一只** `isWebSearchProvider`,词表属主 = `WEB_SEARCH_PROVIDERS`) |
|
|
1557
|
+
| `apiKey` | string(明文) | brave/tavily 建议带(缺了首次调用才报错,见下表);searxng 不读 | 无(不继承 env 的 `WEB_SEARCH_API_KEY`) | `webSearchConfigFromSettings` 的 `apiKey` 条件展开 |
|
|
1558
|
+
| `endpoint` | string | searxng 必需(缺了首次调用才报错);brave/tavily 可选覆盖 | 无(不继承 env 的 `WEB_SEARCH_ENDPOINT`) | `webSearchConfigFromSettings` 的 `endpoint` 条件展开 |
|
|
1559
|
+
| `searxngParams` | `Record<string,string>`(对象)或 `"k=v;k=v"`(字符串) | 否 | 无 | `webSearchConfigFromSettings` 的 `searxngParams` 臂,解析逻辑与 env 腿共用 `parseSearxngParams`。⚠️ **未列入已发布的 `@sema-agent/sdk` 8.8.0 `SettingsWebSearch` 类型**(该接口只有 `provider`/`apiKey`/`endpoint`/`maxResults` 四键、无开放下标)——server 侧代码认这个键,但当前发布的 TS 类型接不到它;手写 JSON 请求体仍可以发,server 会照常解析(源码头注自述:字段和消费点先落地,配置录入面尚未跟上,是一笔尚未还清的既有债务) |
|
|
1560
|
+
| `maxResults` | number | 否 | 10(与 env 同一 clamp,`[1,20]`,小数向下取整) | `webSearchConfigFromSettings` 的 `maxResults` 归一 |
|
|
1558
1561
|
| *(无)* `timeoutMs` | — | — | — | **per-request 车道没有这个字段**——即便部署用 `WEB_SEARCH_TIMEOUT_MS` 配了非默认超时,per-request 生效那次调用永远退回 backend 自己的默认(10000ms,下限 1000ms),因为"整段整取"意味着 env 的 `timeoutMs` 根本不在 `reqWebSearch` 那个新对象里 |
|
|
1559
1562
|
| *(无)* `fetchImpl` | — | — | — | 同上,仅测试注入用,per-request 车道不可达 |
|
|
1560
1563
|
|
|
@@ -1575,7 +1578,7 @@ TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结
|
|
|
1575
1578
|
| 缺 `apiKey`(brave/tavily,env 或 per-request 均未给) | 首次调用抛 `WEB_SEARCH_API_KEY is required for the <provider> provider`,core 接住转成 `Error (WebSearch): the search backend failed. …` 文本回模型 | `"unknown"`(消息里没有 HTTP 状态码模式,`classifySearchFailure` 落最后一条默认分支) |
|
|
1576
1579
|
| `searxngParams` 非对象/非字符串(如数字、布尔、`null`) | 静默丢弃——`parseSearxngParams` 落到"非 object 且非 string"分支,返回 `undefined`,键整个不铸,**不报错、不 warn** | 不适用 |
|
|
1577
1580
|
| ⚠️ `searxngParams` 是**数组**(如 `["engines=bing"]`) | `typeof [] === "object"` 让它落进对象分支:`Object.entries` 按数组下标产出 `{"0":"engines=bing"}`——**不被拒绝**,但产出的键是数字字符串、值是未拆分的原始 `"k=v"` 串,传给 core adapter 的 `extraParams` 后是一组没有意义的查询参数(不是解析出 `engines=bing`)。这条边角只在 per-request 车道可达(env 值恒为字符串,不会触发);已用与源码逐字一致的独立复现脚本核验(见收车档),未改代码 | 不适用(不是异常路径) |
|
|
1578
|
-
| 搜索后端返回**非 2xx HTTP 响应**(鉴权失败/限流/服务端 5xx 等) | 三个 provider 各自的 `!res.ok` 分支抛错,消息形固定为 `"<provider> search failed (<status>): <body首 200 字符>"`(brave/tavily
|
|
1581
|
+
| 搜索后端返回**非 2xx HTTP 响应**(鉴权失败/限流/服务端 5xx 等) | 三个 provider 各自的 `!res.ok` 分支抛错,消息形固定为 `"<provider> search failed (<status>): <body首 200 字符>"`(brave/tavily,`braveSearch` / `tavilySearch` 的 `!res.ok` 分支)或 `"SearXNG <status> <statusText> from <url>"`(searxng,core adapter) | ⚠️ **实测几乎恒为 `"unknown"`,不是按状态码分档**:`classifySearchFailure` 的状态码分支要求 `http`/`status`/`code`/`error` 四词之一紧邻数字前(`\D{0,12}`内),但本仓三个 adapter 的消息把状态码写在 `failed (…)`/`SearXNG …` 之后,不触发该分支;独立复现脚本核验(见收车档)brave/tavily/searxng 的 429/500/403/403 全部落到最后一条默认分支 `retryable:"unknown"`。**真正被分类对的只有两条兜底正则**:上游错误体文本里若真含 `"rate limit"` 才判 429 类 `true`,含 `"timed out"`/`"timeout"` 才判 408 类 `true`——都取决于上游返回的具体措辞,不是本仓能保证的 |
|
|
1579
1582
|
| `endpoint` **网络层**不可达(连接被拒/DNS 解析失败/fetch 自身抛错,尚未拿到任何 HTTP 响应) | `fetch` 抛出的传输层错误(`ECONNREFUSED`/`ENOTFOUND`/`fetch failed` 等 Node/undici 标准措辞)被同一 `catch` 接住 | `true`——这条路径的错误文本天然含 `econnrefused`/`enotfound`/`fetch failed`/`dns` 等词,`classifySearchFailure` 的网络故障正则能命中(与上一行"已拿到 HTTP 响应但非 2xx"是两条不同的失败路径,别混淆) |
|
|
1580
1583
|
|
|
1581
1584
|
⚠️ **"坏 provider 静默回落"这条对调用方是否可观察,取决于部署 env 有没有配 backend**:上表第一行说
|
|
@@ -1588,8 +1591,8 @@ TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结
|
|
|
1588
1591
|
见收车档)。
|
|
1589
1592
|
|
|
1590
1593
|
**`apiKey` 明文与 env 槽边界**:server 收到的 `settings.webSearch.apiKey` 是**明文字符串**,没有任何服务端
|
|
1591
|
-
密钥槽位/引用间接——收到什么字符串就直接进 backend 闭包(
|
|
1592
|
-
`src/plugins/web-search.ts
|
|
1594
|
+
密钥槽位/引用间接——收到什么字符串就直接进 backend 闭包(`webSearchConfigFromSettings` 的 `apiKey` 条件展开,与 `WEB_SEARCH_API_KEY` 同一条消费路径,
|
|
1595
|
+
见 `src/plugins/web-search.ts` 的 `WebSearchBackendConfig.apiKey` 字段注)。**server 本批不改受理面**:明文字段的形状维持原样。调用方(壳)如何在
|
|
1593
1596
|
自己机器上管理这份明文是调用方的事——例如 cli 壳侧的约定是在**调用方自己的环境**里按
|
|
1594
1597
|
`SEMA_WEBSEARCH_KEY_<PROVIDER>` 这样的命名空间存放每个 provider 的 key,由壳在本地读出后把明文塞进请求体;
|
|
1595
1598
|
这纯粹是**客户端约定**,本仓不读取、不校验、也不感知任何这类客户端侧 env 命名(这条命名事实来自任务书,
|
|
@@ -24,6 +24,7 @@ export declare function runStage07(ctx: Awaited<ReturnType<typeof runStage06>>):
|
|
|
24
24
|
maxTurns: number | undefined;
|
|
25
25
|
};
|
|
26
26
|
taskTimeoutSec: number;
|
|
27
|
+
webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
|
|
27
28
|
approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
|
|
28
29
|
centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
|
|
29
30
|
checkpointStore: import("../plugins/store-backend.js").CheckpointStoreFull | undefined;
|
|
@@ -158,6 +158,6 @@ export async function runStage07(ctx) {
|
|
|
158
158
|
const builtinScenarioNames = Object.keys(scenarioDetails);
|
|
159
159
|
configCenter.applyCenterScenarios({ scenarioDeps, scenarios, scenarioDetails, builtinScenarioNames });
|
|
160
160
|
logger.info("capabilities", { skills: skills.length, scenarios: Object.keys(scenarios), repoTools: Boolean(repoClient) });
|
|
161
|
-
return { ...ctx, approvalExemptionStore, approvalNonceStore, authorize, imageBakes, imageIndex, parkedReviveInheritedGate, parkedReviveTool, rateLimiter, resumeAnchorStore, runStore, scenarioDetails, scenarios, selectEnvTool, singleUserAutoAcceptBaseline, taskLimitCaps, taskTimeoutSec };
|
|
161
|
+
return { ...ctx, approvalExemptionStore, approvalNonceStore, authorize, imageBakes, imageIndex, parkedReviveInheritedGate, parkedReviveTool, rateLimiter, resumeAnchorStore, runStore, scenarioDetails, scenarios, selectEnvTool, singleUserAutoAcceptBaseline, taskLimitCaps, taskTimeoutSec, webSearchProvider: webSearchCfg?.provider };
|
|
162
162
|
}
|
|
163
163
|
//# sourceMappingURL=stage-07-capability-layer.js.map
|
|
@@ -37,6 +37,7 @@ export declare function runStage08(ctx: Awaited<ReturnType<typeof runStage07>>):
|
|
|
37
37
|
maxTurns: number | undefined;
|
|
38
38
|
};
|
|
39
39
|
taskTimeoutSec: number;
|
|
40
|
+
webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
|
|
40
41
|
approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
|
|
41
42
|
centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
|
|
42
43
|
checkpointStore: import("../plugins/store-backend.js").CheckpointStoreFull | undefined;
|
|
@@ -71,6 +71,7 @@ export declare function runStage09(ctx: Awaited<ReturnType<typeof runStage08>>):
|
|
|
71
71
|
maxTurns: number | undefined;
|
|
72
72
|
};
|
|
73
73
|
taskTimeoutSec: number;
|
|
74
|
+
webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
|
|
74
75
|
approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
|
|
75
76
|
centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
|
|
76
77
|
checkpointStore: import("../plugins/store-backend.js").CheckpointStoreFull | undefined;
|
|
@@ -71,6 +71,7 @@ export declare function runStage10(ctx: Awaited<ReturnType<typeof runStage09>>):
|
|
|
71
71
|
maxTurns: number | undefined;
|
|
72
72
|
};
|
|
73
73
|
taskTimeoutSec: number;
|
|
74
|
+
webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
|
|
74
75
|
approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
|
|
75
76
|
centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
|
|
76
77
|
checkpointStore: import("../plugins/store-backend.js").CheckpointStoreFull | undefined;
|
|
@@ -22,7 +22,7 @@ import { installShutdownHandlers } from "./shutdown.js";
|
|
|
22
22
|
import { bindServerLateSeats } from "./late-binds.js";
|
|
23
23
|
import { serviceVersion } from "../service-version.js";
|
|
24
24
|
export async function runStage10(ctx) {
|
|
25
|
-
const { approvalAskAudit, approvalExemptionStore, approvalNonceStore, authorize, backend, backgroundAgentStore, brain, breakerState, centerRuntimeCapsResolver, checkpointStore, config, configCenter, costQuota, deploymentHooks, deviceLane, drainState, durableEnabled, elicitation, fenceSessionOwner, fileHistoryStore, fleetBus, fleetLease, fleetUsage, governanceSeams, handslessRunner, hookAgent, hookLlm, hookWakeBus, imageBakes, imageIndex, instanceId, instrumentDegenerate, leaderEndpoint, localRoot, logger, lspManager, mcpRevocations, memoryBundleFaces, memoryComplianceFaces, memoryConsolidationFaces, memoryEngine, memoryExportBackend, memoryOptOutGrantStore, memoryOriginFaces, memoryPosture, memorySyncCursors, metrics, modelUsageTracker, orgMemoryAdmission, otelExporter, outcomeSink, ownerAware, parkedReviveInheritedGate, parkedReviveTool, perSessionCwd, perTaskImage, planCacheProbe, pricing, principalCaps, promptManifestTracker, purgeSession, question, rateLimiter, reaper, registryJwtVerifier, resumeAnchorStore, retentionLaneStarted, retentionLaneStore, ruleConsent, runStore, runner, runnerDeps, runtimeCapsPeekResolver, scenarioDetails, scenarios, selectEnvTool, sendFileLedger, sendUserFileEmitter, sendUserFileToolSpec, sessionAudit, sessionEnvSelection, sessionMemoryStatusFace, sessionPolicyStore, sessionShellGateRegistry, sessionTitler, sessionWatchRegistry, setSessionCwd, setSessionShellEnv, sharedMemoryStore, sideQueryAccounting, singleUserAutoAcceptBaseline, sqlEngineFacts, sqlWorkflowRunStore, staticWiring, storeBackendDegraded, subRunner, subagentSteerRegistry, taskAttachmentStore, taskLimitCaps, taskTimeoutSec, toolApproval, toolResultStore, usageWindowStore, workflowAgentRegistry, workflowAgentSessionIndex, workflowCompletionInbox, workflowJournalStore, workflowModelAllowlistSnapshot, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, workflowRunStore, workflowsCapable } = ctx;
|
|
25
|
+
const { approvalAskAudit, approvalExemptionStore, approvalNonceStore, authorize, backend, backgroundAgentStore, brain, breakerState, centerRuntimeCapsResolver, checkpointStore, config, configCenter, costQuota, deploymentHooks, deviceLane, drainState, durableEnabled, elicitation, fenceSessionOwner, fileHistoryStore, fleetBus, fleetLease, fleetUsage, governanceSeams, handslessRunner, hookAgent, hookLlm, hookWakeBus, imageBakes, imageIndex, instanceId, instrumentDegenerate, leaderEndpoint, localRoot, logger, lspManager, mcpRevocations, memoryBundleFaces, memoryComplianceFaces, memoryConsolidationFaces, memoryEngine, memoryExportBackend, memoryOptOutGrantStore, memoryOriginFaces, memoryPosture, memorySyncCursors, metrics, modelUsageTracker, orgMemoryAdmission, otelExporter, outcomeSink, ownerAware, parkedReviveInheritedGate, parkedReviveTool, perSessionCwd, perTaskImage, planCacheProbe, pricing, principalCaps, promptManifestTracker, purgeSession, question, rateLimiter, reaper, registryJwtVerifier, resumeAnchorStore, retentionLaneStarted, retentionLaneStore, ruleConsent, runStore, runner, runnerDeps, runtimeCapsPeekResolver, scenarioDetails, scenarios, selectEnvTool, sendFileLedger, sendUserFileEmitter, sendUserFileToolSpec, sessionAudit, sessionEnvSelection, sessionMemoryStatusFace, sessionPolicyStore, sessionShellGateRegistry, sessionTitler, sessionWatchRegistry, setSessionCwd, setSessionShellEnv, sharedMemoryStore, sideQueryAccounting, singleUserAutoAcceptBaseline, sqlEngineFacts, sqlWorkflowRunStore, staticWiring, storeBackendDegraded, subRunner, subagentSteerRegistry, taskAttachmentStore, taskLimitCaps, taskTimeoutSec, toolApproval, toolResultStore, usageWindowStore, webSearchProvider, workflowAgentRegistry, workflowAgentSessionIndex, workflowCompletionInbox, workflowJournalStore, workflowModelAllowlistSnapshot, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, workflowRunStore, workflowsCapable } = ctx;
|
|
26
26
|
const stores = {
|
|
27
27
|
runStore,
|
|
28
28
|
resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined,
|
|
@@ -133,6 +133,7 @@ export async function runStage10(ctx) {
|
|
|
133
133
|
memoryPosture,
|
|
134
134
|
sqlEngineFacts,
|
|
135
135
|
writeProtection: buildWriteProtectionPosture(config),
|
|
136
|
+
webSearchProvider,
|
|
136
137
|
instanceId,
|
|
137
138
|
capabilities: {
|
|
138
139
|
version: serviceVersion(),
|
package/dist/config-catalog.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES } from "@sema-agent/core";
|
|
2
2
|
import { THINKING_FORMATS } from "./model-compat.js";
|
|
3
3
|
import { RESILIENCE_DEFAULTS } from "./config-types.js";
|
|
4
|
-
import { webSearchConfigFromEnv } from "./plugins/web-search.js";
|
|
4
|
+
import { webSearchConfigFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-search.js";
|
|
5
5
|
import { CENTER_READ_FACE_LIVE_KEYS } from "./config-center/read-face.js";
|
|
6
6
|
export const CATALOG_VALUE_TYPES = ["boolean", "number", "string", "enum", "csv", "json", "path", "url", "secret", "opaque"];
|
|
7
7
|
export const CATALOG_TIMINGS = ["hot", "restart", "deploy", "inert"];
|
|
@@ -447,7 +447,7 @@ export const CONFIG_CATALOG = [
|
|
|
447
447
|
r("WEB_SEARCH_ENDPOINT", "orchestration", "url", "web search 后端 endpoint 覆写(searxng 自建面)"),
|
|
448
448
|
r("WEB_SEARCH_MAX_RESULTS", "orchestration", "number", "web search 单次结果数上限", { derivedDefaultNote: "缺席/非正 ⇒ backend 默认" }),
|
|
449
449
|
r("WEB_SEARCH_PROBE_ON_BOOT", "orchestration", "boolean", "boot 期 web search 连通性探测(只认 true/1;含糊值当没开——猜错方向是凭空出网)", { staticDefault: "false" }),
|
|
450
|
-
r("WEB_SEARCH_PROVIDER", "orchestration", "enum", "web search 工具后端(设合法词=装配 WebSearch;其它/缺席=不装配)", { enumValues:
|
|
450
|
+
r("WEB_SEARCH_PROVIDER", "orchestration", "enum", "web search 工具后端(设合法词=装配 WebSearch;其它/缺席=不装配)", { enumValues: WEB_SEARCH_PROVIDERS, danger: SEC, resolve: (_c, env) => webSearchConfigFromEnv(env)?.provider ?? null }),
|
|
451
451
|
r("WEB_SEARCH_SEARXNG_PARAMS", "orchestration", "string", "searxng 额外查询参数(k=v 对;解析不出任何一对 ⇒ 键不铸)"),
|
|
452
452
|
r("WEB_SEARCH_TIMEOUT_MS", "orchestration", "number", "web search 单次超时(ms)", { derivedDefaultNote: "缺席/非正 ⇒ backend 默认" }),
|
|
453
453
|
r("WORKFLOW_AGENTS_READONLY", "orchestration", "boolean", "workflow 子 agent 只读钳(TOB 保守旋钮;默认同权 CC parity)", { staticDefault: "false", danger: SEC, configKey: "workflowAgentsReadOnly" }),
|
|
@@ -12,6 +12,7 @@ import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
|
12
12
|
import { projectMemoryEngineCapability } from "../../memory-posture.js";
|
|
13
13
|
import { projectWriteProtectionCapability } from "../../write-protection.js";
|
|
14
14
|
import { projectSqlEngineCapability } from "../../sql-engine-posture.js";
|
|
15
|
+
import { projectWebSearchCapability } from "../../plugins/web-search.js";
|
|
15
16
|
import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-workflow.js";
|
|
16
17
|
import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
|
|
17
18
|
import { judgeAutoModeArming } from "../../auto-mode-face.js";
|
|
@@ -76,6 +77,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
76
77
|
memoryWrite: false,
|
|
77
78
|
memoryEngine: projectMemoryEngineCapability(deps.memoryPosture),
|
|
78
79
|
sql: projectSqlEngineCapability(deps.sqlEngineFacts?.()),
|
|
80
|
+
webSearch: projectWebSearchCapability(deps.webSearchProvider),
|
|
79
81
|
sessionBackgroundable: Boolean(deps.runStore),
|
|
80
82
|
runMemoryCaptureOptOut: Boolean(deps.runStore),
|
|
81
83
|
sharedMemory: Boolean(deps.sharedMemoryStore && deps.orgMemoryDirectory),
|
package/dist/http/server.d.ts
CHANGED
|
@@ -689,6 +689,14 @@ export interface ServiceDeploymentDeps {
|
|
|
689
689
|
* boot 期算一次的**存活对象**(纯 config 派生,运行期不变;core 座没有热改腿)。
|
|
690
690
|
* 缺席 = 这个进程不是由 composition root 装起来的(测试夹具形)⇒ 诊断面报 null、能力位报 null。 */
|
|
691
691
|
writeProtection?: import("../write-protection.js").WriteProtectionPosture;
|
|
692
|
+
/** S-382:**部署侧 WebSearch 后端**(`WEB_SEARCH_PROVIDER`)—— `GET /v1/capabilities` 的 `webSearch` 位的
|
|
693
|
+
* 唯一席位。值**逐字**取自 boot 期真正拿去造后端的那份 `webSearchConfigFromEnv()` 产物
|
|
694
|
+
* (`boot/stage-07-capability-layer.ts`),所以「能力位说 brave、装配里装的是 tavily」结构上不可能。
|
|
695
|
+
* 缺席 = 本部署没有搜索后端(未设 / 拼错 / 装配没喂席位)⇒ 窄投影报 `{backend:"none"}`;
|
|
696
|
+
* 折词的属主与全部判据在 `plugins/web-search.ts` 的 `projectWebSearchCapability` 头注。
|
|
697
|
+
* ⚠️ 席位只承载**部署默认**:单用户车道上每请求的 `settings.webSearch` 压过它,但那条腿**不经过本席**
|
|
698
|
+
* (它在场景装配里每请求现算),所以本位不会随调用方漂。 */
|
|
699
|
+
webSearchProvider?: import("../plugins/web-search.js").WebSearchProvider;
|
|
692
700
|
/** ① core ruling — SPLIT: `capabilities.workflows` = the ENGINE-CAN axis, boot-computed from core's own
|
|
693
701
|
* `workflowsCapability(deps)` (hardened script runner ∧ governance), NOT the `Boolean(workflowRunStore)` store
|
|
694
702
|
* proxy. Orthogonal to `workflowsList` (the durable-list axis = `workflowRunStore`). Falls back to the store
|
|
@@ -41,7 +41,23 @@ export interface AdbEnvDeps {
|
|
|
41
41
|
connectOnce?: (signal?: AbortSignal) => Promise<ConnectResult>;
|
|
42
42
|
}
|
|
43
43
|
/** [R78]#1: adb stderr markers that mean the DEVICE TRANSPORT died mid-op (not a normal command failure) —
|
|
44
|
-
* typed retryable (reconnect + retry per idempotency), never a fake command result. Exported for tests.
|
|
44
|
+
* typed retryable (reconnect + retry per idempotency), never a fake command result. Exported for tests.
|
|
45
|
+
*
|
|
46
|
+
* 🔴 **S-401 把这个判据的输入面改了一半(复审 F12 纠正:原来写的「此前 stderr 恒空 ⇒ 整条是死码」是
|
|
47
|
+
* **overclaim**,被本车自己的真机读数推翻)**。`exec-out` 合流的是**设备**的 stderr;**adb 客户端自己的**
|
|
48
|
+
* 诊断在两种传输下都照样走宿主 stderr。真机实测(同一台设备,拔掉 serial):
|
|
49
|
+
* `adb -s BOGUS exec-out 'true'` ⇒ host exit **255** + stderr `error: device 'BOGUS' not found`;
|
|
50
|
+
* `adb -s BOGUS shell -T -- 'true'` ⇒ host exit **1** + stderr `adb: device 'BOGUS' not found`。
|
|
51
|
+
* ⇒ 本判据在 S-401 之前**对 adb 客户端级的传输故障是工作的**(这也正是原作者把词表写成这些 adb 自家
|
|
52
|
+
* 措辞的原因);S-401 改变的是**设备侧 stderr 从此也进这条通道**。
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ **随之而来的、如实登记的一格**:分流之后落到这里的 stderr **既有 adb 自己的诊断,也有设备命令自己的
|
|
55
|
+
* 输出**(adb 客户端把两者写在同一个 fd 上,真机实测无法在传输层分开)。于是设备上一条自己打印
|
|
56
|
+
* `Connection reset by peer` 的命令(curl/nc)会被判成 `transport_lost`:本次命令结果不返回、`connected`
|
|
57
|
+
* 置假、下一次 exec fail-closed 直到 reconnect。**刻意不收窄词表**——词表收窄的方向是「漏判一次真掉线」,
|
|
58
|
+
* 那是 fail-open;误判的方向是多断一次连接,是 fail-closed。[ref] 轴上后者必须赢(能力探测的失败方向要与
|
|
59
|
+
* 能力语义方向一致)。ssh / k8s 两腿不吃这一格,因为它们的传输层自己给「通道关闭却没有 exit status」这个
|
|
60
|
+
* **结构**信号;adb 的 shell v2 没有等价物,所以这里只能靠标记词,**这条臂就把话说在这里**。 */
|
|
45
61
|
export declare function isAdbTransportLost(stderr: string): boolean;
|
|
46
62
|
export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
47
63
|
/**
|
|
@@ -123,8 +139,15 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
123
139
|
execStream(command: string, options?: ExecStreamOptions): AsyncIterable<OutputChunk>;
|
|
124
140
|
absolutePath(p: string): Promise<Result<string, FileError>>;
|
|
125
141
|
joinPath(parts: string[]): Promise<Result<string, FileError>>;
|
|
126
|
-
/**
|
|
127
|
-
*
|
|
142
|
+
/** 文件面的门 —— 与 host/local-docker/k8s/e2b 的姿态对齐(它们的文件面在未连/已拆时都 fail-closed)。
|
|
143
|
+
*
|
|
144
|
+
* 🔴 **S-401 复审 F2:这道门此前只查 `destroyed`,不查 `connected`**,于是 `readBinaryFile` /
|
|
145
|
+
* `readTextFile` / `readTextLines` / `appendFile` 能在**从没连过**的 env 上直接打一次真设备往返。
|
|
146
|
+
* 在本批之前那只是个不一致;本批**新开的 `shell_v2` fail-closed 闸就长在 `connect()` 里**,
|
|
147
|
+
* 所以「不查 connected」= 那道闸对整个读面**不生效** —— 而 `readTextFile` 把错误文本当文件内容返回
|
|
148
|
+
* 正是本 P1 点名的症状之一。闸的射程必须覆盖它要保护的每一个面,否则它只是一句话。
|
|
149
|
+
* ⇒ 规则收成一条:**这只 env 在 connect 成功之前、destroy 之后,文件面一个字都不答。**
|
|
150
|
+
* (`exec`/`execStream` 本就是这条规则,见 `:257`/`:276`;文件面从此同判,不再是两套。) */
|
|
128
151
|
private fsGate;
|
|
129
152
|
readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
|
|
130
153
|
readTextFile(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
@@ -162,6 +185,42 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
162
185
|
}): Promise<Result<string, FileError>>;
|
|
163
186
|
/** Spawn the adb binary with the given args; collect stdout/stderr (+ raw bytes when binary), bound by timeout/abort. */
|
|
164
187
|
private adb;
|
|
188
|
+
/**
|
|
189
|
+
* 🔴 **S-401 —— 本适配器跑设备命令的唯一往返形**(`exec` / `execStream` / `readBinaryFile` 三个调用点
|
|
190
|
+
* 共用一只;此前 `exec-out` 写了三遍,于是「传输到底给不给退出码」这件事没有属主)。
|
|
191
|
+
*
|
|
192
|
+
* **`adb shell`(shell protocol v2)而不是 `adb exec-out`。** 协议出处 = adb 自己的 `adb help`:
|
|
193
|
+
* `-x: disable remote exit codes and stdout/stderr separation` —— 即**不带** `-x` 的 `adb shell` 就是
|
|
194
|
+
* 「远端退出码 + stdout/stderr 分流」那一形。`exec-out` 走的是另一条 `exec:` 服务:单条裸流,设备
|
|
195
|
+
* stderr **合流进 stdout**,宿主进程退出码**恒 0**。
|
|
196
|
+
*
|
|
197
|
+
* 真机实测(车GV 2026-09-17,SM-S721B / Android 15 / toybox 0.8.11 / adb 1.0.41 `35.0.2`,设备 features
|
|
198
|
+
* 含 `shell_v2`),逐条复现而不是从文档推断:
|
|
199
|
+
* - `exec-out 'false'` ⇒ 宿主 exit **0**;`shell -T -- 'false'` ⇒ **1**;`shell -T -- 'exit 7'` ⇒ **7**
|
|
200
|
+
* - `exec-out 'realpath -- <坏链>'` ⇒ exit 0 + 错误文本在 **stdout**;`shell -T --` ⇒ exit 1 + 在 **stderr**
|
|
201
|
+
* - 1 MiB 随机字节 `cat` 回读:两形 md5 **相同**(`shell -T` 无 CRLF 转换、无包裹字节),故 binary 面照用
|
|
202
|
+
* - 同一条 `realpath` 的墙钟:`shell -T` 68 ms/次 vs `exec-out` 79 ms/次(n=10)—— 不是性能代价
|
|
203
|
+
*
|
|
204
|
+
* 修前的后果链([ref] 安全轴静默 fail-open):共享基座 `posix-shell-fs.ts` 的**每一处**失败判据(7 处)
|
|
205
|
+
* 与链原语 `remote-shell.ts` 的 `runSymlinkChain`(第 8 处)认的都是 `exitCode !== 0`,而这条传输恒答 0
|
|
206
|
+
* ⇒ adb 腿上 `realpath` / `readlink` / `stat` / `ls` / `cat` **永不失败**,`canonicalPath` 把那行**错误
|
|
207
|
+
* 文本**当成 canonical key 交给敏感路径门。⚠️ **复审 F15 纠正:原文写「五处」是数错了**,而且漏掉的
|
|
208
|
+
* 第 8 处恰恰是最贵的一处 —— `canonicalChain`(S-362 / core 7.21.0 [ref],上一版刚接)在 exit 恒 0 下会把
|
|
209
|
+
* **合流进 stdout 的错误文本当成链上的跳**解析出来,等于给 core 的路径判官递一条**伪造的** symlink 链。
|
|
210
|
+
*
|
|
211
|
+
* 🔴 **`-T` 与 `--` 都是承重件,不是装饰**:
|
|
212
|
+
* - `-T` = 不分配 pty。带 pty 的交互 shell 会把 stderr 并进 stdout 并做 CRLF 转换(council §9.2 的那句话
|
|
213
|
+
* 本身没错,错的是它被用来给 `exec-out` 背书)。给了命令的 `adb shell` 默认本就不开 pty,`-T` 是**显式**
|
|
214
|
+
* 关闭,不依赖那个默认。
|
|
215
|
+
* - `--` = 终止 adb 自己的旗标解析。没有它,**首字符为 `-` 的设备命令会被 adb 当成自己的旗标吃掉**
|
|
216
|
+
* (真机复现:`adb shell -T '-x'` 不报错,直接变成一个挂住的交互 shell)—— 而 `-x` 恰好就是关掉
|
|
217
|
+
* 远端退出码与分流的那根旗标,即「一条以 `-x` 开头的命令能静默把本修关回去」。真机验证 `--` 之后的
|
|
218
|
+
* 参数原样落到设备 shell(`adb shell -T -- '-x'` ⇒ 设备答 `/system/bin/sh: -c: requires an argument`)。
|
|
219
|
+
*
|
|
220
|
+
* 命令永远是**最后一个**参数,整条包在一个 argv 元素里(设备侧 `sh -c` 收全串,与 `withCwdEnv` 的
|
|
221
|
+
* `cd … && …` 前缀语义一致)。
|
|
222
|
+
*/
|
|
223
|
+
private deviceShellArgs;
|
|
165
224
|
private withCwdEnv;
|
|
166
225
|
}
|
|
167
226
|
/**
|
|
@@ -5,7 +5,7 @@ import { spawn } from "node:child_process";
|
|
|
5
5
|
import { shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
|
|
6
6
|
import { signalNumber } from "./host-platform.js";
|
|
7
7
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
8
|
-
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
8
|
+
import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
|
|
9
9
|
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
10
10
|
const PROVIDER = "adb";
|
|
11
11
|
const ok = (value) => ({ ok: true, value });
|
|
@@ -76,6 +76,18 @@ export class RemoteAdbExecutionEnv {
|
|
|
76
76
|
const state = st.value.stdout.trim();
|
|
77
77
|
const err = `${st.value.stdout} ${st.value.stderr}`.toLowerCase();
|
|
78
78
|
if (state === "device") {
|
|
79
|
+
const feats = await this.adb(["-s", this.cfg.serial, "features"], { signal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
80
|
+
if (!feats.ok)
|
|
81
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", `adb features failed: ${feats.error.message}`, feats.error) };
|
|
82
|
+
if (feats.value.exitCode !== 0) {
|
|
83
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", `adb features exited ${feats.value.exitCode} for ${this.cfg.serial}: ${feats.value.stderr.trim().slice(0, 200) || "(no stderr)"} — could not read the device's feature list this attempt`) };
|
|
84
|
+
}
|
|
85
|
+
if (!/(^|\s)shell_v2(\s|$)/.test(feats.value.stdout.trim())) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: new RemoteExecutionError("unsupported", `device ${this.cfg.serial} does not advertise the 'shell_v2' feature, so this adapter cannot establish that its command round-trip carries a real exit code with stderr kept separate — the two properties every filesystem judgment on this lane depends on to tell failure from success. Refusing to connect rather than run on an unverifiable transport: attach a device that supports adb shell protocol v2.`),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
79
91
|
this.connected = true;
|
|
80
92
|
return ok(this.workspaceHandle());
|
|
81
93
|
}
|
|
@@ -112,7 +124,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
112
124
|
if (!this.connected)
|
|
113
125
|
return { ok: false, error: new ExecutionError("shell_unavailable", "ADB device not connected — call connect() first") };
|
|
114
126
|
const wrapped = this.withCwdEnv(command, options?.cwd, options?.env);
|
|
115
|
-
const r = await this.adb(
|
|
127
|
+
const r = await this.adb(this.deviceShellArgs(wrapped), {
|
|
116
128
|
signal: options?.abortSignal,
|
|
117
129
|
timeoutMs: options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs,
|
|
118
130
|
onStdout: options?.onStdout,
|
|
@@ -130,7 +142,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
130
142
|
if (!this.connected)
|
|
131
143
|
throw new RemoteExecutionError("connect_failed", "ADB device not connected — call connect() first");
|
|
132
144
|
const wrapped = this.withCwdEnv(command, options?.cwd, options?.env);
|
|
133
|
-
const child = this.spawnFn(this.cfg.adbPath,
|
|
145
|
+
const child = this.spawnFn(this.cfg.adbPath, this.deviceShellArgs(wrapped), { stdio: ["ignore", "pipe", "pipe"] });
|
|
134
146
|
const queue = [];
|
|
135
147
|
let finished = false;
|
|
136
148
|
let failure;
|
|
@@ -176,6 +188,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
176
188
|
if (queue.length >= 512 && !paused) {
|
|
177
189
|
paused = true;
|
|
178
190
|
child.stdout.pause();
|
|
191
|
+
child.stderr.pause();
|
|
179
192
|
}
|
|
180
193
|
signalReady();
|
|
181
194
|
};
|
|
@@ -206,6 +219,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
206
219
|
if (paused) {
|
|
207
220
|
paused = false;
|
|
208
221
|
child.stdout.resume();
|
|
222
|
+
child.stderr.resume();
|
|
209
223
|
}
|
|
210
224
|
if (failure)
|
|
211
225
|
throw failure;
|
|
@@ -244,6 +258,8 @@ export class RemoteAdbExecutionEnv {
|
|
|
244
258
|
fsGate(p) {
|
|
245
259
|
if (this.destroyed)
|
|
246
260
|
return { ok: false, error: new FileError("unknown", "execution env already destroyed", this.resolve(p)) };
|
|
261
|
+
if (!this.connected)
|
|
262
|
+
return { ok: false, error: new FileError("unknown", "ADB device not connected — call connect() first", this.resolve(p)) };
|
|
247
263
|
return undefined;
|
|
248
264
|
}
|
|
249
265
|
async readBinaryFile(p, abortSignal) {
|
|
@@ -251,21 +267,11 @@ export class RemoteAdbExecutionEnv {
|
|
|
251
267
|
if (gated)
|
|
252
268
|
return gated;
|
|
253
269
|
const abs = this.resolve(p);
|
|
254
|
-
const r = await this.adb(
|
|
270
|
+
const r = await this.adb(this.deviceShellArgs(`cat ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.dataTimeoutMs, binary: true });
|
|
255
271
|
if (!r.ok)
|
|
256
272
|
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
257
|
-
const errOut = r.value.stderr.trim();
|
|
258
|
-
if (errOut) {
|
|
259
|
-
if (/no such file/i.test(errOut))
|
|
260
|
-
return { ok: false, error: new FileError("not_found", errOut, abs) };
|
|
261
|
-
if (/permission denied/i.test(errOut))
|
|
262
|
-
return { ok: false, error: new FileError("permission_denied", errOut, abs) };
|
|
263
|
-
if (/is a directory/i.test(errOut))
|
|
264
|
-
return { ok: false, error: new FileError("is_directory", errOut, abs) };
|
|
265
|
-
return { ok: false, error: new FileError("unknown", errOut, abs) };
|
|
266
|
-
}
|
|
267
273
|
if (r.value.exitCode !== 0)
|
|
268
|
-
return { ok: false, error:
|
|
274
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr.trim() || `adb cat exited ${r.value.exitCode}`, abs) };
|
|
269
275
|
return ok(r.value.stdoutBytes ?? new Uint8Array(0));
|
|
270
276
|
}
|
|
271
277
|
async readTextFile(p, abortSignal) {
|
|
@@ -284,6 +290,9 @@ export class RemoteAdbExecutionEnv {
|
|
|
284
290
|
return ok(lines);
|
|
285
291
|
}
|
|
286
292
|
async writeFile(p, content, abortSignal) {
|
|
293
|
+
const gated = this.fsGate(p);
|
|
294
|
+
if (gated)
|
|
295
|
+
return gated;
|
|
287
296
|
const abs = this.resolve(p);
|
|
288
297
|
const dir = path.posix.dirname(abs);
|
|
289
298
|
if (dir && dir !== "." && dir !== "/") {
|
|
@@ -436,6 +445,9 @@ export class RemoteAdbExecutionEnv {
|
|
|
436
445
|
});
|
|
437
446
|
});
|
|
438
447
|
}
|
|
448
|
+
deviceShellArgs(command) {
|
|
449
|
+
return ["-s", this.cfg.serial, "shell", "-T", "--", command];
|
|
450
|
+
}
|
|
439
451
|
withCwdEnv(command, cwd, env) {
|
|
440
452
|
const parts = [];
|
|
441
453
|
const dir = cwd ?? this.cwd;
|
|
@@ -1012,8 +1012,8 @@ export class RemoteHostExecutionEnv {
|
|
|
1012
1012
|
if (!ready.ok)
|
|
1013
1013
|
return ready;
|
|
1014
1014
|
const hops = [];
|
|
1015
|
-
const seen = new Set();
|
|
1016
1015
|
let cur = this.resolve(p);
|
|
1016
|
+
const seen = new Set([cur]);
|
|
1017
1017
|
for (let i = 0; i < SYMLINK_CHAIN_MAX_HOPS; i += 1) {
|
|
1018
1018
|
let target;
|
|
1019
1019
|
try {
|
|
@@ -64,16 +64,12 @@ export declare function buildSymlinkChainCommand(absPath: string): string;
|
|
|
64
64
|
* {@link buildSymlinkChainCommand} 的 stdout → `SymlinkChain.hops`。
|
|
65
65
|
*
|
|
66
66
|
* 三件事**只在这里**做一次(四条 shell 腿 + e2b 共读):
|
|
67
|
-
* · **环即止** —— 出现重复跳时在**重复的那一跳之前**截断(CC `vr` 的 `d.has(u)` 同律)
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* (
|
|
71
|
-
* `[b
|
|
72
|
-
*
|
|
73
|
-
* 造成误放行),而改成与 core 同形是**摘掉**一个视图 —— 在 [ref] 轴上那是错的方向,要动得先有裁定;
|
|
74
|
-
* ②那一跳是 `b` **真的写下的**目标,契约句「不重复被寻址的拼法」两种读法都讲得通(core 读作「起点不入表」,
|
|
75
|
-
* 本族读作「每条链接写下的目标都入表」),属**待 core 裁**的一格,不是本仓单方面能定的。
|
|
76
|
-
* ⚠️ 故各腿头注里「与 core 同形 / 逐字节对得上」那句话的射程是**无环链**;有环的那一格以本段为准。
|
|
67
|
+
* · **环即止** —— 出现重复跳时在**重复的那一跳之前**截断(CC `vr` 的 `d.has(u)` 同律),且 seen 表用
|
|
68
|
+
* **被寻址的拼法播种**(S-408,core 裁 2026-09-17:契约句「不重复被寻址的拼法」的正解是**起点本身不是一跳**——
|
|
69
|
+
* 链视图列的是「从被寻址拼法出发经过的中间拼法」,链绕回起点即结束、起点不再列;成环的事实由终点判决
|
|
70
|
+
* `canonicalPath`(realpath ELOOP ⇒ unresolvable ⇒ 拒)承担,不由链视图承担)。于是 `a -> b`、`b -> a`,问 `a`,
|
|
71
|
+
* 答 `[b]`,与 core `NodeExecutionEnv.canonicalChain`(`new Set([resolved])` 播种)逐字节同形。此前本族从空表
|
|
72
|
+
* 开始、答 `[b, a]`(7.82.0 及之前;多一个冗余视图 —— 判官对 `a` 已按拼法判过一次),裁定后对齐。
|
|
77
73
|
* 脚本侧不做:
|
|
78
74
|
* POSIX sh 里维护一张 seen 表要么靠内嵌换行的 `case` 模式(易碎),要么另起一个进程;放在这里则
|
|
79
75
|
* **一份实现**被四条腿与 e2b 共用,且与 host 腿的 Node 循环逐字节对得上。代价如实登记:链上真有环时,
|
|
@@ -83,7 +79,7 @@ export declare function buildSymlinkChainCommand(absPath: string): string;
|
|
|
83
79
|
* 处理,不假装支持)。残留如实写出来:目标拼法真带首尾空白时,trim 后的跳可能与真目标差几个字节 ——
|
|
84
80
|
* 方向上只会让一条跳**匹配不到**受保护行(少问,不是多放行),而终点判决(`canonicalPath`)完全不经本路。
|
|
85
81
|
*/
|
|
86
|
-
export declare function symlinkChainHopsFromStdout(stdout: string): string[];
|
|
82
|
+
export declare function symlinkChainHopsFromStdout(stdout: string, addressed: string): string[];
|
|
87
83
|
/** {@link runSymlinkChain} 的传输面 —— 各腿的 `exec` 收敛成的那一个形(与 `PosixShellFs` 基座的 `run` 同形)。 */
|
|
88
84
|
export type ShellChainRun = (cmd: string) => Promise<Result<{
|
|
89
85
|
stdout: string;
|
|
@@ -47,9 +47,9 @@ export function buildSymlinkChainCommand(absPath) {
|
|
|
47
47
|
`*) d="${"${c%/*}"}"; case "$d" in "") c="/$t" ;; */) c="$d$t" ;; *) c="$d/$t" ;; esac ;; esac; ` +
|
|
48
48
|
`printf '%s\\n' "$c"; n=$((n+1)); done; }`);
|
|
49
49
|
}
|
|
50
|
-
export function symlinkChainHopsFromStdout(stdout) {
|
|
50
|
+
export function symlinkChainHopsFromStdout(stdout, addressed) {
|
|
51
51
|
const hops = [];
|
|
52
|
-
const seen = new Set();
|
|
52
|
+
const seen = new Set([addressed]);
|
|
53
53
|
for (const raw of stdout.split("\n")) {
|
|
54
54
|
const hop = raw.trim();
|
|
55
55
|
if (hop === "")
|
|
@@ -69,6 +69,6 @@ export async function runSymlinkChain(absPath, run) {
|
|
|
69
69
|
return { ok: false, error: fileErrorFromExec(r.error, absPath) };
|
|
70
70
|
if (r.value.exitCode !== 0)
|
|
71
71
|
return { ok: false, error: classifyFsStderr(r.value.stderr, absPath, "unknown") };
|
|
72
|
-
return { ok: true, value: { hops: symlinkChainHopsFromStdout(r.value.stdout) } };
|
|
72
|
+
return { ok: true, value: { hops: symlinkChainHopsFromStdout(r.value.stdout, absPath) } };
|
|
73
73
|
}
|
|
74
74
|
//# sourceMappingURL=remote-shell.js.map
|
|
@@ -52,8 +52,56 @@
|
|
|
52
52
|
* 验证搜索配对不对。挂哪儿(boot 期一次性 warn / `/health` 子字段 / 显式端点)未定 ——
|
|
53
53
|
* 注意 boot 期探活会给每次启动加一次出网,多租/离线部署要能关。
|
|
54
54
|
*/
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Search providers this backend speaks —— **词表的唯一属主**(运行期与类型同一份)。
|
|
57
|
+
* Absent `WEB_SEARCH_PROVIDER` ⇒ no backend ⇒ WebSearch is not assembled.
|
|
58
|
+
*
|
|
59
|
+
* 🔴 **为什么是常量元组而不是裸 union**(车GW 对抗复审 F1[medium],2026-09-17):改前这张表在仓里有**四份**
|
|
60
|
+
* —— 类型一份 + 两只解析器各一条 `provider !== "brave" && …` 后置链 + `config-catalog` 的
|
|
61
|
+
* `enumValues: [...]` 字面量(那个字段是 `readonly string[]`,与本类型零类型联系)。加一只后端时 tsc
|
|
62
|
+
* **只红** `createWebSearchBackend` 的 switch:守卫链把 provider 收窄成旧三词,而旧三词是新 union 的
|
|
63
|
+
* 子集 ⇒ 赋值合法 ⇒ 改完 switch 全绿发版,`WEB_SEARCH_PROVIDER=<新词>` 却被判 undefined、能力位
|
|
64
|
+
* (`projectWebSearchCapability`)诚实报 `"none"`,运维以为自己配上了。收成一只元组之后:类型从它派生、
|
|
65
|
+
* 两只解析器读它、目录行**就用它本人**(身份相等有钉),规则集从四张表降到一张。
|
|
66
|
+
*/
|
|
67
|
+
export declare const WEB_SEARCH_PROVIDERS: readonly ["brave", "tavily", "searxng"];
|
|
68
|
+
export type WebSearchProvider = (typeof WEB_SEARCH_PROVIDERS)[number];
|
|
69
|
+
/**
|
|
70
|
+
* S-382:`GET /v1/capabilities` 的 `webSearch` 位 —— **部署默认后端**的闭集自述。
|
|
71
|
+
*
|
|
72
|
+
* ## 病
|
|
73
|
+
* 「这台 worker 的搜索走哪只后端」此前只在 operator 面可知(`GET /v1/config/catalog` 的
|
|
74
|
+
* `WEB_SEARCH_PROVIDER` 行)。消费端(cli doctor / client-core 的能力投影)要答「本部署有没有搜索、
|
|
75
|
+
* 是哪一只」,只能让用户跑一个真任务去撞 —— 而搜索缺席时工具**根本不装配**(`capabilities/scenarios.ts`
|
|
76
|
+
* 的 `assembleCodeTools({ webSearch })`),模型只会说「我没有这个工具」,那句话在 UX 上与「后端配错了」
|
|
77
|
+
* 不可区分。
|
|
78
|
+
*
|
|
79
|
+
* ## 形
|
|
80
|
+
* `{ backend: "brave" | "tavily" | "searxng" | "none" }`。三条纪律逐条写明:
|
|
81
|
+
* ① **词源单点** —— 闭集是 {@link WebSearchProvider} **取型** ∪ `"none"`,而该类型又派生自
|
|
82
|
+
* {@link WEB_SEARCH_PROVIDERS}(全仓**唯一**一张 provider 词表:两只解析器读它、`config-catalog` 的
|
|
83
|
+
* `enumValues` **就用它本人**〔身份相等有钉〕)。加一只后端 = 改那只元组一处,这一位自动跟着长;
|
|
84
|
+
* 没跟上的地方只剩 `createWebSearchBackend` 的穷尽 `switch`,而那一处是**编译红**。
|
|
85
|
+
* ② **只出词,不出内情** —— 端点(SearXNG 实例 URL)、API key、配额一个字都不上这条 wire:本面在
|
|
86
|
+
* service-credential 门之后但**非 operator 专属**,任何拿得到凭据的调用方都读得到它。
|
|
87
|
+
* ③ **广告的是部署默认** —— 每请求的 `body.settings.webSearch`(单用户车道上**压过**部署座,见
|
|
88
|
+
* `capabilities/scenarios.ts` 的 default 场景)**刻意不在这一面回显**:那是请求方自己送进来的东西,
|
|
89
|
+
* 把它回显出去等于让能力面随调用方漂,而消费端正是拿这一位去渲「这台机器有没有搜索」的。
|
|
90
|
+
*/
|
|
91
|
+
export interface WebSearchCapability {
|
|
92
|
+
readonly backend: WebSearchProvider | "none";
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 部署席 → 消费端窄投影。**唯一**写 `"none"` 的地方(缺席折词的属主),形照 `projectSqlEngineCapability` /
|
|
96
|
+
* `projectWriteProtectionCapability`。
|
|
97
|
+
*
|
|
98
|
+
* 🔴 缺席折 `"none"` **不是**兜底 fail-open:`undefined`(装配没喂席位 / `WEB_SEARCH_PROVIDER` 未设或拼错)
|
|
99
|
+
* 与「本部署没有搜索后端」在**行为上是同一件事** —— `webSearchConfigFromEnv` 对这两种输入都返回
|
|
100
|
+
* `undefined`,于是 WebSearch 工具都不装配。方向也是安全的那一向:本位只会**少报**能力(说没有、其实
|
|
101
|
+
* 也真没有),不会替一台没配后端的部署点亮一格。「键整个缺席」才是另一回事(= 老 server),那一层由
|
|
102
|
+
* 消费端的探位纪律管,见契约附录 B.1 本行的「缺席何义」。
|
|
103
|
+
*/
|
|
104
|
+
export declare function projectWebSearchCapability(provider: WebSearchProvider | undefined): WebSearchCapability;
|
|
57
105
|
export interface WebSearchBackendConfig {
|
|
58
106
|
readonly provider: WebSearchProvider;
|
|
59
107
|
/** API key (brave/tavily). From `WEB_SEARCH_API_KEY` — never reaches the model. */
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { createSearxngSearchBackend } from "@sema-agent/core";
|
|
2
|
+
export const WEB_SEARCH_PROVIDERS = ["brave", "tavily", "searxng"];
|
|
3
|
+
function isWebSearchProvider(word) {
|
|
4
|
+
return WEB_SEARCH_PROVIDERS.includes(word);
|
|
5
|
+
}
|
|
6
|
+
export function projectWebSearchCapability(provider) {
|
|
7
|
+
return provider === undefined ? { backend: "none" } : { backend: provider };
|
|
8
|
+
}
|
|
2
9
|
const DEFAULT_MAX_RESULTS = 10;
|
|
3
10
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
4
11
|
const BRAVE_BASE = "https://api.search.brave.com/res/v1/web/search";
|
|
@@ -152,7 +159,7 @@ export function shouldProbeWebSearchOnBoot(env = process.env) {
|
|
|
152
159
|
}
|
|
153
160
|
export function webSearchConfigFromEnv(env = process.env) {
|
|
154
161
|
const provider = env.WEB_SEARCH_PROVIDER?.trim().toLowerCase();
|
|
155
|
-
if (provider
|
|
162
|
+
if (provider === undefined || !isWebSearchProvider(provider))
|
|
156
163
|
return undefined;
|
|
157
164
|
const searxngParams = parseSearxngParams(env.WEB_SEARCH_SEARXNG_PARAMS);
|
|
158
165
|
const maxRaw = Number(env.WEB_SEARCH_MAX_RESULTS ?? "");
|
|
@@ -171,7 +178,7 @@ export function webSearchConfigFromSettings(raw) {
|
|
|
171
178
|
return undefined;
|
|
172
179
|
const s = raw;
|
|
173
180
|
const provider = typeof s.provider === "string" ? s.provider.trim().toLowerCase() : "";
|
|
174
|
-
if (provider
|
|
181
|
+
if (!isWebSearchProvider(provider))
|
|
175
182
|
return undefined;
|
|
176
183
|
const max = typeof s.maxResults === "number" && Number.isFinite(s.maxResults) && s.maxResults > 0 ? Math.floor(s.maxResults) : undefined;
|
|
177
184
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.82.
|
|
3
|
+
"version": "7.82.1",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"sharp": "^0.35.3"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@sema-agent/sdk": "9.
|
|
72
|
+
"@sema-agent/sdk": "9.6.0",
|
|
73
73
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
74
74
|
"@types/node": "22.10.2",
|
|
75
75
|
"@types/pg": "^8.20.0",
|