@sema-agent/server 7.98.0 → 7.100.0
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/MIGRATION.md +19 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +86 -60
- package/deploy/sema-up/lib/envf.sh +110 -42
- package/deploy/sema-up/sema-up.sh +76 -34
- package/dist/approval-ask-machine.d.ts +10 -0
- package/dist/approval-ask-machine.js +3 -0
- package/dist/approval-card.d.ts +74 -1
- package/dist/approval-card.js +41 -8
- package/dist/approval.d.ts +44 -20
- package/dist/approval.js +14 -6
- package/dist/boot/config-center.d.ts +16 -8
- package/dist/boot/config-center.js +206 -103
- package/dist/boot/execution-env.d.ts +1 -2
- package/dist/boot/execution-env.js +2 -2
- package/dist/boot/leader.js +12 -9
- package/dist/boot/memory-consolidation.d.ts +38 -0
- package/dist/boot/memory-consolidation.js +35 -0
- package/dist/boot/org-memory.d.ts +2 -1
- package/dist/boot/org-memory.js +9 -5
- package/dist/boot/resolve-spec.d.ts +0 -1
- package/dist/boot/resolve-spec.js +4 -3
- package/dist/boot/runner-deps.d.ts +2 -2
- package/dist/boot/runtime-caps.js +4 -4
- package/dist/boot/stage-05-execution-env.d.ts +1 -1
- package/dist/boot/stage-06-runners.d.ts +2 -2
- package/dist/boot/stage-06-runners.js +3 -3
- package/dist/boot/stage-07-capability-layer.d.ts +2 -3
- package/dist/boot/stage-07-capability-layer.js +6 -6
- package/dist/boot/stage-08-reapers.d.ts +3 -4
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +2 -3
- package/dist/boot/stage-10-http-server.d.ts +2 -3
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/capabilities/center-plugins.d.ts +14 -2
- package/dist/capabilities/center-plugins.js +13 -5
- package/dist/capabilities/center-prompts.d.ts +3 -0
- package/dist/capabilities/center-prompts.js +1 -1
- package/dist/capabilities/hands-lane.js +2 -1
- package/dist/capabilities/scenarios.js +18 -5
- package/dist/capabilities/tool-defer.d.ts +2 -2
- package/dist/center-credential.d.ts +112 -0
- package/dist/center-credential.js +131 -0
- package/dist/config-catalog.d.ts +2 -2
- package/dist/config-catalog.js +15 -15
- package/dist/config-center/apply-effective.d.ts +34 -39
- package/dist/config-center/apply-effective.js +100 -64
- package/dist/config-center/apply-ledger.d.ts +35 -16
- package/dist/config-center/apply-ledger.js +25 -18
- package/dist/config-center/center-request.d.ts +60 -0
- package/dist/config-center/center-request.js +71 -0
- package/dist/config-center/facade.d.ts +11 -5
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/hot-keys-registry.d.ts +11 -4
- package/dist/config-center/hot-keys-registry.js +11 -7
- package/dist/config-center/http-client.d.ts +17 -9
- package/dist/config-center/http-client.js +27 -36
- package/dist/config-center/restart-signal.d.ts +21 -14
- package/dist/config-center/restart-signal.js +16 -22
- package/dist/config-center/skills-mcp.d.ts +4 -1
- package/dist/config-center/skills-mcp.js +2 -2
- package/dist/config-center/types.d.ts +3 -3
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +0 -8
- package/dist/config-lkg.d.ts +47 -8
- package/dist/config-lkg.js +107 -20
- package/dist/config-provider.d.ts +8 -7
- package/dist/config-provider.js +5 -5
- package/dist/config-types.d.ts +30 -11
- package/dist/config.js +11 -13
- package/dist/cross-session-settings.js +1 -1
- package/dist/fleet-client.d.ts +8 -8
- package/dist/fleet-client.js +7 -7
- package/dist/fleet-lease.d.ts +11 -11
- package/dist/fleet-lease.js +17 -13
- package/dist/host-lsp-manager.d.ts +29 -0
- package/dist/host-lsp-manager.js +14 -0
- package/dist/hosted-posture.d.ts +49 -93
- package/dist/hosted-posture.js +1 -25
- package/dist/http/active-run-conflict.d.ts +2 -12
- package/dist/http/active-run-conflict.js +1 -6
- package/dist/http/admission.js +8 -0
- package/dist/http/resume-legs.d.ts +1 -1
- package/dist/http/resume-legs.js +14 -10
- package/dist/http/route-ctx.d.ts +2 -2
- package/dist/http/routes/approvals-assistant.d.ts +1 -1
- package/dist/http/routes/approvals-assistant.js +2 -2
- package/dist/http/routes/memory-compliance.js +5 -0
- package/dist/http/routes/memory-origin.js +5 -0
- package/dist/http/routes/sessions.js +6 -0
- package/dist/http/routes/tasks.js +7 -6
- package/dist/http/server.d.ts +6 -5
- package/dist/http/wire-types.d.ts +6 -3
- package/dist/leader/wire.d.ts +20 -12
- package/dist/leader/wire.js +7 -6
- package/dist/memory-layer-preflight.d.ts +46 -0
- package/dist/memory-layer-preflight.js +121 -0
- package/dist/memory-operator-faces.js +17 -1
- package/dist/observability/corrupt-read-seat.d.ts +21 -4
- package/dist/observability/corrupt-read-seat.js +31 -0
- package/dist/observability/fail-open.d.ts +8 -4
- package/dist/observability/fail-open.js +8 -4
- package/dist/observability/run-terminal.js +1 -0
- package/dist/observability/secret-env-scrub.d.ts +3 -26
- package/dist/observability/secret-env-scrub.js +3 -2
- package/dist/orchestration/workflow-completion-inbox.d.ts +31 -23
- package/dist/orchestration/workflow-completion-inbox.js +8 -3
- package/dist/plugins/approval-ask-store-file.d.ts +20 -2
- package/dist/plugins/approval-ask-store-file.js +21 -3
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -0
- package/dist/plugins/approval-ask-store-memory.js +39 -13
- package/dist/plugins/approval-ask-store-sql.d.ts +24 -5
- package/dist/plugins/approval-ask-store-sql.js +27 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +9 -3
- package/dist/plugins/checkpoint-store-sql.js +19 -2
- package/dist/plugins/local-checkpoint-store.js +2 -0
- package/dist/plugins/mailbox-store-sql.d.ts +7 -5
- package/dist/plugins/mailbox-store-sql.js +6 -4
- package/dist/plugins/permission-rule-store-file.js +3 -2
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +3 -2
- package/dist/plugins/sql-json-column.d.ts +16 -3
- package/dist/plugins/sql-json-column.js +15 -4
- package/dist/plugins/web-search.d.ts +42 -22
- package/dist/plugins/web-search.js +141 -49
- package/dist/project-memory.d.ts +28 -1
- package/dist/project-memory.js +22 -22
- package/dist/run-local.js +6 -7
- package/dist/runs.js +6 -5
- package/dist/runtime-caps-resolver.d.ts +8 -8
- package/dist/runtime-caps-resolver.js +16 -14
- package/dist/runtime-governance.d.ts +50 -3
- package/dist/runtime-governance.js +5 -0
- package/dist/sealed-key.js +3 -7
- package/dist/server-secret-env.d.ts +43 -0
- package/dist/server-secret-env.js +18 -0
- package/dist/task-settings.d.ts +1 -1
- package/dist/tool-approval.d.ts +144 -9
- package/dist/tool-approval.js +74 -18
- package/dist/trace/core-keyset-guard.d.ts +5 -5
- package/dist/trace/ledger-sink.js +2 -2
- package/dist/trace/project.d.ts +13 -0
- package/dist/trace/project.js +22 -5
- package/dist/trace/projection-drop.d.ts +2 -0
- package/dist/trace/projection-drop.js +6 -0
- package/dist/trace/sema-provenance.d.ts +4 -1
- package/dist/trace/sema-provenance.js +1 -1
- package/dist/trace/task-notification-facets.d.ts +65 -0
- package/dist/trace/task-notification-facets.js +31 -0
- package/dist/trace/wire-projection-faces.d.ts +3 -3
- package/dist/trace/wire-projection-faces.js +3 -3
- package/package.json +5 -4
package/MIGRATION.md
CHANGED
|
@@ -7,6 +7,25 @@
|
|
|
7
7
|
> ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
|
|
8
8
|
> 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
|
|
9
9
|
|
|
10
|
+
## 7.100.0 —— 十条 BREAKING(部署面 `CROSS_SESSION_INBOUND=accept` 拒启 / 类型面 `MailboxStore.ack` / WebSearch 配置形错响亮 / 请求面 `settings.webSearch` / WebSearch 报错句换形 / deny 部署的「无人可答」记部署政策 / 记忆抹除面新 409 / 审批名单热应用 / 子进程剥名 / local 车道回滚前收敛 ask 账本)
|
|
11
|
+
|
|
12
|
+
- **`CROSS_SESSION_INBOUND=accept` ⇒ 拒启**(core 7.30.0 把 `accept` 从 `crossSessionInbound` 闭集删掉,拆成 `wake`(立刻唤醒收件方)/ `next-turn`(排到收件方下一轮 = 旧 `accept` 的那一档);本仓词表直接取 core 常量,零别名 —— 拒启句列出新词表)。**迁移**:env 写 `next-turn`(原义)或 `wake`;`settings.json` 里的 `accept` 由引擎自己读成 `next-turn` 并通告一次,但 `@sema-agent/settings-schema` 6.0.0 的文件层对 `accept` 响亮拒 ⇒ 一并改写。本版精确钉 core 7.30.1 + settings-schema 6.0.0(锁步序:core → settings-schema → server → 宿主才可写 `wake`)。
|
|
13
|
+
- **`MailboxStore.ack` 返回 `Promise<{ acked: boolean }>`**(core 7.30.0):树外手铸的 `MailboxStore` 实现升级即 tsc 红 —— 围栏拒(盒不在 / 无人持租 / 非持租者)答 `{ acked: false }`,放行答 `{ acked: true }`(删 0 行也是 true)。本仓 SQL 两只孪生已改。
|
|
14
|
+
- **部署 env:WebSearch 旋钮形错 ⇒ 拒启**。`WEB_SEARCH_PROVIDER` 点名了后端时,`WEB_SEARCH_ENDPOINT`(不是 `scheme://host[:port][/path]` 形的绝对 URL)、`WEB_SEARCH_SEARXNG_PARAMS`(任一段不是 `name=value`、名不是参数名、同名两次、把数组 / 对象的 JSON 文本填进来)、`WEB_SEARCH_MAX_RESULTS` / `WEB_SEARCH_TIMEOUT_MS`(非数或 < 1,含 `30s` 这类带单位的)任一形错,进程起不来,stderr 首屏带键名与机读码 `config.web_search_invalid`。改前这些被静默吸收或落默认。**空串 / 未设不受影响;没配 `WEB_SEARCH_PROVIDER` 的部署不读这四键。** **迁移**:按拒启句把那一键改对或删掉。
|
|
15
|
+
- **每请求 `settings.webSearch` 形错 ⇒ `400 request.field_invalid`**(不分车道,请求面 BREAKING),错误句点名字段。**`provider` 缺席或不在词表里同样 400**(改前:整段丢弃、静默改用部署后端)—— 从 `settings.json` 带着拼错 provider(或只写了 `apiKey` / `endpoint`、没写 provider,或一个空的 `webSearch: {}`)的壳,修后提交即 400。**迁移**:按错误句改请求体(`provider` 写 `brave` / `tavily` / `searxng` 之一,不需要 per-request 搜索就整段删掉);`searxngParams` 用对象 `{ name: value }` 或串 `name=value;name=value`。
|
|
16
|
+
- **brave / tavily 非 2xx 报错句换形**:`<provider> search failed: HTTP <status> — <响应体摘录>`(改前 `<provider> search failed (<status>): …`)。按旧句形匹配的消费方要改。
|
|
17
|
+
- **`UNATTENDED_APPROVAL_POLICY=deny` 下「无人可答」记在部署政策名下(S-689,行为面;点名 cli)**:没人能答的受门调用,`tool_end.gate.settlement` 从 `{kind:"human_refused", who:{party:"person"}}` 变 `{kind:"policy_refused", who:{party:"none"}}`;模型读到引擎的策略句,run **不再**以 `haltedOnUserRejection:true` 收束、接着跑(auto 模式否决限回落那一问被拒时改为 `failed` / `classifier.denial_limit`);持久 ask 行当场落 DECIDED(deny) + `settled_by="policy"`,重入回放同一个政策拒。**谁受伤**:按 `haltedOnUserRejection` 或 `settlement.kind === "human_refused"` 判「用户拒了」的壳。**迁移**:改读 `policy_refused`;模型多跑的拍数由部署的 `maxTurns` / 墙钟兜顶。
|
|
18
|
+
- **记忆抹除面:三个既有端点新答 409 `memory.layer_unwritable`(S-693,行为面)**:`POST /v1/memory/erase`、`POST /v1/memory/origin/entries/:entryId/clear`、`POST /v1/sessions/:id/memory/erase`。锁住且有条目的记忆层在场时(默认路径 core 7.30.1 每次项目会话都会锁 `memory/local`),operator / 属主抹除与清标先答 409 `memory.layer_unwritable`(7.99.0:项目层条目 200、`local` 条目 500 + journal 卡盘、下次起服 fatal);同一门序让这种层在场时的非法选择子(400 `config.memory_erasure_request`)、清标空理由(422 `memory.origin_clear_invalid`)与已提交抹除同 `requestId` 重放的 200 收执也先答 409。挂载面里有指向树外目录的软链 ⇒ 未分类 500(判不了,日志点名软链)。**迁移**:按 409 体点名的目录 `chmod u+w <dir>` 后**用同一个 `requestId`** 重试(得到原答复);软链形把软链移出记忆层。core 7.31.0(#1076)提货版撤销本预检与这个码。
|
|
19
|
+
- **审批名单热应用(S-668 波 2,行为面)**:`governance.approvalRequire` 从「改了要重启」变热,`/health.restart.reasons` 与 `GET /v1/config/catalog` 的 `restartSlices` 不再有 `runtime-gates`(同版波 0:`models-tiers` 出、`memory-consolidation` 进 ⇒ 闭集 7 词)。三处行为变化:① 中心下发的名单形错(非数组 / `null` / 非字符串项)或拼法匹配不到 ⇒ 审批组**整组拒**:活配置零变化、候选不成为 LKG、每拍重判直到修好(改前只 warn、候选照样成为 LKG);没有 checkpoint 店的部署收到非空名单同样整组拒(改前重启后拒启 = 崩溃环);② 中心撤掉这一键 ⇒ 立刻回 env 底 `APPROVAL_REQUIRE`(改前保留到下次重启);③ 开了 leader 端点、有 checkpoint 店、只配 `APPROVAL_DENY` / `APPROVAL_NEVER_AUTO` 而 require 为空的部署,leader worker **开始执行**这两张表(改前一张都不执行;收紧向)。**迁移**:按 `sema_registry_approval_require_invalid` 日志修名单;依赖「leader 不执行 deny / never-auto」的部署核对名单。
|
|
20
|
+
- **回滚到 7.99.0(local 车道 + `UNATTENDED_APPROVAL_POLICY=deny`)要先收敛 / 压实 ask 账本(NP-9,持久数据面)**:7.100.0 起部署政策拒当场落 ask 行,File 形账本(`<数据根>/approval-asks/asks.jsonl`)把它记成新动词 `claimTerminalRefuse`。账本里只要还留着这种记录,7.99.0 **启动即拒启**并点名(「verb outside this build's closed set (a DOWNGRADE …)」)—— 不会静默把政策拒读成 VOID,但也起不来。**回滚步骤**:①停止接新任务,等所有 run 收敛(`GET /v1/approvals` 的 `pending` / `livePending` 都空,没有停驻或在飞的审批);②再二选一:让 7.100.0 继续跑到账本到阈压实(压实后账本只剩行快照,7.99.0 读得动,政策结算原样保留),或停机把 `asks.jsonl` 移开(已收敛的部署上只丢已决历史 —— 与 `docs/DEPLOY-PREREQS.md`「要清就停机删文件」同一动作);③再启 7.99.0。SQL 车道(`mysql` / `pg`)不受影响:政策结算是行上的列,7.99.0 原样读得动。
|
|
21
|
+
- **子进程按名剥凭据形变量(core 7.30.1 #1072,运行面)**:工具子 shell、git 子进程与本地语言服务器收不到名字以 `KEYS` / `TOKENS` / `PASSWORDS` 结尾的宿主变量;计数豁免只看紧挨结尾词的那一个词 ⇒ `MAX_NEW_TOKENS` / `MAX_COMPLETION_TOKENS` / `MAX_BATCH_PREFILL_TOKENS` / `DB_FOREIGN_KEYS` / `JSON_SORT_KEYS` 这五个 7.30.0 不剥的非密钥名也被剥(core 收紧过头,归 core 修)。**迁移**:host shell 用 `inheritEnv:[names]` 点名放行,或给变量改个不以这三形结尾的名字;规则全文见 `docs/DEPLOY-PREREQS.md`。
|
|
22
|
+
|
|
23
|
+
## 7.99.0 —— 三条 BREAKING(行为面 / 类型面 / 运行时下限)
|
|
24
|
+
|
|
25
|
+
- **显式签发方不再算托管证据(7.99.0,S-671 / B-10;按 clay 裁定「显式签发方不算托管证据」)**:`PRINCIPAL_JWT_*` / `AUTH_BRIDGE_ISSUER` 在场而 `REQUIRE_PRINCIPAL` 未设的部署,7.95–7.98 拒启,7.99.0 起服;托管 ⇔ 运维显式声明 `REQUIRE_PRINCIPAL=true`。**谁受伤**:靠这条推断顶着没设 `REQUIRE_PRINCIPAL` 的多租机器,升级后不再被拦(召回缺口如实登记)。**迁移**:多租机器自己设 `REQUIRE_PRINCIPAL=true`(+ `OPERATOR_PRINCIPALS`),见 `docs/DEPLOY-PREREQS.md` 托管形前置。
|
|
26
|
+
- **`ServiceConfig.configCenter.token` 删除,换成 `credential`(7.99.0,S-651 波 A-1,类型面)**:凭证改为一只每次请求取值的活对象;新旋钮 `SEMA_REGISTRY_TOKEN_FILE`(与 `SEMA_REGISTRY_TOKEN` 二选一,都设 ⇒ 拒启)。手铸 `ServiceConfig` 的消费方 tsc 即红。**盘上数据**:旧 LKG 文件(`config-lkg-<worker>.json`,formatVersion 1)按凭证身份分箱后一次性迁移 —— 此刻凭证是该 worker 的拉取令牌且新箱无文件 ⇒ 迁进新箱、旧文件改名 `.migrated-v1`;其它形响亮拒一行(点名旧路径与出路);迁移代码到期删除(S-683,10-31)。旧 prompt 活动状态目录暂不迁移(S-685)。
|
|
27
|
+
- **Node 运行时下限改成实测值 `^20.19.0 || >=22.12.0`(7.99.0)**:此前 `>=20` 是假话 —— 7.98.0 在 Node 20.0–20.18 与 22.0–22.11 上本就起不来(依赖 e2b 的 CJS 入口撞 chalk@5 纯 ESM,S-682);开了 engine-strict 的安装在范围外会失败。**迁移**:Node 升到 20.19+ 或 22.12+。
|
|
28
|
+
|
|
10
29
|
## SQL 存储面 BREAKING(3.0.0 之后的三个窗)
|
|
11
30
|
|
|
12
31
|
**常设口径**:本仓**不出 `ALTER TABLE` 增量迁移**(成文裁定:预生产期零存量用户窗口,schema 变更
|
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ model gateways, and cloud agent execution behind an HTTP/SSE contract.
|
|
|
78
78
|
|
|
79
79
|
## Quick start
|
|
80
80
|
|
|
81
|
-
Requirements: Node
|
|
81
|
+
Requirements: Node 20.19+ or 22.12+ (npm path; `engines.node` = `^20.19.0 || >=22.12.0`) and an OpenAI-compatible model gateway.
|
|
82
82
|
|
|
83
83
|
> **A note on the `glob@11` deprecation warning at install time.** `npm install` prints a deprecation
|
|
84
84
|
> warning for `glob@11.1.0`, pulled in transitively by `e2b` (the E2B sandbox SDK). It is **install-time
|
package/README.zh-CN.md
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
|
|
73
73
|
## 快速开始
|
|
74
74
|
|
|
75
|
-
环境要求:Node
|
|
75
|
+
环境要求:Node 20.19+ 或 22.12+(npm 路径;`engines.node` = `^20.19.0 || >=22.12.0`)+ 一个 OpenAI 兼容模型网关。
|
|
76
76
|
|
|
77
77
|
> **关于安装时那条 `glob@11` 弃用警告。** `npm install` 会为 `glob@11.1.0` 打一条 deprecated 警告,它由
|
|
78
78
|
> `e2b`(E2B 沙箱 SDK)间接引入。这是**安装期噪声,在这里没有任何运行时曝露面**:`glob` 在 `e2b` 里的
|
package/USAGE.md
CHANGED
|
@@ -89,30 +89,39 @@ ANTHROPIC_MAX_RETRIES=10 # 云 Anthropic 腿
|
|
|
89
89
|
```bash
|
|
90
90
|
WEB_SEARCH_PROVIDER=brave|tavily|searxng # 唯一的"装配开关":合法词才挂 WebSearch 工具;缺席/非法词 = 不装配(不是挂了报错)
|
|
91
91
|
WEB_SEARCH_API_KEY=… # brave/tavily 必需(searxng 不读这一键);只进 backend 闭包,永不进模型 prompt 或工具参数
|
|
92
|
-
WEB_SEARCH_ENDPOINT=https://searx.example # searxng 必需(实例地址);brave/tavily 下是可选的 base-URL 覆盖(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
WEB_SEARCH_ENDPOINT=https://searx.example # searxng 必需(实例地址);brave/tavily 下是可选的 base-URL 覆盖(代理/测试/**Brave 兼容网关**)
|
|
93
|
+
# Brave 兼容网关(S-669,2026-09-24 实跑):自托管的 Nólë 网关暴露与 Brave 同形的 `GET …/res/v1/web/search`,
|
|
94
|
+
# 认 `X-Subscription-Token`,返回 `web.results[{title,url,description,…}]` —— 用 brave 词 + endpoint 覆写即可,零新词:
|
|
95
|
+
# WEB_SEARCH_PROVIDER=brave
|
|
96
|
+
# WEB_SEARCH_ENDPOINT=https://<gateway-host>:<port>/res/v1/web/search # 完整搜索 URL(不是 base)
|
|
97
|
+
# WEB_SEARCH_API_KEY=<网关给本部署的独立钥匙> # 从部署密钥存放处注入,不进仓、不进日志
|
|
98
|
+
# `GET /v1/capabilities` 的 `webSearch.backend` 仍报 `"brave"`(闭集不变);网关侧按钥匙限速 / 吊销 / 记 client。
|
|
99
|
+
WEB_SEARCH_MAX_RESULTS=10 # ≥1 的数,>20 夹到 20,小数向下取整;缺席/空 → backend 默认 10;非数字 / <1 → 拒启
|
|
100
|
+
WEB_SEARCH_TIMEOUT_MS=10000 # 单次搜索墙钟(ms),<1000 抬到 1000;缺席/空 → backend 默认 10000;非数字(含 "30s" 这类带单位的)/ <1 → 拒启
|
|
101
|
+
WEB_SEARCH_SEARXNG_PARAMS="engines=bing,duckduckgo;language=zh-CN" # 仅 searxng 腿消费;`;` 分隔 name=value;空 → 不铸;任一段不成形 → 拒启
|
|
96
102
|
WEB_SEARCH_PROBE_ON_BOOT=true # boot 期一次性真出网探活,默认 OFF;只认 "true"/"1"(trim+小写后比较),含糊值当没开
|
|
97
103
|
```
|
|
98
104
|
|
|
99
|
-
七键逐一(`src/plugins/web-search.ts` `webSearchConfigFromEnv`
|
|
100
|
-
`src/config-catalog.ts
|
|
105
|
+
七键逐一(属主 `src/plugins/web-search.ts` `webSearchConfigFromEnv`,每个字段一只判官、与每请求 `settings.webSearch`
|
|
106
|
+
共用;类型/默认/坏值登记见 `src/config-catalog.ts` 的 `WEB_SEARCH_*` 行)。**只有 `WEB_SEARCH_PROVIDER` 点名了后端时其余
|
|
107
|
+
旋钮才被读**——没配后端时它们不装配、也不判:
|
|
101
108
|
|
|
102
109
|
| env 键 | 类型 | 默认 | 缺席行为 | 坏值行为 |
|
|
103
110
|
|---|---|---|---|---|
|
|
104
111
|
| `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` |
|
|
105
|
-
| `WEB_SEARCH_API_KEY` | secret string | 无 | brave/tavily:后端仍会装配(装配只看 `PROVIDER`),**首次真实工具调用**时抛 `WEB_SEARCH_API_KEY is required for the <provider> provider`(`braveSearch` / `tavilySearch` 的首行守卫);searxng:本键无消费点 |
|
|
106
|
-
| `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` 的首行守卫) |
|
|
107
|
-
| `WEB_SEARCH_MAX_RESULTS` | number | `10` | 用默认 10 |
|
|
108
|
-
| `WEB_SEARCH_TIMEOUT_MS` | number(ms) | `10000` | 用默认 10000 |
|
|
109
|
-
| `WEB_SEARCH_SEARXNG_PARAMS` | string(`k=v;k=v`) | 无(不铸键 = 不传 `extraParams`,交给 core adapter 自己的缺省) | 键整个不铸 |
|
|
112
|
+
| `WEB_SEARCH_API_KEY` | secret string | 无 | brave/tavily:后端仍会装配(装配只看 `PROVIDER`),**首次真实工具调用**时抛 `WEB_SEARCH_API_KEY is required for the <provider> provider`(`braveSearch` / `tavilySearch` 的首行守卫);searxng:本键无消费点 | 空串 / 纯空白同缺席;内容不设形(它是凭据) |
|
|
113
|
+
| `WEB_SEARCH_ENDPOINT` | url string(brave 下 = 完整搜索 URL,可指向 Brave 兼容网关如 Nólë) | brave/tavily → 各自官方 API;searxng → 无默认 | brave/tavily:落官方 endpoint;searxng:**首次真实工具调用**时抛 `WEB_SEARCH_ENDPOINT (the SearXNG instance URL) is required for the searxng provider`(`searxngSearch` 的首行守卫) | 空串同缺席(compose 模板 `${WEB_SEARCH_ENDPOINT:-}` 铸的就是空串)。**不是 `scheme://host[:port][/path]` 形的绝对 URL ⇒ 拒启**(`searx.internal`、`localhost:8888` 这类少了 `scheme://` 的形都算;判据与模型路由 base URL 拒启同一只)。拒启句带键名与机读码 `config.web_search_invalid`,**不回显值**(这一格可能带凭据) |
|
|
114
|
+
| `WEB_SEARCH_MAX_RESULTS` | number | `10` | 用默认 10 | 空串同缺席;**非数字或 < 1 ⇒ 拒启**(`config.web_search_invalid`;改前静默落默认 10);是数则向下取整,再夹到 `[1,20]`(设 999 被夹到 20 —— 夹紧不是形错) |
|
|
115
|
+
| `WEB_SEARCH_TIMEOUT_MS` | number(ms) | `10000` | 用默认 10000 | 空串同缺席;**非数字(`"30s"` 这类带单位的也算)或 < 1 ⇒ 拒启**(`config.web_search_invalid`;改前静默落默认 10000 —— 写了 30s 的运维实际拿到 10s);是数则向下取整,再抬到 ≥ 1000ms |
|
|
116
|
+
| `WEB_SEARCH_SEARXNG_PARAMS` | string(`k=v;k=v`) | 无(不铸键 = 不传 `extraParams`,交给 core adapter 自己的缺省) | 键整个不铸 | 空串 / 纯空白 / 只有分号 = 缺席。文法与每请求 `settings.webSearch.searxngParams` 同一套:`name=value` 用 `;` 分隔(尾分号可有可无),名是参数名(字母 / 数字 / `_` / `-` / `.`),值非空,同名只许一次。**任一段不成形 ⇒ 拒启**(`config.web_search_invalid`,句里点名是哪一段 / 哪个名):把数组或对象的 JSON 文本(`["engines=bing"]`、`{"engines":"bing"}`)填进来、一段没有 `=`、名为空、同名两次。改前这些被静默跳过或吸成垃圾参数发给实例 |
|
|
110
117
|
| `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`) |
|
|
111
118
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
**结论:缺席与形错分两件事。** 缺席(未设 / 空串,以及 `WEB_SEARCH_PROVIDER` 不是三词之一)一律走「不装配 / 用默认」,
|
|
120
|
+
不 warn —— WebSearch 挂不挂是**功能面**取舍,不是安全边界。**形错**(写了但写不成该有的形:`WEB_SEARCH_ENDPOINT`、
|
|
121
|
+
`WEB_SEARCH_SEARXNG_PARAMS`、`WEB_SEARCH_MAX_RESULTS`、`WEB_SEARCH_TIMEOUT_MS` 四键)**拒启**,stderr 首屏一句带键名 +
|
|
122
|
+
机读码 `config.web_search_invalid`:形错此前被静默吸收(垃圾参数发给实例)或静默落到一个运维没选过的默认值,
|
|
123
|
+
与「配对了」在外面同形。仍只在**首次真实工具调用**时现形的只剩「形对但不通」一类(缺 key、地址对但连不上),
|
|
124
|
+
以 tool_result 错误文本回给模型,不是 HTTP 层错误、不使任务整体 `failed`(细节见 §9.5)。
|
|
116
125
|
- 结果是**不可信输入**:core 会 `delimitUntrusted` 围栏并**重新施加** `allowed_domains`/`blocked_domains`
|
|
117
126
|
地板,所以 backend 遵不遵守 `opts` 是优化不是正确性要求 —— 换 provider(包括换成自建 SearXNG)
|
|
118
127
|
不会削弱域名地板。
|
|
@@ -492,6 +501,12 @@ SEND_USER_FILE_SANDBOX_PUT_ENDPOINT=… # 可选:沙箱直传 PUT 的端
|
|
|
492
501
|
# 新名(orchestrator 现注入);旧名 CONFIG_CENTER_* 在场即 boot 拒启,只认 SEMA_REGISTRY_*
|
|
493
502
|
SEMA_REGISTRY_URL=http://<config-center-host>:3100 # 启动拉 GET /api/config/effective(Bearer+ETag),覆盖 env 兜底
|
|
494
503
|
SEMA_REGISTRY_TOKEN=<SERVICE_PULL_TOKEN 的值> # 取自配置控制面主机 .env;只读拉取令牌
|
|
504
|
+
# SEMA_REGISTRY_TOKEN_FILE=/path/to/token # 或:凭证文件(与上一行二选一,都设=拒启)。每条中心请求都重读 ⇒ 轮换 / 换账号改写文件即可,
|
|
505
|
+
# 中心请求面不重启;文件不可读 = error 级 center_credential_unreadable,不当作无凭证。
|
|
506
|
+
# ⚠️ 换账号(凭证身份变化)后,只在启动期物化的面 —— 技能 / MCP / A2A / 场景 —— 仍是上一个
|
|
507
|
+
# 身份的,直到重启(`/health` 报 restartRequired,reasons 含 skills);热面(models / roles /
|
|
508
|
+
# limits / 审批名单 …)在新身份第一份候选到达那一拍换代。「换账号即降档 + 当场拉取」候 S-667(不在 7.100.0)。
|
|
509
|
+
# 用户 JWT 形(CLI 登录)⇒ skill 正文 / prompt 产物走 /api/v1/me/…;盘上 LKG 与 prompt 状态按「worker + 凭证身份」分箱。
|
|
495
510
|
SEMA_REGISTRY_DRY_RUN=true # 安全灰度:只 LOG 中心配置 vs env 推导的差异,不 apply
|
|
496
511
|
SEMA_REGISTRY_WORKER=<worker名> # 可选:拉取 /effective?worker=<名> 取该 worker 的 roster(reconciler 按 worker 注);不设=全局 roster(向后兼容)
|
|
497
512
|
FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启 fleet 上报腿(announce/heartbeat+usage 批报到中心)。
|
|
@@ -507,7 +522,11 @@ FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启 flee
|
|
|
507
522
|
|---|---|---|
|
|
508
523
|
| models / roles / default 模型 | **热**(下一 refresh 拍) | 全 boot Runner **原子换代**(swap 失败=候选整拒,活配置零触碰);此前「改 models 需重启」的时代已随 Runner swap 腿落地终结 |
|
|
509
524
|
| pricing / keys(env-名引用)/ prompts / teams | **热** | 值换代即生效;cost 族限额座(rate limit / cost quota)同热(限额=纯比较参数,窗内累计不动;窗长换代=记账周期重开) |
|
|
510
|
-
|
|
|
525
|
+
| tier 变更(档位组 / 档位绑定) | **热**(下一 refresh 拍) | 与 models 同一条换代腿:tier 表随模型面在 commit 前原子换进全部 boot Runner,下一任务按新档位路由,`/health` 不报 `restartRequired`(7.100.0 起 `models-tiers` 重启理由退役——它在生产上从未发出过) |
|
|
526
|
+
| plugins(插件引用) | 重启生效 | 插件只在启动期物化;改了之后 `/health` 报 `restartRequired`,`restart.reasons` 含 `skills`(7.100.0 前这一改动**不报**,要等一次无关的重启) |
|
|
527
|
+
| skills / mcp / a2a / scenarios(技能 / MCP / A2A / 场景) | 重启生效 | 只在启动期物化;中心改了 ⇒ `/health` 报 `restartRequired`,`restart.reasons` 各含其名。**换账号**(凭证身份变化)同理:这些面仍是上一身份的直到重启(候 S-667) |
|
|
528
|
+
| 审批名单(`governance.approvalRequire`) | **热**(下一 refresh 拍) | 7.100.0 起:下一个任务调名单里的工具即停车等审批;撤键回 env 底(`APPROVAL_REQUIRE`)。**没有 checkpoint 店**的部署(`DURABLE_APPROVAL` 关 / 后端无 checkpoint 面)发非空名单 ⇒ 审批组**整组拒**(旧名单继续服务、`configAppliedVersion` 停旧代、拒因见日志 `sema_registry_approval_require_invalid` 与诊断端点 `configApply.groups.approvalGate`)—— 与启动期「名单已配却无 durable 门 ⇒ 拒启」同一条判据。此前 `/health` 报 `restartRequired`(`runtime-gates`,该理由退役) |
|
|
529
|
+
| 记忆整理驱动席(仅 `MEMORY_CONSOLIDATION_DRIVER=on`) | 重启生效 | 整理跑用的模型在启动期解析一次;中心改了 roles / 档位 / 该条目录后 `/health` 报 `restartRequired`,`restart.reasons` 含 `memory-consolidation`(7.100.0 新理由;此前不报) |
|
|
511
530
|
|
|
512
531
|
生效与否的观测口=`/health` 世代账键(`configTargetVersion`/`configAppliedVersion`+两 ordinal+`configApplyStaleMs`,见 API 表 `/health` 行)——target≠applied 持续=「改了没生效」的机读信号。
|
|
513
532
|
- **仅 `/v1/tasks`(同步)+ `/v1/runs`(异步)**——`/v1/tasks/stream` 不支持(多次尝试非单流,400)。与 `verify` **互斥**(同时给 → 400)。
|
|
@@ -654,10 +673,18 @@ UNATTENDED_APPROVAL_POLICY=park
|
|
|
654
673
|
没有 park 设施的部署由引擎 fail-closed 拒绝并告诉模型「没有可停靠的 durable 审批门」。
|
|
655
674
|
- `deny` ⇒ **真无人值守/headless 部署的显式声明**:这类 ask 当场拒绝,让模型自己改道,不积压一堆
|
|
656
675
|
等不到人的挂起 run。它是**收紧**方向(不放行任何东西)。
|
|
676
|
+
这一拒记在**部署政策**名下,不记在人名下(7.100.0 起):那次调用的 `tool_end.gate.settlement` 是
|
|
677
|
+
`{kind:"policy_refused", who:{party:"none"}}`;模型读到的是「部署政策拒了、此处没有交互审批,别重试,
|
|
678
|
+
先做不需要审批的部分」,run 接着跑(auto 模式下分类器连拒后回落的那一问被拒时,引擎按「无人可判」停下
|
|
679
|
+
run:终局 `failed` / `classifier.denial_limit`)。此前记成 `human_refused` / `who.party:"person"`,模型被告知
|
|
680
|
+
「用户不想继续,停下来等他」,run 以 `haltedOnUserRejection` 收束 —— 而这台部署自己声明了没人会来。
|
|
681
|
+
少数情形记的是 `approval_window_expired`,同样不在人名下:活卡已送达、本服务自己的窗走完没人点,**且**没有持久 ask 店(或 ask 店
|
|
682
|
+
当时报错、结束等待的只能说是本服务的窗)。有 ask 店时窗走完、终局 claim 赢下的那一支记的是 `policy_refused`(行上同落 `settled_by`)。
|
|
657
683
|
- **7.34.0 的行为变化(park 侧)**:此前「窗走完没人答」是**当场拒绝 + 一条 error 回模型**(模型往往就
|
|
658
684
|
绕开了那次治理);现在它与其余「无人可答」的情形一样走 park。要恢复旧结局请显式配 `deny`。
|
|
659
685
|
- 纯部署级:**不看**客户端的任何表态/权限模式。协调器整体关掉(`TOOL_APPROVAL_ENABLED` 关)时这个旋钮
|
|
660
|
-
|
|
686
|
+
没有施加对象。`APPROVAL_REQUIRE` 名单只决定「这只工具要不要审批」,与本旋钮无关;协调器在场时,名单工具的
|
|
687
|
+
审批与其余 ask 走同一条路、同一个政策(`deny` 部署上同样当场拒)。
|
|
661
688
|
- 🔴 **与 `STREAM_APPROVAL_ENABLED` 正交**:把流内协议开关关掉**不会**回滚 R-13 —— 它只是不发
|
|
662
689
|
`approval_request`、不落 ask 行、不起收敛器腿,活卡窗到期照样走 park(park 的承载是引擎的 checkpoint,
|
|
663
690
|
不是 server 的 ask 行,「有 durable 设施但没开协议」正是 R-13 要救的那类部署)。要回到旧的「窗满即拒」
|
|
@@ -782,20 +809,20 @@ TASK_WRITE_FACE_ESCALATION=allow # 词表 allow|refuse;坏词启动期响亮
|
|
|
782
809
|
| `allow` | 任意 | **放行**(部署允许任务层开写面) |
|
|
783
810
|
| `refuse` | 任意 | **422 拒**(运维显式表态**无条件**恒赢,不挂任何客户端派生腿) |
|
|
784
811
|
| 未设 | 否(单用户 turnkey) | **放行** —— 与 7.94 及以前逐字节相同 |
|
|
785
|
-
| 未设 | **是** | **422 拒**,拒因点名 `TASK_WRITE_FACE_ESCALATION=allow` 与判成托管的证据 |
|
|
812
|
+
| 未设 | **是**(`REQUIRE_PRINCIPAL=true`) | **422 拒**,拒因点名 `TASK_WRITE_FACE_ESCALATION=allow` 与判成托管的证据 |
|
|
786
813
|
|
|
787
814
|
- 🔴 **`REMOTE_EXEC=device` 车道上本门不适用**(任何 `REQUIRE_PRINCIPAL` 值):那条车道的路径落在
|
|
788
815
|
**发起者自己的设备**上,归属由 `device_session` 绑定行 + 准入链的 owner 谓词验过 —— host 闸防的
|
|
789
816
|
跨租户宿主穿越在那儿**结构性不存在**(成文裁定,与 `settings.env` / `cwd` 两键在该车道上的既有臂同源),
|
|
790
817
|
也叠加「device 车道不额外加一道审批」这条既定纪律。⚠️ 运维的**显式** `refuse` 在 device 上照样恒赢(旋钮不挂车道派生腿)。
|
|
791
|
-
- **「托管形」怎么判**(判据单点 `src/hosted-posture.ts
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
`
|
|
818
|
+
- **「托管形」怎么判**(判据单点 `src/hosted-posture.ts`;7.99.0 起 / S-671):
|
|
819
|
+
**托管 ⇔ `REQUIRE_PRINCIPAL=true`**,只认运维自己的声明,零推断。
|
|
820
|
+
🔴 **签发方配置不算证据**:`PRINCIPAL_JWT_PUBKEYS` / `_ISS` / `_AUD` 与 `AUTH_BRIDGE_ISSUER` 在不在场都不改变判定
|
|
821
|
+
(7.95.0–7.98.x 曾把它们当证据,并对「配了签发方却没设 `REQUIRE_PRINCIPAL`」的机器拒启;那条拒启随之删除)。
|
|
822
|
+
auth-bridge 那条 `?? configCenter.baseUrl` 回退、`SEMA_REGISTRY_*`(含 worker 名)同样不读。
|
|
823
|
+
⚠️ 反面是**全量召回缺口**:忘设 `REQUIRE_PRINCIPAL` 的团队机器按单用户运行、本门放行 —— 多租机器必须自己设它,
|
|
824
|
+
后果与被排除的候选(`OPERATOR_PRINCIPALS`、`SERVICE_AUTH_TOKENS` 条目数、`DB_BACKEND`、`BIND_HOST`)逐条写在
|
|
825
|
+
`docs/DEPLOY-PREREQS.md`。
|
|
799
826
|
- **同族的 `approverPosture` 是「忽略 + 留声」,不是拒**:提交体的 `approverPosture` 同样是一句宿主姿态
|
|
800
827
|
声明(引擎据此把 bypass 指令渲给模型 —— 那段文字指示模型改用 Bash 改文件而不是用文件工具),
|
|
801
828
|
托管形上它被**忽略**并留一条 `task_approver_posture_ignored`(与 `settings.hooks` / `cwd` /
|
|
@@ -1602,7 +1629,7 @@ armed ⟺ 本次请求 permissionMode 是 "auto" ∧ 本部署装配了分类
|
|
|
1602
1629
|
| 旋钮 | 缺省 | 语义 |
|
|
1603
1630
|
|---|---|---|
|
|
1604
1631
|
| `PERMISSIONS_DISABLE_AUTO_MODE` | `false`(opt-in) | CC/center/settings 键名 `permissions.disableAutoMode` 的 **env 腿**(config catalog 登记 center 键名 `permissions.disableAutoMode`,settings-schema 现无 `permissions` 域 ⇒ 目录行 `domainExists:false`):**tighten-only** 棘轮 —— `true` ⇒ 本部署每个 principal 的 `caps.autoMode` 折成 `false`(center 授予 `true` **翻不回**);未设 ⇒ 不动 caps。布尔词表(`true/false/1/0/yes/no/on/off`),词表外的值**拒启**(安全轴旋钮不许静默回默认)。目录行见 `GET /v1/config/catalog`(approval 域,security 轴) |
|
|
1605
|
-
| `CROSS_SESSION_INBOUND` | 未设 | 跨终端会话设计线:CC settings 键名 `crossSessionInbound` 的**部署/组织层**(引擎层形的 `managed` 层,CC `policySettings` 对位)——
|
|
1632
|
+
| `CROSS_SESSION_INBOUND` | 未设 | 跨终端会话设计线:CC settings 键名 `crossSessionInbound` 的**部署/组织层**(引擎层形的 `managed` 层,CC `policySettings` 对位)—— 本部署对**入站跨会话消息**的治理四态(词表取自引擎闭集,core 7.30.0 起):`wake` 投递且**宿主**可在收件箱增长时起一个 turn / `next-turn` 在下一个 turn 边界投递 / `hold` 停在收件会话的待审队列(模型看不见、不能据此行动)/ `refuse` 本部署整体退出这条车道。⚠️ 本服务宿主**不起 turn**,所以在本服务上 `wake` 按引擎契约的降级臂读作 `next-turn`。🔴 **旧词 `accept` 自 7.100.0(core 7.30.0 提货)起拒启** —— 它的原义就是 `next-turn`,改成那个词即可(零别名,拒启即通告)。🔴 **未设 ≠ `next-turn`**:未设 = 这一层不表态,由其余层与引擎的 mode-parity 判定决定。词表外的值**拒启**。⚠️ **生效前提**:引擎的 peer 目录席(`RunnerDeps.peerDirectory`)在场 —— 7.91.0 起已接线(会话枚举面 ∧ mailbox 店 ∧ 收件人生命周期面三合取),缺任一合取项的部署上本旋钮仍「就位待命」;目录行见 `GET /v1/config/catalog`(approval 域,security 轴) |
|
|
1606
1633
|
| `CROSS_SESSION_DIALOG_EXPIRY` | 未设(⇒ 引擎缺省 `5m`) | 同族:CC settings 键名 `dialogExpiry` 的部署层 —— 被 hold 的跨会话消息等待人审多久后按**安全缺省**结算(过期丢弃,**带回执**告知发送方,不静默吞)。词表 `60s`/`5m`/`10m`/`never`(`never` = 不设期限);未设 ⇒ 本仓**不铸键**,由引擎落它自己的缺省(不复制上游缺省值)。词表外拒启。⚠️ 生效前提同上。另注:CC 的同名键还有第二个消费面(转发到远端客户端的审批对话框停靠时长),本旋钮**今天不驱动**那一面 |
|
|
1607
1634
|
|
|
1608
1635
|
**自查读面**(`GET /v1/capabilities.permissionModeAuto`,壳的 `sema doctor permissions` 消费;下例第一行是
|
|
@@ -1657,20 +1684,22 @@ GET /v1/capabilities?permissionMode=default
|
|
|
1657
1684
|
是唯二两条配置来源。当 per-request 那条腿"生效"(见下面的受理门槛)时,它产出一个**全新、完全独立**的
|
|
1658
1685
|
`WebSearchBackendConfig`,**整只替换**掉 env 配出来的 backend —— 不是把 per-request 给的字段一个个覆盖到
|
|
1659
1686
|
env 的 config 上,env 的其余字段(尤其是 `WEB_SEARCH_TIMEOUT_MS`)**不会**被继承到 per-request 那次调用里
|
|
1660
|
-
(`src/capabilities/scenarios.ts
|
|
1687
|
+
(`src/capabilities/scenarios.ts` 的 `requestWebSearchBackend`,判决来自 `src/plugins/web-search.ts` 的
|
|
1688
|
+
`judgeRequestWebSearch`):
|
|
1661
1689
|
|
|
1662
1690
|
```
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1691
|
+
judgeRequestWebSearch(req.settings?.webSearch) // 单用户车道才问;多租车道恒答 absent
|
|
1692
|
+
absent ⇒ 部署后端(env 那只;env 也没配 ⇒ 工具不装配)
|
|
1693
|
+
honored ⇒ 这次调用用 per-request 那只(整段整取)
|
|
1694
|
+
malformed ⇒ 新鲜提交已在受理面 400(含 provider 缺席 / 词表外);只有续跑重放会走到这里 ⇒ 这一次不装配
|
|
1695
|
+
WebSearch(不拿部署后端顶替调用方点名的目的地)+ 一行 warn `web_search_settings_unusable` 点名字段
|
|
1667
1696
|
```
|
|
1668
1697
|
|
|
1669
|
-
|
|
1698
|
+
判决为 `honored`(即 `settings.webSearch` 形对、`provider` 是合法词,**且**本部署是单用户车道)时,
|
|
1670
1699
|
`deps.webSearch`(env 配的 backend)整个不参与这次请求 —— 谁赢是**二选一**,不是字段级合并。
|
|
1671
1700
|
|
|
1672
1701
|
**受理门槛(单用户车道)**:🔒 per-request `settings.webSearch` 只在 `REQUIRE_PRINCIPAL !== true`(单用户 /
|
|
1673
|
-
TOC 本地形)
|
|
1702
|
+
TOC 本地形)时被**采纳**;`REQUIRE_PRINCIPAL=true`(多租户)上一段**形对**的配置**结构性够不着**——不是被拒、也不留任何
|
|
1674
1703
|
`warn`/`capabilities` 位说"你发的 webSearch 被忽略了"(对比 `mcpServers` 有 `capabilities.mcpInjection` +
|
|
1675
1704
|
`mcp_injection_dropped` 日志可读;`settings.webSearch` 没有对应的能力探测位,`src/http/wire-types.ts:320-323`)。
|
|
1676
1705
|
🔴 **别把 S-382 的新位读成这个探测位**:`GET /v1/capabilities` 的 `webSearch.backend`(S-382)说的是
|
|
@@ -1678,17 +1707,20 @@ TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结
|
|
|
1678
1707
|
依旧是结构性够不着、无 warn、无位,这一段的结论一字未变。
|
|
1679
1708
|
原因是安全边界(与多租户能力配置隔离规则同源):per-request `endpoint`/`apiKey` 是能力配置,多租户下一个租户把 `searxng`
|
|
1680
1709
|
指向内网地址就是 SSRF,所以多租户上只认部署 env 配的 backend。
|
|
1710
|
+
**形判不分车道**:一段**形错**的 `settings.webSearch`(见下表「形错」列)在任何车道上都在提交当场答
|
|
1711
|
+
`400 request.field_invalid`,错误句逐字点名字段(`settings.webSearch.<字段> must be …`)——与 `settings.hooks`
|
|
1712
|
+
同姿势:形在受理面判,采不采纳由单用户闸决定。
|
|
1681
1713
|
|
|
1682
|
-
**`settings.webSearch` 键表**(`src/plugins/web-search.ts` `
|
|
1683
|
-
`@sema-agent/sdk` 9.0.0 `settings.d.ts` `SettingsWebSearch`):
|
|
1714
|
+
**`settings.webSearch` 键表**(`src/plugins/web-search.ts` `judgeRequestWebSearch`,每个字段一只判官、与部署 env 的
|
|
1715
|
+
同名旋钮共用;类型契约见 `@sema-agent/sdk` 9.0.0 `settings.d.ts` `SettingsWebSearch`)。整段本身必须是对象(`null` = 缺席):
|
|
1684
1716
|
|
|
1685
|
-
| 键 | 类型 | 必填 | 默认 |
|
|
1717
|
+
| 键 | 类型 | 必填 | 默认 | 形错(⇒ 400) |
|
|
1686
1718
|
|---|---|---|---|---|
|
|
1687
|
-
| `provider` | `"brave"|"tavily"|"searxng"` |
|
|
1688
|
-
| `apiKey` | string(明文) | brave/tavily 建议带(缺了首次调用才报错,见下表);searxng 不读 | 无(不继承 env 的 `WEB_SEARCH_API_KEY`) |
|
|
1689
|
-
| `endpoint` | string | searxng 必需(缺了首次调用才报错);brave/tavily 可选覆盖 | 无(不继承 env 的 `WEB_SEARCH_ENDPOINT`) | `
|
|
1690
|
-
| `searxngParams` | `Record<string,string>`(对象)或 `"
|
|
1691
|
-
| `maxResults` | number | 否 | 10(与 env 同一 clamp,`[1,20]
|
|
1719
|
+
| `provider` | `"brave"|"tavily"|"searxng"` | **是** | 无 | 缺席、不是字符串、或不是三词之一(大小写 / 首尾空白照旧归一)——一段 `settings.webSearch` 就是在点名目的地,认不出就 400 点名 `settings.webSearch.provider`,**不**静默换成部署 env 的 backend(改前:整段丢弃、回落 env backend、不报错)。词表守卫与 env 腿**同一只** `isWebSearchProvider`;env 腿词表外是「不装配」(部署自己的缺席) |
|
|
1720
|
+
| `apiKey` | string(明文) | brave/tavily 建议带(缺了首次调用才报错,见下表);searxng 不读 | 无(不继承 env 的 `WEB_SEARCH_API_KEY`);空串同缺席 | 不是字符串 |
|
|
1721
|
+
| `endpoint` | string | searxng 必需(缺了首次调用才报错);brave/tavily 可选覆盖 | 无(不继承 env 的 `WEB_SEARCH_ENDPOINT`);空串同缺席 | 不是字符串,或不是 `scheme://host[:port][/path]` 形的绝对 URL(`localhost:8888` 这类少了 `scheme://` 的也算) |
|
|
1722
|
+
| `searxngParams` | `Record<string,string>`(对象)或 `"name=value;name=value"`(字符串) | 否 | 无;空串 / 空对象同缺席 | 与 env `WEB_SEARCH_SEARXNG_PARAMS` 同一套文法:数组、数字、一段不是 `name=value`、名不是参数名(字母 / 数字 / `_` / `-` / `.`)、值为空或不是字符串、同名两次。⚠️ **未列入已发布的 `@sema-agent/sdk` 8.8.0 `SettingsWebSearch` 类型**(该接口只有 `provider`/`apiKey`/`endpoint`/`maxResults` 四键、无开放下标)——server 侧代码认这个键,但当前发布的 TS 类型接不到它;手写 JSON 请求体仍可以发,server 会照常解析(源码头注自述:字段和消费点先落地,配置录入面尚未跟上,是一笔尚未还清的既有债务) |
|
|
1723
|
+
| `maxResults` | number | 否 | 10(与 env 同一 clamp,`[1,20]`,小数向下取整;999 夹到 20 不是形错) | 不是数,或 < 1(`"10"` 这种字符串也算) |
|
|
1692
1724
|
| *(无)* `timeoutMs` | — | — | — | **per-request 车道没有这个字段**——即便部署用 `WEB_SEARCH_TIMEOUT_MS` 配了非默认超时,per-request 生效那次调用永远退回 backend 自己的默认(10000ms,下限 1000ms),因为"整段整取"意味着 env 的 `timeoutMs` 根本不在 `reqWebSearch` 那个新对象里 |
|
|
1693
1725
|
| *(无)* `fetchImpl` | — | — | — | 同上,仅测试注入用,per-request 车道不可达 |
|
|
1694
1726
|
|
|
@@ -1696,33 +1728,27 @@ TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结
|
|
|
1696
1728
|
(`src/task-settings.ts:309`,集外顶层键 400 `request.body_shape`),但 `webSearch` **自己的子键没有闭集门**
|
|
1697
1729
|
——不像 `settings.permissions.*` 有 `TASK_SETTINGS_PERMISSION_KEYS` 逐键拒(`src/task-settings.ts:367-403`
|
|
1698
1730
|
的 `taskSettingsKeyIssue` 只扫 `settings.*` 顶层和 `settings.permissions.*` 两层)。`settings.webSearch` 下
|
|
1699
|
-
塞一个上表之外的键(拼错的 `mxResults` 之类)**不会** 400
|
|
1700
|
-
——门槛之外没有"未知键"
|
|
1731
|
+
塞一个上表之外的键(拼错的 `mxResults` 之类)**不会** 400,会被判官静默无视
|
|
1732
|
+
——门槛之外没有"未知键"这一说(子键闭集是另一件事,未随本版做)。
|
|
1701
1733
|
|
|
1702
|
-
**错误形**(
|
|
1703
|
-
`tool_result` 错误文本,回到模型的对话里,**不是** HTTP 层 `errorCode`,**不会**使 `TaskResult` 整体
|
|
1704
|
-
`failed
|
|
1734
|
+
**错误形**(形错已在上表:提交当场 400。下表是**形对**之后的失败,全部经**首次真实工具调用**才现形,均为
|
|
1735
|
+
WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是** HTTP 层 `errorCode`,**不会**使 `TaskResult` 整体
|
|
1736
|
+
`failed`;core `dist/tools/web.js` `createWebSearchTool` 统一兜底,失败卡上的 `retryable` 由 core 从错误文本分档):
|
|
1705
1737
|
|
|
1706
1738
|
| 触发条件 | 现象 | 是否 retryable(core `classifySearchFailure`) |
|
|
1707
1739
|
|---|---|---|
|
|
1708
|
-
| 坏 `provider`(`settings.webSearch.provider` 非三词之一,或缺席) | **不是错误** —— `webSearchConfigFromSettings` 返回 `undefined`,整段回落到 env 配的 backend(env 也没配 ⇒ WebSearch 工具不装配,模型看不到这个工具);无 warn、无日志 | 不适用 |
|
|
1709
1740
|
| 缺 `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` 落最后一条默认分支) |
|
|
1710
|
-
| `
|
|
1711
|
-
| ⚠️ `searxngParams` 是**数组**(如 `["engines=bing"]`) | `typeof [] === "object"` 让它落进对象分支:`Object.entries` 按数组下标产出 `{"0":"engines=bing"}`——**不被拒绝**,但产出的键是数字字符串、值是未拆分的原始 `"k=v"` 串,传给 core adapter 的 `extraParams` 后是一组没有意义的查询参数(不是解析出 `engines=bing`)。这条边角只在 per-request 车道可达(env 值恒为字符串,不会触发);已用与源码逐字一致的独立复现脚本核验(见收车档),未改代码 | 不适用(不是异常路径) |
|
|
1712
|
-
| 搜索后端返回**非 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`——都取决于上游返回的具体措辞,不是本仓能保证的 |
|
|
1741
|
+
| 搜索后端返回**非 2xx HTTP 响应**(鉴权失败/限流/服务端 5xx 等) | brave / tavily:一句形 `"<provider> search failed: HTTP <status> — <响应体首 200 字符>"`(`src/plugins/web-search.ts` 的 `httpFailure`,两腿唯一铸点);searxng:`"SearXNG <status> <statusText> from <url>"`(core adapter 自己抛,本仓不改写) | brave / tavily:**按状态码分档** —— 429 与 5xx ⇒ `true`,其余 4xx ⇒ `false`(句里带 `HTTP <status>` 令牌,core 的分档认得出)。⚠️ searxng 腿**仍是 `"unknown"`**:core adapter 那句话的状态码前没有状态词,分档认不出 —— 归 core 修(源头),修前只有上游错误体里碰巧写着 `rate limit` / `timed out` 时才分得对 |
|
|
1713
1742
|
| `endpoint` **网络层**不可达(连接被拒/DNS 解析失败/fetch 自身抛错,尚未拿到任何 HTTP 响应) | `fetch` 抛出的传输层错误(`ECONNREFUSED`/`ENOTFOUND`/`fetch failed` 等 Node/undici 标准措辞)被同一 `catch` 接住 | `true`——这条路径的错误文本天然含 `econnrefused`/`enotfound`/`fetch failed`/`dns` 等词,`classifySearchFailure` 的网络故障正则能命中(与上一行"已拿到 HTTP 响应但非 2xx"是两条不同的失败路径,别混淆) |
|
|
1714
1743
|
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
调用方以为自己指定的那个;工具存在这一事实本身**不能**证明 per-request 的 `provider`/`endpoint` 真的
|
|
1721
|
-
生效了,两种情形(per-request 生效 / per-request 被静默丢弃回落 env)在壳侧不可判别(此条经独立复现验证,
|
|
1722
|
-
见收车档)。
|
|
1744
|
+
✅ **"坏 provider 静默回落"这一形自 7.100.0 起没有了**:改前部署 env 配了合法 backend(例如 `WEB_SEARCH_PROVIDER=brave`)
|
|
1745
|
+
时,调用方 per-request 传一个拼错的 `provider`(如 `"searx"`)、或带着一个本想打到自建 SearXNG 的 `endpoint`,
|
|
1746
|
+
`settings.webSearch` 整段被丢弃、静默回落到 env 的 brave backend —— 工具照常挂载,查询发给了 env 配的 provider,
|
|
1747
|
+
壳侧判别不了。现在同一请求在提交当场答 `400 request.field_invalid` 点名 `settings.webSearch.provider`;WebSearch
|
|
1748
|
+
工具在场 ⇔ 调用方的 per-request 配置被采纳(单用户车道)或调用方根本没带(用部署默认)。
|
|
1723
1749
|
|
|
1724
1750
|
**`apiKey` 明文与 env 槽边界**:server 收到的 `settings.webSearch.apiKey` 是**明文字符串**,没有任何服务端
|
|
1725
|
-
密钥槽位/引用间接——收到什么字符串就直接进 backend 闭包(
|
|
1751
|
+
密钥槽位/引用间接——收到什么字符串就直接进 backend 闭包(判官 `judgeRequestWebSearch` 只判「是不是字符串」,与 `WEB_SEARCH_API_KEY` 同一只判官、同一条消费路径,
|
|
1726
1752
|
见 `src/plugins/web-search.ts` 的 `WebSearchBackendConfig.apiKey` 字段注)。**server 本批不改受理面**:明文字段的形状维持原样。调用方(壳)如何在
|
|
1727
1753
|
自己机器上管理这份明文是调用方的事——例如 cli 壳侧的约定是在**调用方自己的环境**里按
|
|
1728
1754
|
`SEMA_WEBSEARCH_KEY_<PROVIDER>` 这样的命名空间存放每个 provider 的 key,由壳在本地读出后把明文塞进请求体;
|
|
@@ -10,50 +10,74 @@
|
|
|
10
10
|
# has_env / `. "$ENVF"` / `printf '%s=%s' >> "$ENVF"` / `grep … "$ENVF"` —— 第二套写读规则迟早分叉
|
|
11
11
|
# (test/deploy-lanes-contract.test.ts「S-618 续」两道机器钉)。
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
13
|
+
# **一条文法,两半共用。** 文法的依据是 compose-go dotenv 的**真实**读法(车MV / S-646,k3s8 docker compose 2.40.3
|
|
14
|
+
# 逐形实跑的语料,金样钉在 test/deploy-lanes-contract.test.ts「S-656 + S-646」):一行 `KEY=<右半>` 的右半有三种形,
|
|
15
|
+
# 每种形里只有少数几个字符会让 compose **改写**读到的值 —— 不碰这几个字符,compose 读到的就是字面量:
|
|
16
|
+
# · 裸值 `KEY=v`:插值起点(`$` 后跟 `$` / `{` / 字母或 `_`,见 envf_has_interp)、「空格 + `#`」(行内注释;只认 ASCII 空格,`\t#` 是字面)、首尾空白(修剪 —— 开头按
|
|
17
|
+
# compose-go 自己的 isSpace = ASCII 空白 + U+0085 + U+00A0,结尾按 Go `unicode.IsSpace`,多出全角空格等 16 个)、
|
|
18
|
+
# 首字符是 `'` / `"`(转去引号形)。**`\` 在裸值里是字面量**(`k\x` ⇒ `k\x`,`abc\` ⇒ `abc\`,不续行);
|
|
19
|
+
# 空白、`;`、反引号、引号(不在首位)也都是字面量。
|
|
20
|
+
# · 单引号 `KEY='c'`:只有 `\'` 是转义(转义引号)。`\\` `\x` 原样。于是 c 里不能有 `'`,且 c 不能以**奇数个** `\`
|
|
21
|
+
# 结尾(最后那个 `\` 会把闭引号吃成转义引号:`unterminated quoted value`,或把后面几行吞进这个值、后续键静默落缺省)。
|
|
22
|
+
# 以偶数个 `\` 结尾照常闭合(`'abc\\'` ⇒ `abc\\`)。
|
|
23
|
+
# · 双引号 `KEY="c"`:`\` 走一张转义表(`\n` `\\` `\"` `\$` `\0NNN` …)、插值起点同裸值。c 里不碰 `"` `\`、没有插值起点
|
|
24
|
+
# 就是字面量。
|
|
25
|
+
# (刻意不认带 `\` 的双引号值:认它就得在这里抄 compose 的转义表。)
|
|
26
|
+
# 任何形之后同一行不许再有东西(闭引号后的二次赋值 = SL;行尾注释):规范行的右半整段就是这一形。
|
|
27
27
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# `x;
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
28
|
+
# · 读(`envf_canonical_line` + `envf_decode`,读口 envf_read 的判据):右半是上面三形之一**且不碰改写字符** ⇒ 认,
|
|
29
|
+
# 按字面解(裸值原样、引号形剥一对引号)—— 这正是 compose 读到的值;否则响亮拒并点名行号(不猜 compose 会改成什么)。
|
|
30
|
+
# 所以 ≤7.97.0 写口原样落盘的值(`p%ss;x!`、`x; touch …`、`k\x`、以 `\` 结尾……)只要 compose 按字面读,重跑就读得回。
|
|
31
|
+
# · 写(`envf_representable` + `envf_encode`):只写**更窄**的一形 —— 两个读者(compose 与 bash `source`)都读成同一字面量
|
|
32
|
+
# 的形。本脚本族 7.98.0 起已不 source `.env`,但运维自己的脚本、没升级的旧版 sema-up / kube-up 仍可能 source 它,写口
|
|
33
|
+
# 不替它们开执行面:安全裸词(ENVF_WORD)原样裸写;其余一律整值单引号(单引号里两个读者都字面:`$()` / 反引号 / `;`
|
|
34
|
+
# / 空白 / `\` 都不执行不展开)。**确实不可表示**的只有:换行 / 回车(一行一条赋值)、单引号本身(整值单引号里放不下;
|
|
35
|
+
# 裸写则 bash 语法错)、以奇数个 `\` 结尾(单引号形 compose 闭引号失效;裸写则 bash 当续行吞下一行)⇒ **写之前**响亮拒,
|
|
36
|
+
# 点名键、不回显值,「下一步」按拒因给。写口产物恒在读口文法内(安全裸词 ⊂ 裸值形,单引号编码形 ⊂ 单引号形)⇒ 往返成立。
|
|
34
37
|
#
|
|
35
38
|
# 收编(S-618):7.96.0「写口只拒换行 / 回车」的判据、`envf_get` 的物理行假设,连同曾经的「另立
|
|
36
39
|
# 一件做渲染对账」都收进来——写口判据 = 本文件的 `envf_representable`;物理行假设的兜底 = 生成 `.env`
|
|
37
40
|
# 之后、`compose up` 之前那一道 `docker compose config` 渲染对账(sema-up.sh `reconcile_render`,
|
|
38
41
|
# 以 compose 自己为唯一属主)。不再有第二套文法。
|
|
42
|
+
#
|
|
43
|
+
# 🔴 安全裸词是**正向白名单**(不是「排除危险字符」的负向类):`;` `&` `|` `<` `>` `(` `)` 这些 shell 命令分隔符 / 重定向
|
|
44
|
+
# 若留在写口的裸词里,任何 `source` 这份 .env 的读者会把 `x;id` 执行掉。决策键的合法值(true/false/host/e2b/k8s)、
|
|
45
|
+
# 端口(数字)、十六进制口令、URL(`https://h:port/p`)、镜像引用(`ghcr.io/x/y:1.0`)全在白名单内 ⇒ 仍裸写,.env 升级零 churn。
|
|
39
46
|
ENVF_WORD='[A-Za-z0-9_./:+=@-]'
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# (
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
# compose 修剪裸值首尾时认的空白,逐字列出(UTF-8 字节形),不用 `[[:space:]]`:那个类随区域变(C 区域只认 ASCII,UTF-8
|
|
48
|
+
# 区域可能多认全角空格)⇒ 同一份 .env 换个 LANG 判定就变。k3s8 compose 2.40.3 实测(车MV 语料):开头只修剪 compose-go 的
|
|
49
|
+
# isSpace(ASCII 空白 + U+0085 + U+00A0;开头的全角空格 / U+2000 是字面);结尾修剪 Go `unicode.IsSpace` 全集。7.98.0 的
|
|
50
|
+
# 读口没列非 ASCII 这几个 ⇒ `KEY=a<全角空格>` 读口读成 `a<全角空格>`、compose 读成 `a`(静默分叉,车MV 金样实测)。
|
|
51
|
+
ENVF_SPACE_LEAD=(' ' $'\t' $'\v' $'\f' $'\r' $'\xc2\x85' $'\xc2\xa0')
|
|
52
|
+
ENVF_SPACE_TRAIL=("${ENVF_SPACE_LEAD[@]}" $'\xe1\x9a\x80' $'\xe2\x80\x80' $'\xe2\x80\x81' $'\xe2\x80\x82' $'\xe2\x80\x83' \
|
|
53
|
+
$'\xe2\x80\x84' $'\xe2\x80\x85' $'\xe2\x80\x86' $'\xe2\x80\x87' $'\xe2\x80\x88' $'\xe2\x80\x89' $'\xe2\x80\x8a' \
|
|
54
|
+
$'\xe2\x80\xa8' $'\xe2\x80\xa9' $'\xe2\x80\xaf' $'\xe2\x81\x9f' $'\xe3\x80\x80')
|
|
55
|
+
|
|
56
|
+
# envf_odd_trailing_bs <s> —— s 以**奇数个** `\` 结尾 ⇒ 0。读口(单引号形)与写口(representable)同用这一只。
|
|
57
|
+
envf_odd_trailing_bs() {
|
|
58
|
+
local s="$1" n=0
|
|
59
|
+
while [ "${s%\\}" != "$s" ]; do s="${s%\\}"; n=$((n + 1)); done
|
|
60
|
+
[ $((n % 2)) -eq 1 ]
|
|
61
|
+
}
|
|
46
62
|
|
|
47
|
-
# envf_representable <value> ——
|
|
48
|
-
# 不能 ⇒ 非零 +
|
|
63
|
+
# envf_representable <value> —— 值能否写成写口的窄形(安全裸词或整值单引号)。
|
|
64
|
+
# 不能 ⇒ 非零 + 拒因写进 `ENVF_REJECT`、这一拒因的出路写进 `ENVF_REJECT_NEXT`(两者都**不含键、不含值**)。
|
|
49
65
|
envf_representable() {
|
|
50
66
|
case "$1" in
|
|
51
|
-
*$'\n'*) ENVF_REJECT='值里有换行'; return 1 ;;
|
|
52
|
-
*$'\r'*) ENVF_REJECT='值里有回车'; return 1 ;;
|
|
53
|
-
*\'*)
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
*$'\n'*) ENVF_REJECT='值里有换行'; ENVF_REJECT_NEXT='去掉值里的换行(.env 一行一条赋值,多行值写不进去)'; return 1 ;;
|
|
68
|
+
*$'\r'*) ENVF_REJECT='值里有回车'; ENVF_REJECT_NEXT='去掉值里的回车(.env 一行一条赋值)'; return 1 ;;
|
|
69
|
+
*\'*)
|
|
70
|
+
ENVF_REJECT='值里有单引号(整值单引号编码形里放不下单引号;裸写则任何 source 这份 .env 的 shell 会语法错)'
|
|
71
|
+
ENVF_REJECT_NEXT='换一个不含单引号的值(凭据可在签发方重新生成)'
|
|
72
|
+
return 1 ;;
|
|
56
73
|
esac
|
|
74
|
+
if envf_odd_trailing_bs "$1"; then
|
|
75
|
+
ENVF_REJECT='值以奇数个反斜杠结尾(单引号编码形里 compose 把最后的反斜杠与闭引号读成转义引号、闭引号失效;裸写则任何 source 这份 .env 的 shell 把它当续行)'
|
|
76
|
+
ENVF_REJECT_NEXT='换一个不以反斜杠结尾的值(结尾是偶数个反斜杠的值写得进;凭据可在签发方重新生成)'
|
|
77
|
+
return 1
|
|
78
|
+
fi
|
|
79
|
+
ENVF_REJECT='' ENVF_REJECT_NEXT=''
|
|
80
|
+
return 0
|
|
57
81
|
}
|
|
58
82
|
|
|
59
83
|
# envf_encode <value> —— 序列化出 `.env` 一行赋值的**右半**(值部分)。调用方须先过
|
|
@@ -67,15 +91,59 @@ envf_encode() {
|
|
|
67
91
|
fi
|
|
68
92
|
}
|
|
69
93
|
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
94
|
+
# envf_has_interp <s> —— s 里有 compose 插值的起点 ⇒ 0。compose-go 的 template 只认三种:`$$`(转义)、`${`(花括号;
|
|
95
|
+
# 里面不合法就整行报 Invalid template)、`$` + 变量名首字符。变量名首字符是 `[_a-z]` 在 Go `(?i)` 下的大小写折叠闭包 =
|
|
96
|
+
# ASCII 字母 + `_` + U+212A(开尔文符号,折叠到 k)+ U+017F(长 s,折叠到 s)—— 后两个是 codex 修复验证轮 [medium] 指出、
|
|
97
|
+
# k3s8 compose 2.40.3 22:14 实测确认的(`a$K` ⇒ `a`);`İ` `ı` `µ` `Å` 不在其列。其余的 `$`(后跟数字 / 标点 / 空白 /
|
|
98
|
+
# 其他非 ASCII / 串尾)是字面量(`p$9x` ⇒ `p$9x`,`p$` ⇒ `p$`,`a$é` ⇒ `a$é`;codex 一审 [medium])。
|
|
99
|
+
# 字母逐字列出、不写 `[a-z]` 区间:区间在 bash 3.2 的 UTF-8 区域按排序规则展开,会把 `é` 这类也算进去;两个折叠字符按
|
|
100
|
+
# UTF-8 字节串匹配,与区域无关。
|
|
101
|
+
ENVF_FOLD_KELVIN=$'\xe2\x84\xaa' ENVF_FOLD_LONG_S=$'\xc5\xbf'
|
|
102
|
+
envf_has_interp() {
|
|
103
|
+
case "$1" in
|
|
104
|
+
*'$$'* | *'${'* | *'$'[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_]*) return 0 ;;
|
|
105
|
+
*'$'"$ENVF_FOLD_KELVIN"* | *'$'"$ENVF_FOLD_LONG_S"*) return 0 ;;
|
|
106
|
+
esac
|
|
107
|
+
return 1
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# envf_rhs_literal <rhs> —— 右半是否是 compose 按字面读的三形之一(见文件头)。0 = 是(`envf_decode` 解出的就是
|
|
111
|
+
# compose 读到的值)。纯 bash 模式匹配,不起子进程;bash 3.2 可用。
|
|
112
|
+
envf_rhs_literal() {
|
|
113
|
+
local v="$1" c u
|
|
114
|
+
case "$v" in
|
|
115
|
+
'') return 0 ;;
|
|
116
|
+
\'*\')
|
|
117
|
+
c="${v:1:${#v}-2}"
|
|
118
|
+
case "$c" in *\'*) return 1 ;; esac
|
|
119
|
+
envf_odd_trailing_bs "$c" && return 1
|
|
120
|
+
return 0 ;;
|
|
121
|
+
\"*\")
|
|
122
|
+
c="${v:1:${#v}-2}"
|
|
123
|
+
case "$c" in *\"* | *\\*) return 1 ;; esac
|
|
124
|
+
envf_has_interp "$c" && return 1
|
|
125
|
+
return 0 ;;
|
|
126
|
+
\'* | \"* | *" #"*) return 1 ;;
|
|
127
|
+
esac
|
|
128
|
+
envf_has_interp "$v" && return 1
|
|
129
|
+
for u in "${ENVF_SPACE_LEAD[@]}"; do
|
|
130
|
+
case "$v" in "$u"*) return 1 ;; esac
|
|
131
|
+
done
|
|
132
|
+
for u in "${ENVF_SPACE_TRAIL[@]}"; do
|
|
133
|
+
case "$v" in *"$u") return 1 ;; esac
|
|
134
|
+
done
|
|
135
|
+
return 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# envf_canonical_line <key> <line> —— 该**整行**是否是 <key> 的规范赋值行:行首顶格 `KEY=`(无 export、键旁无空格)
|
|
139
|
+
# + 右半是 compose 按字面读的一形(envf_rhs_literal)。0 = 是。
|
|
74
140
|
envf_canonical_line() {
|
|
75
|
-
|
|
141
|
+
case "$2" in "$1="*) ;; *) return 1 ;; esac
|
|
142
|
+
envf_rhs_literal "${2#"$1="}"
|
|
76
143
|
}
|
|
77
144
|
|
|
78
|
-
# envf_decode <rhs> ——
|
|
145
|
+
# envf_decode <rhs> —— 规范行右半的字面值:引号形剥掉一对同型引号,裸值原样(空 = 未设)。只对过了 envf_rhs_literal
|
|
146
|
+
# 的右半调用 —— 那时这就是 compose 读到的值(裸值不会以引号开头,所以不会被误剥)。
|
|
79
147
|
envf_decode() {
|
|
80
148
|
local v="$1"
|
|
81
149
|
case "$v" in \"*\") v="${v#\"}"; v="${v%\"}" ;; \'*\') v="${v#\'}"; v="${v%\'}" ;; esac
|
|
@@ -112,7 +180,7 @@ envf_read() {
|
|
|
112
180
|
line="${hits#*:}"
|
|
113
181
|
if ! envf_canonical_line "$k" "$line"; then
|
|
114
182
|
envf_log "⛔ $ENVF 第 $nums 行给 $k 赋值,但不是本脚本认得的规范形"
|
|
115
|
-
envf_log " ↳ 下一步:改成 $k=<值>(行首顶格、不带 export
|
|
183
|
+
envf_log " ↳ 下一步:改成 $k=<值>(行首顶格、不带 export、键旁无空格、行尾无注释;值是 compose 按字面读的一形:裸写 = 不以空白或引号开头、不以空白结尾、不含插值(\$ 后跟 \$ / { / 字母或 _)、不含「空格+#」;整值单引号 = 里面没有单引号、不以奇数个反斜杠结尾;整值双引号 = 里面没有双引号 / 反斜杠 / 插值),或删掉这一行"
|
|
116
184
|
envf_log " ↳ 为什么不猜:这一行 compose 怎么读(注释、空白、展开、闭引号后的二次赋值)与本脚本怎么读一旦不一致,容器拿到的就不是这里读到的值"
|
|
117
185
|
return 1
|
|
118
186
|
fi
|
|
@@ -137,8 +205,8 @@ envf_load() {
|
|
|
137
205
|
# envf_guard <key> <value> —— 值无法表示 ⇒ 写之前响亮拒(点名键、不回显值),退整个脚本。
|
|
138
206
|
envf_guard() {
|
|
139
207
|
envf_representable "$2" && return 0
|
|
140
|
-
envf_log "⛔ 要写进 $ENVF 的 $1 的值无法安全写成一条 .env 赋值($ENVF_REJECT)
|
|
141
|
-
envf_log " ↳
|
|
208
|
+
envf_log "⛔ 要写进 $ENVF 的 $1 的值无法安全写成一条 .env 赋值($ENVF_REJECT)"
|
|
209
|
+
envf_log " ↳ 下一步:$ENVF_REJECT_NEXT —— 查 $1 对应的命令行参数或 -f 配置项后重跑;拒因不回显值"
|
|
142
210
|
exit 1
|
|
143
211
|
}
|
|
144
212
|
# envf_put —— `.env` 的**唯一**追加口:一次写入恰好一个物理行,值按文法序列化。
|