@sema-agent/server 7.99.0 → 7.101.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 +35 -1
- package/README.md +12 -1
- package/README.zh-CN.md +9 -1
- package/USAGE.md +150 -56
- package/dist/approval-ask-machine.d.ts +10 -0
- package/dist/approval-ask-machine.js +3 -0
- package/dist/approval-card.d.ts +86 -1
- package/dist/approval-card.js +42 -8
- package/dist/approval-policy-settlement.d.ts +31 -0
- package/dist/approval-policy-settlement.js +16 -0
- package/dist/approval-reconciler.d.ts +8 -1
- package/dist/approval-reconciler.js +5 -4
- 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 +114 -90
- package/dist/boot/execution-env.d.ts +1 -2
- package/dist/boot/execution-env.js +21 -3
- 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/parked-revive-gate.js +1 -0
- package/dist/boot/reapers.js +1 -0
- package/dist/boot/resolve-spec.d.ts +0 -1
- package/dist/boot/resolve-spec.js +28 -10
- package/dist/boot/runner-deps.d.ts +2 -2
- package/dist/boot/runner-deps.js +9 -3
- package/dist/boot/stage-02-registry-pull.js +9 -0
- 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/boot/stores.js +4 -0
- package/dist/capabilities/center-plugins.d.ts +14 -2
- package/dist/capabilities/center-plugins.js +13 -5
- 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/config-catalog.d.ts +2 -2
- package/dist/config-catalog.js +16 -15
- package/dist/config-center/apply-effective.d.ts +59 -39
- package/dist/config-center/apply-effective.js +231 -120
- package/dist/config-center/apply-ledger.d.ts +45 -16
- package/dist/config-center/apply-ledger.js +28 -18
- package/dist/config-center/facade.d.ts +12 -6
- package/dist/config-center/facade.js +2 -2
- 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 +4 -1
- package/dist/config-center/http-client.js +15 -10
- package/dist/config-center/read-warnings.d.ts +115 -6
- package/dist/config-center/read-warnings.js +115 -5
- package/dist/config-center/restart-signal.d.ts +21 -14
- package/dist/config-center/restart-signal.js +16 -22
- package/dist/config-center/types.d.ts +9 -3
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +15 -0
- package/dist/config-provider.d.ts +4 -0
- package/dist/config-provider.js +64 -9
- package/dist/config-types.d.ts +57 -13
- package/dist/config.js +87 -22
- package/dist/cross-session-settings.js +1 -1
- package/dist/deployment-governance.d.ts +45 -12
- package/dist/deployment-governance.js +37 -9
- package/dist/env-name-allowlist-knobs.d.ts +37 -0
- package/dist/env-name-allowlist-knobs.js +24 -0
- package/dist/fleet-client.d.ts +2 -1
- package/dist/hooks/hook-llm.js +13 -4
- package/dist/host-lsp-manager.d.ts +41 -0
- package/dist/host-lsp-manager.js +15 -0
- package/dist/http/active-run-conflict.d.ts +2 -12
- package/dist/http/active-run-conflict.js +1 -6
- package/dist/http/admission.js +30 -7
- package/dist/http/memory-store-refusal-reply.d.ts +24 -0
- package/dist/http/memory-store-refusal-reply.js +10 -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 +3 -2
- package/dist/http/routes/capabilities.js +3 -3
- package/dist/http/routes/memory-bundle.js +3 -0
- package/dist/http/routes/memory-compliance.js +3 -0
- package/dist/http/routes/memory-origin.js +3 -0
- package/dist/http/routes/sessions.js +7 -0
- package/dist/http/routes/side-query.js +10 -4
- 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/instruction-source-notice.d.ts +97 -0
- package/dist/instruction-source-notice.js +78 -0
- package/dist/leader/wire.d.ts +20 -12
- package/dist/leader/wire.js +7 -6
- package/dist/memory-layer-legacy-lock.d.ts +14 -0
- package/dist/memory-layer-legacy-lock.js +74 -0
- package/dist/memory-store-refusal.d.ts +37 -0
- package/dist/memory-store-refusal.js +14 -0
- package/dist/model-entry-refusal.d.ts +130 -0
- package/dist/model-entry-refusal.js +157 -0
- package/dist/model-provider.d.ts +9 -3
- package/dist/model-route-endpoint.d.ts +1 -1
- package/dist/model-select.d.ts +49 -4
- package/dist/model-select.js +48 -17
- 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 +16 -4
- package/dist/observability/fail-open.js +16 -4
- package/dist/observability/metrics.js +1 -0
- package/dist/observability/run-terminal.js +1 -0
- package/dist/observability/secret-env-scrub.d.ts +27 -3
- package/dist/observability/secret-env-scrub.js +22 -6
- 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/caching-session-store.d.ts +6 -0
- package/dist/plugins/caching-session-store.js +6 -0
- 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/local-session-store.d.ts +20 -25
- package/dist/plugins/local-session-store.js +111 -64
- 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/pg-session-storage.d.ts +7 -0
- package/dist/plugins/pg-session-storage.js +19 -0
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +3 -6
- package/dist/plugins/sql-json-column.d.ts +16 -3
- package/dist/plugins/sql-json-column.js +15 -4
- package/dist/plugins/tidb-pool.js +5 -0
- package/dist/plugins/tidb-session-store.d.ts +13 -0
- package/dist/plugins/tidb-session-store.js +14 -0
- package/dist/plugins/web-search.d.ts +44 -23
- package/dist/plugins/web-search.js +161 -50
- package/dist/project-memory.d.ts +10 -0
- package/dist/project-memory.js +28 -14
- package/dist/run-local.d.ts +17 -15
- package/dist/run-local.js +119 -18
- package/dist/runs.js +6 -5
- package/dist/runtime-governance.d.ts +50 -3
- package/dist/runtime-governance.js +5 -0
- package/dist/sealed-key.d.ts +5 -2
- package/dist/server-secret-env.d.ts +26 -2
- package/dist/server-secret-env.js +9 -0
- package/dist/task-settings.d.ts +1 -1
- package/dist/tool-approval.d.ts +150 -28
- package/dist/tool-approval.js +150 -54
- package/dist/trace/core-keyset-guard.d.ts +30 -8
- package/dist/trace/engine-notice-wire.d.ts +27 -1
- package/dist/trace/engine-notice-wire.js +8 -0
- 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 +3 -3
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { type AcquiredSession, type PlacedSessionRow, type SessionPlacement, type SessionPlacementRecord, type SessionStore, type SessionTreeEntry } from "@sema-agent/core";
|
|
14
14
|
import type { Pool } from "mysql2/promise";
|
|
15
|
+
import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
15
16
|
import type { SessionSummary } from "./store-contracts.js";
|
|
16
17
|
import { type PlacementAcquireOpts } from "./session-placement.js";
|
|
17
18
|
import { type StagingHandle } from "../session-sync-kernel.js";
|
|
@@ -115,6 +116,18 @@ export declare class TiDBSessionStore implements SessionStore {
|
|
|
115
116
|
/** Write-once auto-title: lands ONLY when title IS NULL (a concurrent/prior title wins;
|
|
116
117
|
* the user-facing rename layer is BFF-side). Returns whether this call set it. */
|
|
117
118
|
setTitleIfNull(sessionId: string, title: string): Promise<boolean>;
|
|
119
|
+
/**
|
|
120
|
+
* S-690 —— 换源通告的基线读(一行 PK SELECT;列的三态读法见 `tidb-pool.ts` 的列注)。行不在 ⇒ 读作「没记过」
|
|
121
|
+
* (loader 跑在 acquire 之后,行恒在;不在 = 被带外删了,没有下一 run 可比)。
|
|
122
|
+
*/
|
|
123
|
+
readInstructionSource(sessionId: string): Promise<InstructionSourceBaseline>;
|
|
124
|
+
/**
|
|
125
|
+
* S-690 —— CAS 覆写基线(期望值 = 调用方读到的那一份;不符 ⇒ 0 行、不抛 —— 契约见 `InstructionSourceStore`)。
|
|
126
|
+
* **不**碰 `updated_at`(那是会话活动时刻,留存腿按它判龄;记基线不是一次会话活动)。
|
|
127
|
+
* 两条基线查询都带每查询超时(`BASELINE_IO_DEADLINE_MS`,mysql2 的 `{sql, timeout}` 形):被行锁卡住的写由查询自己
|
|
128
|
+
* 在期限内拒。⚠️ mysql2 的超时是客户端侧的 —— 拒之后那条连接仍被挂着的命令占到服务端结束(S-9 同形,如实披露)。
|
|
129
|
+
*/
|
|
130
|
+
recordInstructionSource(sessionId: string, expected: InstructionSourceBaseline, source: string | null): Promise<void>;
|
|
118
131
|
/**
|
|
119
132
|
* E17 — fork a session: copy its WHOLE durable history to a NEW uuidv7 session owned by `owner`, mirroring
|
|
120
133
|
* core's `PgSessionRepo.fork`. Entry ids / parent links are per-session and copy VERBATIM; only `seq` is
|
|
@@ -3,6 +3,7 @@ import { TiDBSessionStorage } from "./tidb-session-storage.js";
|
|
|
3
3
|
import { decodeSessionEventPayload } from "./session-event-payload.js";
|
|
4
4
|
import { jsonColumnText } from "./sql-json-column.js";
|
|
5
5
|
import { escapeLike } from "./sql-escape.js";
|
|
6
|
+
import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
|
|
6
7
|
import { MANAGED_RETENTION } from "./retention-store-sql.js";
|
|
7
8
|
import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
|
|
8
9
|
import { assertPlacementAdmission, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertValues, SQL_SESSION_PLACEMENTS, } from "./session-placement.js";
|
|
@@ -183,6 +184,19 @@ export class TiDBSessionStore {
|
|
|
183
184
|
const [r] = await this.pool.query("UPDATE session_meta SET title = ? WHERE session_id = ? AND title IS NULL", [title, sessionId]);
|
|
184
185
|
return r.affectedRows > 0;
|
|
185
186
|
}
|
|
187
|
+
async readInstructionSource(sessionId) {
|
|
188
|
+
const [rows] = await this.pool.query({ sql: "SELECT instruction_source FROM session_meta WHERE session_id = ?", timeout: BASELINE_IO_DEADLINE_MS }, [sessionId]);
|
|
189
|
+
return decodeInstructionSource(rows[0]?.instruction_source);
|
|
190
|
+
}
|
|
191
|
+
async recordInstructionSource(sessionId, expected, source) {
|
|
192
|
+
const next = encodeInstructionSource(source);
|
|
193
|
+
if (expected.recorded) {
|
|
194
|
+
await this.pool.query({ sql: "UPDATE session_meta SET instruction_source = ? WHERE session_id = ? AND instruction_source = ?", timeout: BASELINE_IO_DEADLINE_MS }, [next, sessionId, encodeInstructionSource(expected.source)]);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
await this.pool.query({ sql: "UPDATE session_meta SET instruction_source = ? WHERE session_id = ? AND instruction_source IS NULL", timeout: BASELINE_IO_DEADLINE_MS }, [next, sessionId]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
186
200
|
async fork(sourceId, owner) {
|
|
187
201
|
const [metaRows] = await this.pool.query("SELECT leaf_id FROM session_meta WHERE session_id = ?", [sourceId]);
|
|
188
202
|
if (!metaRows[0])
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* backend here is what makes `assembleFullBodyTools({ webSearch })` actually mount the WebSearch tool.
|
|
5
5
|
*
|
|
6
6
|
* Multi-provider, picked by `WEB_SEARCH_PROVIDER`:
|
|
7
|
-
* - `brave` — Brave Web Search API (GET, `X-Subscription-Token` header)
|
|
7
|
+
* - `brave` — Brave Web Search API (GET, `X-Subscription-Token` header); with `WEB_SEARCH_ENDPOINT` set to a
|
|
8
|
+
* Brave-compatible gateway's full search URL (e.g. a self-hosted Nólë gateway, S-669) the same leg
|
|
9
|
+
* serves that backend — no new provider word, `capabilities.webSearch.backend` still reads `brave`
|
|
8
10
|
* - `tavily` — Tavily Search API (POST, `Authorization: Bearer`, native include/exclude_domains)
|
|
9
11
|
* - `searxng` — a self-hosted SearXNG instance (GET `?format=json`, no auth)
|
|
10
12
|
*
|
|
@@ -137,23 +139,10 @@ export interface WebSearchBackend {
|
|
|
137
139
|
export declare function setWebSearchBadPayloadObserver(fn: ((provider: string) => void) | undefined): void;
|
|
138
140
|
/**
|
|
139
141
|
* Build the `WebSearchConfig` (a `search` fn + `maxResults`) for `assembleFullBodyTools({ webSearch })`. The API key
|
|
140
|
-
* stays captured in this closure — it is never surfaced to the model or the tool args.
|
|
142
|
+
* stays captured in this closure — it is never surfaced to the model or the tool args. Every error a leg throws leaves
|
|
143
|
+
* through {@link redactedBackendFailure} (S-716 ③).
|
|
141
144
|
*/
|
|
142
145
|
export declare function createWebSearchBackend(cfg: WebSearchBackendConfig): WebSearchBackend;
|
|
143
|
-
/**
|
|
144
|
-
* `WEB_SEARCH_SEARXNG_PARAMS` / `settings.webSearch.searxngParams` 的解析 —— SearXNG 实例侧参数
|
|
145
|
-
* (`engines=` / `language=` 等)。
|
|
146
|
-
*
|
|
147
|
-
* 🔴 [ref] cli 逐串直证:3.18.0 我加了 `searxngParams` 字段和消费点,**却没开任何配置入口** ——
|
|
148
|
-
* 两条门都没有位置填它,于是我在 [ref] 说的「这条是给你们的」是句空话。单测直接构造
|
|
149
|
-
* `WebSearchBackendConfig` 传进去,**天然跳过了配置入口这一层**,于是「能力在」与「配得进」之间
|
|
150
|
-
* 断开,而两边的绿都是真的。判据:**按消费端实际能填的那个入口验,不是按自己构造的对象验。**
|
|
151
|
-
*
|
|
152
|
-
* 形状:`k=v` 用 `;` 分隔(`engines=bing,duckduckgo;language=zh-CN`)——值里本来就常含逗号,
|
|
153
|
-
* 所以分隔符取 `;` 而不是 `,`。整串解析不出任何一对 ⇒ **返回 undefined,键整个不铸**
|
|
154
|
-
* (不产出空对象:空对象会让下游以为「配了但是空的」,与本仓「缺席就要缺席得干净」同源)。
|
|
155
|
-
*/
|
|
156
|
-
export declare function parseSearxngParams(raw: unknown): Record<string, string> | undefined;
|
|
157
146
|
/**
|
|
158
147
|
* #81④ 探活门控 —— **默认 OFF**,显式开才探。
|
|
159
148
|
*
|
|
@@ -165,14 +154,46 @@ export declare function parseSearxngParams(raw: unknown): Record<string, string>
|
|
|
165
154
|
* 而猜错的方向是「凭空出网」。缺席要缺席得干净,含糊也一样。
|
|
166
155
|
*/
|
|
167
156
|
export declare function shouldProbeWebSearchOnBoot(env?: NodeJS.ProcessEnv): boolean;
|
|
168
|
-
/**
|
|
157
|
+
/** env 载体形错的机读码(拼法随 `config.<族>_invalid` 既有族,如 `config.model_provider_invalid`)。码按**规则**
|
|
158
|
+
* 命名、不按键名 —— 键名逐字在拒因里。 */
|
|
159
|
+
export declare const WEB_SEARCH_CONFIG_INVALID_CODE = "config.web_search_invalid";
|
|
160
|
+
/**
|
|
161
|
+
* `WEB_SEARCH_PROVIDER` 这一格的判官(provider 字段的 env 载体):缺席 / 词表外 ⇒ `undefined`(= 不装配,缺席语义)。
|
|
162
|
+
*
|
|
163
|
+
* 🔴 **读面只问这一只**(codex R1-F2):`config-catalog` 的 `WEB_SEARCH_PROVIDER` 行要的是「装配了哪只后端」这一格,
|
|
164
|
+
* 不是整份配置。首版它调的是 {@link webSearchConfigFromEnv} —— 那只对旁的 `WEB_SEARCH_*` 形错**抛**(拒启);嵌入式
|
|
165
|
+
* `createHttpServer` 不走 stage-07,形错的环境可以活在一只跑着的 server 里,于是读 provider 这一格就把整张目录打成
|
|
166
|
+
* 500。形错拒启归装配点(stage-07 / run-local 调整份解析),读面不替装配点再判一遍。
|
|
167
|
+
*/
|
|
168
|
+
export declare function webSearchProviderFromEnv(env?: NodeJS.ProcessEnv): WebSearchProvider | undefined;
|
|
169
|
+
/**
|
|
170
|
+
* 部署 env ⇒ 后端配置。`WEB_SEARCH_PROVIDER` 缺席 / 词表外 ⇒ `undefined`(WebSearch 不装配,**不**读其余旋钮 ——
|
|
171
|
+
* 不消费的键不判);其余 `WEB_SEARCH_*` 任一形错 ⇒ **抛**(拒启,见 {@link envValue})。
|
|
172
|
+
*/
|
|
169
173
|
export declare function webSearchConfigFromEnv(env?: NodeJS.ProcessEnv): WebSearchBackendConfig | undefined;
|
|
170
174
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
175
|
+
* 每请求 `body.settings.webSearch` 的判决 —— 受理面与装配点**同问这一只**:
|
|
176
|
+
* · `absent` —— 没带(或 `null`)⇒ 用部署后端;
|
|
177
|
+
* · `honored` —— 每个字段形对,且 `provider` 点名了一只本部署会说的后端 ⇒ 这次调用用它;
|
|
178
|
+
* · `malformed` —— 任一字段形错 ⇒ 受理面 400 `request.field_invalid`(`issue` 逐字点名字段)。
|
|
179
|
+
* **`provider` 缺席或不在词表里也是形错**(S-199 主会话改判,codex 验修 V1 [high]):一段 `settings.webSearch` 就是
|
|
180
|
+
* 调用方在点名目的地(`endpoint` / `apiKey` 都挂在它点名的那只后端上),认不出就拒,不静默换成部署后端 —— 改判前
|
|
181
|
+
* 这一形回落部署后端,`{provider:"searx", endpoint:<私有地址>}` 的查询于是发到了部署配的后端。env 腿的同一只
|
|
182
|
+
* {@link isWebSearchProvider} 守卫对**部署**的词表外答「不装配」,那是部署自己的缺席,不是换别人的目的地。
|
|
183
|
+
* 规则集因此只剩三态,没有「形对但不采纳」这一臂。
|
|
184
|
+
*
|
|
185
|
+
* 🔒 采纳与否是**调用方**的事:装配点把整条腿闸在单用户车道(每请求 `endpoint`/`apiKey` 是能力配置;多租上一个
|
|
186
|
+
* 租户把 `searxng` 指向内网地址就是 SSRF,所以多租只认部署 env 的后端)。受理面的形判**不分车道**(与
|
|
187
|
+
* `settings.hooks` 同姿势:形在受理面判,采不采纳在装配点决定)。
|
|
176
188
|
*/
|
|
177
|
-
export
|
|
189
|
+
export type RequestWebSearchVerdict = {
|
|
190
|
+
readonly kind: "absent";
|
|
191
|
+
} | {
|
|
192
|
+
readonly kind: "honored";
|
|
193
|
+
readonly config: WebSearchBackendConfig;
|
|
194
|
+
} | {
|
|
195
|
+
readonly kind: "malformed";
|
|
196
|
+
readonly issue: string;
|
|
197
|
+
};
|
|
198
|
+
export declare function judgeRequestWebSearch(raw: unknown): RequestWebSearchVerdict;
|
|
178
199
|
//# sourceMappingURL=web-search.d.ts.map
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createSearxngSearchBackend } from "@sema-agent/core";
|
|
2
|
+
import { isHierarchicalUrl, redactSecrets } from "../trace/redact.js";
|
|
2
3
|
export const WEB_SEARCH_PROVIDERS = ["brave", "tavily", "searxng"];
|
|
3
4
|
function isWebSearchProvider(word) {
|
|
4
5
|
return WEB_SEARCH_PROVIDERS.includes(word);
|
|
5
6
|
}
|
|
7
|
+
const providerWord = (raw) => raw.trim().toLowerCase();
|
|
6
8
|
export function projectWebSearchCapability(provider) {
|
|
7
9
|
return provider === undefined ? { backend: "none" } : { backend: provider };
|
|
8
10
|
}
|
|
@@ -26,6 +28,10 @@ function withTimeout(signal, ms) {
|
|
|
26
28
|
async function errorText(res) {
|
|
27
29
|
return (await res.text().catch(() => "")).slice(0, 200);
|
|
28
30
|
}
|
|
31
|
+
async function httpFailure(provider, res) {
|
|
32
|
+
const body = await errorText(res);
|
|
33
|
+
return new Error(`${provider} search failed: HTTP ${res.status}${body ? ` — ${body}` : ""}`);
|
|
34
|
+
}
|
|
29
35
|
let onBadPayload;
|
|
30
36
|
export function setWebSearchBadPayloadObserver(fn) {
|
|
31
37
|
onBadPayload = fn;
|
|
@@ -62,7 +68,7 @@ async function braveSearch(fetchImpl, cfg, query, max, signal) {
|
|
|
62
68
|
url.searchParams.set("count", String(Math.min(20, max)));
|
|
63
69
|
const res = await fetchImpl(url, { headers: { Accept: "application/json", "X-Subscription-Token": cfg.apiKey }, signal });
|
|
64
70
|
if (!res.ok)
|
|
65
|
-
throw
|
|
71
|
+
throw await httpFailure("brave", res);
|
|
66
72
|
const data = (await res.json());
|
|
67
73
|
return normalize(data.web?.results, (r) => ({ title: r.title, url: r.url, snippet: r.description }), max, "brave");
|
|
68
74
|
}
|
|
@@ -74,14 +80,14 @@ async function tavilySearch(fetchImpl, cfg, query, max, signal, opts) {
|
|
|
74
80
|
body.include_domains = opts.allowedDomains.slice(0, 300);
|
|
75
81
|
if (opts?.blockedDomains?.length)
|
|
76
82
|
body.exclude_domains = opts.blockedDomains.slice(0, 150);
|
|
77
|
-
const res = await fetchImpl(cfg.endpoint || TAVILY_BASE, {
|
|
83
|
+
const res = await fetchImpl(new URL(cfg.endpoint || TAVILY_BASE), {
|
|
78
84
|
method: "POST",
|
|
79
85
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
80
86
|
body: JSON.stringify(body),
|
|
81
87
|
signal,
|
|
82
88
|
});
|
|
83
89
|
if (!res.ok)
|
|
84
|
-
throw
|
|
90
|
+
throw await httpFailure("tavily", res);
|
|
85
91
|
const data = (await res.json());
|
|
86
92
|
return normalize(data.results, (r) => ({ title: r.title, url: r.url, snippet: r.content }), max, "tavily");
|
|
87
93
|
}
|
|
@@ -110,6 +116,11 @@ async function searxngSearch(fetchImpl, cfg, query, max, signal, timeoutMs, opts
|
|
|
110
116
|
}
|
|
111
117
|
return normalize(rows, (r) => ({ title: r.title, url: r.url, snippet: r.snippet }), max, "searxng");
|
|
112
118
|
}
|
|
119
|
+
function redactedBackendFailure(e) {
|
|
120
|
+
const text = e instanceof Error ? e.message : String(e);
|
|
121
|
+
const safe = redactSecrets(text);
|
|
122
|
+
return safe === text ? e : new Error(safe);
|
|
123
|
+
}
|
|
113
124
|
export function createWebSearchBackend(cfg) {
|
|
114
125
|
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
115
126
|
const maxResults = Math.max(1, Math.min(20, cfg.maxResults ?? DEFAULT_MAX_RESULTS));
|
|
@@ -126,70 +137,170 @@ export function createWebSearchBackend(cfg) {
|
|
|
126
137
|
case "searxng": return await searxngSearch(fetchImpl, cfg, q, maxResults, sig, timeoutMs, opts);
|
|
127
138
|
}
|
|
128
139
|
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
throw redactedBackendFailure(e);
|
|
142
|
+
}
|
|
129
143
|
finally {
|
|
130
144
|
done();
|
|
131
145
|
}
|
|
132
146
|
};
|
|
133
147
|
return { search, maxResults };
|
|
134
148
|
}
|
|
135
|
-
export function parseSearxngParams(raw) {
|
|
136
|
-
if (typeof raw === "object" && raw !== null) {
|
|
137
|
-
const entries = Object.entries(raw);
|
|
138
|
-
if (entries.length === 0 || entries.some(([, v]) => typeof v !== "string"))
|
|
139
|
-
return undefined;
|
|
140
|
-
return Object.fromEntries(entries);
|
|
141
|
-
}
|
|
142
|
-
if (typeof raw !== "string")
|
|
143
|
-
return undefined;
|
|
144
|
-
const out = {};
|
|
145
|
-
for (const pair of raw.split(";")) {
|
|
146
|
-
const i = pair.indexOf("=");
|
|
147
|
-
if (i <= 0)
|
|
148
|
-
continue;
|
|
149
|
-
const k = pair.slice(0, i).trim();
|
|
150
|
-
const v = pair.slice(i + 1).trim();
|
|
151
|
-
if (k && v)
|
|
152
|
-
out[k] = v;
|
|
153
|
-
}
|
|
154
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
155
|
-
}
|
|
156
149
|
export function shouldProbeWebSearchOnBoot(env = process.env) {
|
|
157
150
|
const raw = env.WEB_SEARCH_PROBE_ON_BOOT?.trim().toLowerCase();
|
|
158
151
|
return raw === "true" || raw === "1";
|
|
159
152
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
153
|
+
const ABSENT = { ok: true };
|
|
154
|
+
const valueOf = (j) => (j.ok ? j.value : undefined);
|
|
155
|
+
const isBlank = (s) => s.trim() === "";
|
|
156
|
+
function judgeText(raw) {
|
|
157
|
+
if (raw === undefined || raw === null)
|
|
158
|
+
return ABSENT;
|
|
159
|
+
if (typeof raw !== "string")
|
|
160
|
+
return { ok: false, issue: "must be a string" };
|
|
161
|
+
return isBlank(raw) ? ABSENT : { ok: true, value: raw };
|
|
162
|
+
}
|
|
163
|
+
const ENDPOINT_FORM = "must be an absolute URL with a scheme and a host, such as https://search.example.com (a bare host or host:port is not one)";
|
|
164
|
+
const ENDPOINT_USERINFO = "must not carry userinfo (user or user:password before the host)";
|
|
165
|
+
function judgeEndpoint(raw) {
|
|
166
|
+
if (raw === undefined || raw === null)
|
|
167
|
+
return ABSENT;
|
|
168
|
+
if (typeof raw !== "string")
|
|
169
|
+
return { ok: false, issue: ENDPOINT_FORM };
|
|
170
|
+
if (isBlank(raw))
|
|
171
|
+
return ABSENT;
|
|
172
|
+
if (!isHierarchicalUrl(raw))
|
|
173
|
+
return { ok: false, issue: ENDPOINT_FORM };
|
|
174
|
+
const url = new URL(raw);
|
|
175
|
+
return url.username !== "" || url.password !== "" ? { ok: false, issue: ENDPOINT_USERINFO } : { ok: true, value: raw };
|
|
176
|
+
}
|
|
177
|
+
const COUNT_FORM = "must be a number ≥ 1";
|
|
178
|
+
function judgeCount(raw) {
|
|
179
|
+
if (raw === undefined || raw === null)
|
|
180
|
+
return ABSENT;
|
|
181
|
+
return typeof raw === "number" && Number.isFinite(raw) && raw >= 1 ? { ok: true, value: Math.floor(raw) } : { ok: false, issue: COUNT_FORM };
|
|
182
|
+
}
|
|
183
|
+
function envNumber(raw) {
|
|
184
|
+
return raw === undefined || isBlank(raw) ? undefined : Number(raw.trim());
|
|
185
|
+
}
|
|
186
|
+
const SEARXNG_PARAM_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
187
|
+
const SEARXNG_PARAMS_FORM = 'must be an object of parameter name → non-empty string, or a "name=value;name=value" string';
|
|
188
|
+
const paramIssue = (entry, problem) => ({ ok: false, issue: `${SEARXNG_PARAMS_FORM} — entry #${entry} ${problem}` });
|
|
189
|
+
function judgeSearxngParams(raw) {
|
|
190
|
+
if (raw === undefined || raw === null)
|
|
191
|
+
return ABSENT;
|
|
192
|
+
const params = [];
|
|
193
|
+
if (typeof raw === "string") {
|
|
194
|
+
const segments = raw.split(";");
|
|
195
|
+
for (const [i, seg] of segments.entries()) {
|
|
196
|
+
if (isBlank(seg))
|
|
197
|
+
continue;
|
|
198
|
+
const eq = seg.indexOf("=");
|
|
199
|
+
if (eq < 0)
|
|
200
|
+
return paramIssue(i + 1, 'has no "="');
|
|
201
|
+
params.push({ entry: i + 1, name: seg.slice(0, eq).trim(), value: seg.slice(eq + 1).trim() });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (typeof raw === "object" && !Array.isArray(raw)) {
|
|
205
|
+
const entries = Object.entries(raw);
|
|
206
|
+
for (const [i, [k, v]] of entries.entries()) {
|
|
207
|
+
if (typeof v !== "string")
|
|
208
|
+
return paramIssue(i + 1, "has a value that is not a string");
|
|
209
|
+
params.push({ entry: i + 1, name: k, value: v.trim() });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
return { ok: false, issue: `${SEARXNG_PARAMS_FORM} — got ${Array.isArray(raw) ? "an array" : `a ${typeof raw}`}` };
|
|
214
|
+
}
|
|
215
|
+
const firstEntryOf = new Map();
|
|
216
|
+
for (const p of params) {
|
|
217
|
+
if (!SEARXNG_PARAM_NAME.test(p.name))
|
|
218
|
+
return paramIssue(p.entry, 'has a name that is not a parameter name (letters, digits, "_", "-", ".")');
|
|
219
|
+
if (p.value === "")
|
|
220
|
+
return paramIssue(p.entry, "has an empty value");
|
|
221
|
+
const first = firstEntryOf.get(p.name);
|
|
222
|
+
if (first !== undefined)
|
|
223
|
+
return paramIssue(p.entry, `repeats the name of entry #${first}`);
|
|
224
|
+
firstEntryOf.set(p.name, p.entry);
|
|
225
|
+
}
|
|
226
|
+
return params.length === 0 ? ABSENT : { ok: true, value: Object.fromEntries(params.map((p) => [p.name, p.value])) };
|
|
227
|
+
}
|
|
228
|
+
function assembleBackendConfig(provider, f) {
|
|
167
229
|
return {
|
|
168
230
|
provider,
|
|
169
|
-
...(
|
|
170
|
-
...(
|
|
171
|
-
...(searxngParams ? { searxngParams } : {}),
|
|
172
|
-
...(
|
|
173
|
-
...(
|
|
231
|
+
...(f.apiKey !== undefined ? { apiKey: f.apiKey } : {}),
|
|
232
|
+
...(f.endpoint !== undefined ? { endpoint: f.endpoint } : {}),
|
|
233
|
+
...(f.searxngParams !== undefined ? { searxngParams: f.searxngParams } : {}),
|
|
234
|
+
...(f.maxResults !== undefined ? { maxResults: f.maxResults } : {}),
|
|
235
|
+
...(f.timeoutMs !== undefined ? { timeoutMs: f.timeoutMs } : {}),
|
|
174
236
|
};
|
|
175
237
|
}
|
|
176
|
-
export
|
|
177
|
-
|
|
238
|
+
export const WEB_SEARCH_CONFIG_INVALID_CODE = "config.web_search_invalid";
|
|
239
|
+
function envValue(key, j) {
|
|
240
|
+
if (!j.ok) {
|
|
241
|
+
throw new Error(`env ${key} ${j.issue} [${WEB_SEARCH_CONFIG_INVALID_CODE}] — fix it or unset it (an empty value counts as unset). ` +
|
|
242
|
+
"WebSearch reads its WEB_SEARCH_* knobs only when WEB_SEARCH_PROVIDER names a backend, and every knob it reads must be well-formed: " +
|
|
243
|
+
"a malformed value used to be absorbed silently (garbage parameters sent to the search instance, or a quiet fall-back to a default nobody chose).");
|
|
244
|
+
}
|
|
245
|
+
return j.value;
|
|
246
|
+
}
|
|
247
|
+
export function webSearchProviderFromEnv(env = process.env) {
|
|
248
|
+
const raw = env.WEB_SEARCH_PROVIDER;
|
|
249
|
+
const word = raw === undefined ? undefined : providerWord(raw);
|
|
250
|
+
return word !== undefined && isWebSearchProvider(word) ? word : undefined;
|
|
251
|
+
}
|
|
252
|
+
export function webSearchConfigFromEnv(env = process.env) {
|
|
253
|
+
const provider = webSearchProviderFromEnv(env);
|
|
254
|
+
if (provider === undefined)
|
|
178
255
|
return undefined;
|
|
256
|
+
return assembleBackendConfig(provider, {
|
|
257
|
+
apiKey: envValue("WEB_SEARCH_API_KEY", judgeText(env.WEB_SEARCH_API_KEY)),
|
|
258
|
+
endpoint: envValue("WEB_SEARCH_ENDPOINT", judgeEndpoint(env.WEB_SEARCH_ENDPOINT)),
|
|
259
|
+
searxngParams: envValue("WEB_SEARCH_SEARXNG_PARAMS", judgeSearxngParams(env.WEB_SEARCH_SEARXNG_PARAMS)),
|
|
260
|
+
maxResults: envValue("WEB_SEARCH_MAX_RESULTS", judgeCount(envNumber(env.WEB_SEARCH_MAX_RESULTS))),
|
|
261
|
+
timeoutMs: envValue("WEB_SEARCH_TIMEOUT_MS", judgeCount(envNumber(env.WEB_SEARCH_TIMEOUT_MS))),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
const SETTINGS_WEB_SEARCH_FORM = "settings.webSearch must be an object { provider, apiKey?, endpoint?, searxngParams?, maxResults? }";
|
|
265
|
+
const providerIssue = (shape) => ({
|
|
266
|
+
ok: false,
|
|
267
|
+
issue: `must name one of ${WEB_SEARCH_PROVIDERS.join(" | ")} (${shape}) — it names where this request's searches go, so an unknown or missing provider is refused rather than replaced by this deployment's own backend`,
|
|
268
|
+
});
|
|
269
|
+
function judgeRequestProvider(raw) {
|
|
270
|
+
if (raw === undefined || raw === null)
|
|
271
|
+
return providerIssue("it is missing");
|
|
272
|
+
if (typeof raw !== "string")
|
|
273
|
+
return providerIssue("it is not a string");
|
|
274
|
+
const word = providerWord(raw);
|
|
275
|
+
return isWebSearchProvider(word) ? { ok: true, value: word } : providerIssue("it is not one of them");
|
|
276
|
+
}
|
|
277
|
+
export function judgeRequestWebSearch(raw) {
|
|
278
|
+
if (raw === undefined || raw === null)
|
|
279
|
+
return { kind: "absent" };
|
|
280
|
+
if (typeof raw !== "object" || Array.isArray(raw))
|
|
281
|
+
return { kind: "malformed", issue: SETTINGS_WEB_SEARCH_FORM };
|
|
179
282
|
const s = raw;
|
|
180
|
-
const provider =
|
|
181
|
-
if (!
|
|
182
|
-
return
|
|
183
|
-
const
|
|
283
|
+
const provider = judgeRequestProvider(s.provider);
|
|
284
|
+
if (!provider.ok)
|
|
285
|
+
return { kind: "malformed", issue: `settings.webSearch.provider ${provider.issue}` };
|
|
286
|
+
const fields = {
|
|
287
|
+
apiKey: judgeText(s.apiKey),
|
|
288
|
+
endpoint: judgeEndpoint(s.endpoint),
|
|
289
|
+
searxngParams: judgeSearxngParams(s.searxngParams),
|
|
290
|
+
maxResults: judgeCount(s.maxResults),
|
|
291
|
+
};
|
|
292
|
+
for (const [field, j] of Object.entries(fields)) {
|
|
293
|
+
if (!j.ok)
|
|
294
|
+
return { kind: "malformed", issue: `settings.webSearch.${field} ${j.issue}` };
|
|
295
|
+
}
|
|
184
296
|
return {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
})
|
|
192
|
-
...(max ? { maxResults: max } : {}),
|
|
297
|
+
kind: "honored",
|
|
298
|
+
config: assembleBackendConfig(provider.value, {
|
|
299
|
+
apiKey: valueOf(fields.apiKey),
|
|
300
|
+
endpoint: valueOf(fields.endpoint),
|
|
301
|
+
searxngParams: valueOf(fields.searxngParams),
|
|
302
|
+
maxResults: valueOf(fields.maxResults),
|
|
303
|
+
}),
|
|
193
304
|
};
|
|
194
305
|
}
|
|
195
306
|
//# sourceMappingURL=web-search.js.map
|
package/dist/project-memory.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type PlatformPath } from "node:path";
|
|
2
2
|
import type { ProjectMemoryLoad } from "@sema-agent/core";
|
|
3
|
+
import { type InstructionSourceSeat } from "./instruction-source-notice.js";
|
|
3
4
|
interface ProjectMemoryCtx {
|
|
4
5
|
cwd: string;
|
|
5
6
|
handsEnabled: boolean;
|
|
@@ -46,6 +47,13 @@ export declare function instructionChainCoordinates(absPath: string, p?: Platfor
|
|
|
46
47
|
* (mid-run file edits surface via the probe as tail attachments, never a silently re-rendered prefix).
|
|
47
48
|
* `phase: "post-compact"` forces a re-read (core re-renders the prefix there anyway). No entry for
|
|
48
49
|
* sessionless one-shot runs. (post-clear never arrives from core — [ref] — so it is not a cache key here.)
|
|
50
|
+
*
|
|
51
|
+
* S-690 — source-change notice: with `instructionSource` wired, every prepare of a non-subagent session that WALKED the
|
|
52
|
+
* instruction chain hands the chain member it selected (`null` = none) to {@link announceInstructionSource}, which
|
|
53
|
+
* compares it with the session's persisted baseline. A prepare stopped by a pre-walk gate (hands-less, a cwd that is not
|
|
54
|
+
* an absolute local directory) is "not applicable", not "none" — it never reaches the mint (O6, see `LoadOutcome`). The member is the one the snapshot carries — a cache hit answers the cached pick, so
|
|
55
|
+
* the notice follows what the model actually reads, not the disk (a same-process edit changes nothing until the
|
|
56
|
+
* snapshot is re-read: restart, post-compact, or a new session).
|
|
49
57
|
*/
|
|
50
58
|
export declare function makeLoadProjectMemory(opts: {
|
|
51
59
|
logger?: {
|
|
@@ -54,6 +62,8 @@ export declare function makeLoadProjectMemory(opts: {
|
|
|
54
62
|
maxBytes?: number;
|
|
55
63
|
/** TEST-ONLY: shrink the snapshot-cache bound so the settle-time trim is observable without 256 sessions. */
|
|
56
64
|
cacheMax?: number;
|
|
65
|
+
/** S-690: the baseline store + notice exit. Absent ⇒ no source-change notice (the session store has no baseline face). */
|
|
66
|
+
instructionSource?: InstructionSourceSeat;
|
|
57
67
|
}): (ctx: ProjectMemoryCtx) => Promise<string | null | ProjectMemoryLoad>;
|
|
58
68
|
/**
|
|
59
69
|
* ② The `RunnerDeps.probeInstructionSources` seam: re-fingerprint the declared instruction files mid-run so
|
package/dist/project-memory.js
CHANGED
|
@@ -2,6 +2,7 @@ import { promises as fs, constants as fsConstants } from "node:fs";
|
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { createHmac, randomBytes } from "node:crypto";
|
|
4
4
|
import hostPath, { isAbsolute, join } from "node:path";
|
|
5
|
+
import { announceInstructionSource } from "./instruction-source-notice.js";
|
|
5
6
|
import { recordSecretEnvScrub } from "./observability/secret-env-scrub.js";
|
|
6
7
|
import { scrubServerSecrets } from "./server-secret-env.js";
|
|
7
8
|
import { redactSecrets } from "./trace/redact.js";
|
|
@@ -41,6 +42,7 @@ function instructionHash(raw) {
|
|
|
41
42
|
return `hmac-sha256:${createHmac("sha256", INSTRUCTION_HASH_KEY).update(raw).digest("hex")}`;
|
|
42
43
|
}
|
|
43
44
|
const ABSENT_HASH = null;
|
|
45
|
+
const NOT_APPLICABLE = { walked: false, answer: null };
|
|
44
46
|
export function instructionChainCoordinates(absPath, p = hostPath) {
|
|
45
47
|
const candidates = INSTRUCTION_FILE_CHAIN.filter((rel) => absPath.endsWith(p.sep + rel.split("/").join(p.sep))).sort((a, b) => b.length - a.length);
|
|
46
48
|
for (const rel of candidates) {
|
|
@@ -72,7 +74,7 @@ function git(root, args, maxBytes) {
|
|
|
72
74
|
const safeArgs = ["-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", ...args];
|
|
73
75
|
const scrubFindings = [];
|
|
74
76
|
const scrubbedEnv = scrubServerSecrets(process.env, scrubFindings);
|
|
75
|
-
recordSecretEnvScrub("server.project-memory.git-subprocess", scrubFindings);
|
|
77
|
+
recordSecretEnvScrub("server.project-memory.git-subprocess", scrubFindings, []);
|
|
76
78
|
return new Promise((res) => {
|
|
77
79
|
execFile("git", safeArgs, { cwd: root, timeout: 5000, maxBuffer: maxBytes, env: { ...scrubbedEnv, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_SYSTEM: "/dev/null" } }, (err, stdout, stderr) => {
|
|
78
80
|
if (!err)
|
|
@@ -90,17 +92,24 @@ export function makeLoadProjectMemory(opts) {
|
|
|
90
92
|
const gitCap = 8 * 1024;
|
|
91
93
|
const cache = new Map();
|
|
92
94
|
const CACHE_MAX = opts.cacheMax ?? 256;
|
|
95
|
+
const seat = opts.instructionSource;
|
|
93
96
|
return async (ctx) => {
|
|
97
|
+
const outcome = await loadOutcome(ctx);
|
|
98
|
+
if (seat && ctx.sessionId && !ctx.isSubagent && outcome.walked)
|
|
99
|
+
await announceInstructionSource(seat, ctx.sessionId, outcome.instructionFile);
|
|
100
|
+
return outcome.answer;
|
|
101
|
+
};
|
|
102
|
+
async function loadOutcome(ctx) {
|
|
94
103
|
if (!ctx.handsEnabled)
|
|
95
|
-
return
|
|
104
|
+
return NOT_APPLICABLE;
|
|
96
105
|
if (!isAbsolute(ctx.cwd))
|
|
97
|
-
return
|
|
106
|
+
return NOT_APPLICABLE;
|
|
98
107
|
let root;
|
|
99
108
|
try {
|
|
100
109
|
root = await fs.realpath(ctx.cwd);
|
|
101
110
|
}
|
|
102
111
|
catch {
|
|
103
|
-
return
|
|
112
|
+
return NOT_APPLICABLE;
|
|
104
113
|
}
|
|
105
114
|
const cacheKey = ctx.sessionId && !ctx.isSubagent ? `${ctx.sessionId}\u0000${root}` : undefined;
|
|
106
115
|
if (cacheKey) {
|
|
@@ -144,11 +153,11 @@ export function makeLoadProjectMemory(opts) {
|
|
|
144
153
|
});
|
|
145
154
|
}
|
|
146
155
|
return load;
|
|
147
|
-
}
|
|
156
|
+
}
|
|
148
157
|
async function doLoad(ctx, root) {
|
|
149
158
|
const st = await fs.stat(root).catch(() => null);
|
|
150
159
|
if (!st?.isDirectory())
|
|
151
|
-
return
|
|
160
|
+
return NOT_APPLICABLE;
|
|
152
161
|
const claudeTrunc = { v: false };
|
|
153
162
|
let claudeRaw = null;
|
|
154
163
|
let hitFile;
|
|
@@ -202,28 +211,33 @@ export function makeLoadProjectMemory(opts) {
|
|
|
202
211
|
parts.push(`## Working tree\n${status.out.split("\n").slice(0, 40).join("\n")}`);
|
|
203
212
|
gitNarrative = parts.join("\n\n");
|
|
204
213
|
}
|
|
214
|
+
const instructionFile = hitFile ?? null;
|
|
205
215
|
const sections = [];
|
|
206
216
|
if (claude && hitFile)
|
|
207
217
|
sections.push(`# ${hitFile}\n${claude}`);
|
|
208
218
|
if (gitNarrative)
|
|
209
219
|
sections.push(gitNarrative);
|
|
210
220
|
if (sections.length === 0)
|
|
211
|
-
return null;
|
|
221
|
+
return { walked: true, answer: null, instructionFile };
|
|
212
222
|
const redacted = redactSecrets(sections.join("\n\n"));
|
|
213
223
|
const capped = capUtf8(redacted, totalCap);
|
|
214
224
|
opts.logger?.debug?.("project_memory_loaded", { cwd: root, bytes: Buffer.byteLength(capped, "utf8"), isSubagent: ctx.isSubagent, gitOk, ...(gitDropReason ? { gitDropReason } : {}), claudeTruncated, ...(hitFile ? { instructionFile: hitFile } : {}), ...(ctx.phase ? { phase: ctx.phase } : {}), cacheSize: cache.size });
|
|
215
225
|
if (!capped)
|
|
216
|
-
return null;
|
|
226
|
+
return { walked: true, answer: null, instructionFile };
|
|
217
227
|
if (hitFile && hitHash) {
|
|
218
228
|
return {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
walked: true,
|
|
230
|
+
answer: {
|
|
231
|
+
content: capped,
|
|
232
|
+
instructionSources: [
|
|
233
|
+
...INSTRUCTION_FILE_CHAIN.slice(0, hitIndex).map((rel) => ({ path: join(root, rel), contentHash: ABSENT_HASH })),
|
|
234
|
+
{ path: join(root, hitFile), contentHash: hitHash },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
instructionFile,
|
|
224
238
|
};
|
|
225
239
|
}
|
|
226
|
-
return capped;
|
|
240
|
+
return { walked: true, answer: capped, instructionFile };
|
|
227
241
|
}
|
|
228
242
|
}
|
|
229
243
|
export function makeProbeInstructionSources() {
|
package/dist/run-local.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { type
|
|
2
|
+
import { type AskRequest, type Brain, type TaskSpec } from "@sema-agent/core";
|
|
3
3
|
import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView } from "./deployment-governance.js";
|
|
4
4
|
import { type ServiceConfig } from "./config.js";
|
|
5
5
|
import { type Logger } from "./observability/logger.js";
|
|
@@ -77,9 +77,12 @@ export interface LocalGovernanceConfigView extends DeploymentGovernanceConfigVie
|
|
|
77
77
|
* 叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE / 守卫集)。本函数一条 policy 都不
|
|
78
78
|
* 自己合成。
|
|
79
79
|
*
|
|
80
|
-
* · **审批基线 =
|
|
81
|
-
*
|
|
82
|
-
* (
|
|
80
|
+
* · **审批基线 = live 席**({@link RUN_LOCAL_APPROVAL_SEAT},S-694):一次性 CLI 没有 durable 审批设施(没有
|
|
81
|
+
* `/decide` 腿可赎回),但**有**一只活 onAsk 席({@link createLocalApprover})。三张审批名单因此与 HTTP 腿
|
|
82
|
+
* **同一只**策略执法(`createDurableAskPolicy`:deny ⇒ 拒;require / never-auto ⇒ ask ⇒ 那只席当场服),
|
|
83
|
+
* 不叠 durable 问答门。修前本腿不传席 ⇒ 落 allow-all 臂 ⇒ 名单在 CLI 上无声空转 —— [ref] 件一把「没有
|
|
84
|
+
* durable 赎回腿」等同成了「ask 无处可服」,可件三同窗装上的正是那只服 ask 的席。运维照旧用
|
|
85
|
+
* `AUTONOMY` / `commandPolicy` / 守卫集在其上收紧。
|
|
83
86
|
* · **裁决 env 恒 host 形,锚 `workspaceDir`**:run-local 的手就跑在这台机器的这个目录上
|
|
84
87
|
* (`--workspace` / config.d workdir / cwd 三选一,调用方已解析好),所以 `cwd` 恒在场 —— 相对形写目标
|
|
85
88
|
* 由 core 按 `rootPath` 解析进真身裁决,构造口的词法补层(沙箱腿那条)在这里自动不铺。
|
|
@@ -122,27 +125,26 @@ export interface LocalApproverIo {
|
|
|
122
125
|
* ⛔ **不在 toolPolicy 层做 ask→deny 映射**:core 自己就有 headless auto-deny 的语义与文案,在策略层
|
|
123
126
|
* 复刻一份 = 同源谎(两处判据日后各改各的)。裁决归 core,座位归这里。
|
|
124
127
|
*
|
|
128
|
+
* 🔴 S-694 起本席也服**三张审批名单**产的 ask(审批基线取 live 席,见 {@link RUN_LOCAL_APPROVAL_SEAT}):
|
|
129
|
+
* `APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` 命中 ⇒ 策略 `ask` ⇒ 落到这里。`AskRequest` 不带是哪条声明产的
|
|
130
|
+
* ask(core 把 `matchedAskRule` 刻意留在闸内、不上请求),所以无 TTY 拒句**列出**可能产门的全部旋钮,
|
|
131
|
+
* `APPROVAL_DENY` 也点名(它当场拒、不经本席 —— 运维排查「为什么这只工具不跑」时要在同一句里看到它)。
|
|
132
|
+
*
|
|
125
133
|
* 🔴 非 TTY 臂答 `false`(真 deny),**绝不答 `"unavailable"`**:后者被 core 读成「席位够不到」并**改道
|
|
126
134
|
* `suspendAsk`**(dist `core/hooks.js` 的 `approverUnavailable` 重投)。今天本腿无店 ⇒ 那条改道无处
|
|
127
135
|
* 可去(fail-closed 拒),但这一条仍是本席的契约:答案是「不批」,不是「找不到人」。
|
|
128
136
|
*/
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*
|
|
133
|
-
* 🔴 决断三臂(codex 验收轮 high,红先修):`readline/promises` 的 `question` 在输入流 EOF/close 时
|
|
134
|
-
* **不决**(promise 悬挂)——Ctrl-D、关闭的 stdin、任务取消都会把 runLocal 永久挂住,文件后端的锁
|
|
135
|
-
* 跟着被拖住,下一次同 root 调用直接卡死。三臂全部 fail-closed 决 false:
|
|
136
|
-
* · `signal` abort(core `OnAsk` 递进来的取消,含预先 aborted);
|
|
137
|
-
* · readline `close`(输入流 end/close 时 readline 自关);
|
|
138
|
-
* · `question` 自身 reject(abort throw / 流错误)。 */
|
|
137
|
+
/** 本席的形:core `OnAsk` 的函数臂(`"allow"` / `"deny"` 两个字面臂本席从不用),答案恒是布尔 —— 结构上可赋给 `OnAsk`。 */
|
|
138
|
+
export type LocalApprover = (req: AskRequest, signal?: AbortSignal) => Promise<boolean>;
|
|
139
|
+
export declare function createLocalApprover(io: LocalApproverIo): LocalApprover;
|
|
139
140
|
export declare function promptYesNoOnTty(prompt: string, signal?: AbortSignal, streams?: {
|
|
140
141
|
input: NodeJS.ReadableStream;
|
|
141
142
|
output: NodeJS.WritableStream;
|
|
142
143
|
}): Promise<boolean>;
|
|
143
144
|
/**
|
|
144
145
|
* Boot the engine in LOCAL mode, run ONE task to completion, print the result. Returns the process exit
|
|
145
|
-
* code (0 = completed, 1 = the task did not complete
|
|
146
|
+
* code (0 = completed its work, 1 = the task did not complete — including a `completed` run that a person stopped,
|
|
147
|
+
* see {@link personStopOf} — / a fatal error, 2 = bad usage / unmet env). Pure of
|
|
146
148
|
* `process.exit` so a test can assert the code; `main()` applies it.
|
|
147
149
|
*/
|
|
148
150
|
export declare function runLocal(argv: string[], deps?: RunLocalDeps): Promise<number>;
|