@sema-agent/server 7.43.0 → 7.44.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/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
|
@@ -1,50 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Durable backing store for offloaded large tool results (core 1.47 offload / 1.49 durable contract) —
|
|
3
|
-
* SINGLE-FILE DUAL-DIALECT (design/158 A12 定型半场). ONE implementation, TWO dialects; the historical
|
|
4
|
-
* `TiDBToolResultStore` / `PgToolResultStore` class names survive as thin ctor subclasses so every consumer
|
|
5
|
-
* (store-backend.ts, the fake-pool unit suites, the real-DB integration suites) is untouched.
|
|
6
|
-
*
|
|
7
|
-
* Mirrors core's reference `TiDbToolResultStore` adapter. Without this, the Runner defaults to a
|
|
8
|
-
* task-scoped in-memory store: an async run that wakes on ANOTHER replica can't `read_tool_result` an
|
|
9
|
-
* offloaded result (the ref misses → the model is told it's unavailable; the preview still stands, so it
|
|
10
|
-
* degrades, never crashes). A durable store survives wake/resume across the stateless fleet.
|
|
11
|
-
*
|
|
12
|
-
* core namespaces the ref as `tr_<sessionId>~<toolCallId>~<contentCoordinate>` (1.49, re-minted injectively in
|
|
13
|
-
* 5.26.0 #119 — `~`-separated, four segments max, content-addressed) → globally unique → usable directly as
|
|
14
|
-
* the PRIMARY KEY (no composite key needed). `put` is write-once (idempotent on replay/retry: a retry is
|
|
15
|
-
* a NEW toolCallId → new ref, so a given ref never changes content). Retention is anchored to a run's
|
|
16
|
-
* RECOVERABLE window via TTL (`reapOlderThan`): a resumable run may re-fetch an old ref after wake, but a
|
|
17
|
-
* completed run only needs the preview for audit/replay — so a TTL comfortably exceeding the run lifetime
|
|
18
|
-
* bounds storage without losing recoverable full-text.
|
|
19
|
-
*
|
|
20
|
-
* ── Dialect deltas, kept EXPLICIT ────────────────────────────────────────────────────────────────────
|
|
21
|
-
* - `?` placeholders vs `$n`
|
|
22
|
-
* - write-once insert = `INSERT IGNORE` vs `INSERT ... ON CONFLICT (ref) DO NOTHING`
|
|
23
|
-
* - affectedRows vs rowCount (via SqlDriver)
|
|
24
|
-
* - `SUBSTRING(content, ?, ?)` (1-indexed, comma form) vs the ANSI `SUBSTRING(content FROM $n::int FOR
|
|
25
|
-
* $m::int)` positional form WITH explicit `::int` casts — node-pg binds bare params as text, and
|
|
26
|
-
* `SUBSTRING(s FROM <text>)` is the POSIX-REGEX overload (it would treat $1 as a pattern); the cast
|
|
27
|
-
* forces the positional integer overload. `CHAR_LENGTH` is ANSI and shared.
|
|
28
|
-
* - content column: TiDB LONGTEXT vs PG TEXT (PG has no LONGTEXT; TEXT is unbounded), DATETIME(3) vs
|
|
29
|
-
* TIMESTAMPTZ(3) — schema only (TiDB DDL in tidb-pool.ts; PG DDL self-contained below, exported for the
|
|
30
|
-
* integration test + pg-pool.ts's central apply).
|
|
31
|
-
* - `put`'s UTF-16 sanitize is a GENUINE algorithm divergence, not just SQL text — see the dialect branch
|
|
32
|
-
* inline for why.
|
|
33
|
-
* - (历史条目,已随码消失)ESCAPE-clause literal:`deleteBySession` 曾按 ref 前缀 LIKE 选行,两方言因此
|
|
34
|
-
* 各自显式声明转义符。重扫二轮起该腿改为**只按 `owner_session_id` 出处选行**(core 契约:ref 是
|
|
35
|
-
* opaque handle,禁解析),本文件已无 LIKE,这条方言差随之退役 —— 留档以防有人「照旧例」把前缀
|
|
36
|
-
* 选行请回来。
|
|
37
|
-
*/
|
|
38
1
|
import { MAX_MINTED_TOOL_RESULT_REF_CHARS, assertSafeToolResultRef, assertToolResultProvenanceMatch, normalizeToolResultProvenance } from "@sema-agent/core";
|
|
39
2
|
import { pgSanitizeText } from "./pg-safe-json.js";
|
|
40
3
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
41
4
|
import { MANAGED_RETENTION } from "./retention-store-sql.js";
|
|
42
|
-
/** PG schema for the tool_result table — the PG translation of tidb-pool.ts's tool_result DDL
|
|
43
|
-
* (LONGTEXT→TEXT, DATETIME(3)→TIMESTAMPTZ(3), inline KEY→separate CREATE INDEX). Disjoint from other stores'
|
|
44
|
-
* tables, so a self-contained ensureSchema is safe (central pg-pool aggregation is done separately). */
|
|
45
5
|
export const PG_TOOL_RESULT_SCHEMA = [
|
|
46
|
-
// #119(core 5.26.0):宽度与出处两列与 TiDB 双生逐字对齐 —— 理由写在 tidb-pool.ts 的同一张 DDL 上
|
|
47
|
-
// (`MAX_MINTED_TOOL_RESULT_REF_CHARS` = 518;截断主键 = 两枚 ref 折成一行 = 身份故障)。
|
|
48
6
|
`CREATE TABLE IF NOT EXISTS tool_result (
|
|
49
7
|
ref VARCHAR(518) COLLATE "C" NOT NULL,
|
|
50
8
|
content TEXT COLLATE "C" NOT NULL,
|
|
@@ -55,33 +13,15 @@ export const PG_TOOL_RESULT_SCHEMA = [
|
|
|
55
13
|
)`,
|
|
56
14
|
`CREATE INDEX IF NOT EXISTS idx_tool_result_created ON tool_result (created_at)`,
|
|
57
15
|
];
|
|
58
|
-
/** Idempotent schema apply for the tool_result table (for the integration test to call). */
|
|
59
16
|
export async function ensureSchema(pool) {
|
|
60
17
|
for (const stmt of PG_TOOL_RESULT_SCHEMA)
|
|
61
18
|
await pool.query(stmt);
|
|
62
19
|
}
|
|
63
|
-
/**
|
|
64
|
-
* #119 升级前置断言 —— **拒启**,不是 warn(codex 复审 [high],已核真)。
|
|
65
|
-
*
|
|
66
|
-
* 病:本仓不发 `ALTER TABLE` 迁移(标准裁定:改列就改 CREATE + 删库重建)。于是一台**没删表**就升上来的
|
|
67
|
-
* 部署,`CREATE TABLE IF NOT EXISTS` 对它是空操作,旧表既没有出处两列也只有 `VARCHAR(190)` 的 ref 列。
|
|
68
|
-
* 那样跑起来的后果不是「少个功能」:每一次 offload 写都会撞 unknown column 报错,而 core 的 offload
|
|
69
|
-
* 失败臂会把错误吞成一条内联占位("[offload LOST at write time…]")—— 服务照跑,工具产物全丢,日志里只有
|
|
70
|
-
* 一句看不出根因的话。这正是「响亮或 fail-closed,禁静默降级」那条要挡的形态。
|
|
71
|
-
*
|
|
72
|
-
* 判据用**能力探测**而不是版本号/information_schema:发一条恒空的 `WHERE 1=0` 读,列不在就报错。
|
|
73
|
-
* 探不通即拒启,错误文案直接给出两条方言的动作(删表重建),不让运维去猜。
|
|
74
|
-
*/
|
|
75
20
|
export async function assertToolResultProvenanceSchema(query, dialect) {
|
|
76
|
-
// ② 键宽(codex 复审 round2 [high],已核真):只探两列会放过「有出处列、但 ref 仍是 VARCHAR(190)」的
|
|
77
|
-
// 半迁移表 —— 而这道断言的文案与 CHANGELOG 都在宣称它护着加宽后的键。截断的主键把两枚不同的 ref 折成
|
|
78
|
-
// 同一行 = 身份故障,正是本次升级要消灭的东西,所以宽度必须**真查**,不能靠「列在 ⇒ 表是新的」推断。
|
|
79
21
|
const widthRow = await query(dialect === "tidb"
|
|
80
22
|
? "SELECT CHARACTER_MAXIMUM_LENGTH AS len FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'tool_result' AND column_name = 'ref'"
|
|
81
23
|
: "SELECT character_maximum_length AS len FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'tool_result' AND column_name = 'ref'").catch(() => ({ rows: [] }));
|
|
82
24
|
const width = Number(widthRow.rows[0]?.len ?? NaN);
|
|
83
|
-
// 查不到宽度(权限受限的 information_schema / 意外的列类型)⇒ **不**据此拒启:那是「没看见」,不是
|
|
84
|
-
// 「看见了不合格」。看见了才判,判就判死。
|
|
85
25
|
if (Number.isFinite(width) && width < MAX_MINTED_TOOL_RESULT_REF_CHARS) {
|
|
86
26
|
throw new Error(`tool_result.ref is VARCHAR(${width}) but core 5.26.0 mints refs up to ${MAX_MINTED_TOOL_RESULT_REF_CHARS} characters ` +
|
|
87
27
|
`(#119 made the ref injective: four \`~\`-separated segments). A truncating primary key aliases two distinct refs onto ` +
|
|
@@ -103,30 +43,16 @@ export async function assertToolResultProvenanceSchema(query, dialect) {
|
|
|
103
43
|
`Underlying probe error: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
44
|
}
|
|
105
45
|
}
|
|
106
|
-
/** Dual-dialect durable ToolResultStore. See the file header for the dialect-delta ledger. */
|
|
107
46
|
export class SqlToolResultStore {
|
|
108
47
|
db;
|
|
109
|
-
/** #270 车1:托管留存声明。读法与「为什么 SQL 店答 managed 而三方法的实现体在 `retention-store-sql.ts`」
|
|
110
|
-
* 逐字见 {@link MANAGED_RETENTION}(本店的 `tool_result` 行由那只聚合店按期清:随会话树删 + 孤儿腿)。 */
|
|
111
48
|
retention = MANAGED_RETENTION;
|
|
112
49
|
constructor(db) {
|
|
113
50
|
this.db = db;
|
|
114
51
|
}
|
|
115
|
-
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
116
52
|
q(tidb, pg) {
|
|
117
53
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
118
54
|
}
|
|
119
|
-
/** D1(试剂盒揪出,RB-266 语义):ref 是主键,而 `deleteBySession` 只认**记录下来的出处**与单射的
|
|
120
|
-
* `tr_<sid>~` 前缀(重扫二轮起;旧注写的 `tr_<sid>_%` 前缀清理已作废,理由见该方法顶注)——
|
|
121
|
-
* 不合规 ref 的行既进不来、也就谈不上被 session 删除清掉,只能等 TTL。
|
|
122
|
-
*
|
|
123
|
-
* 🔴 判定**单源**:直调 core 的 `assertSafeToolResultRef`(core 2.8.0 起入公共面)。
|
|
124
|
-
* 在此之前这里是那 6 个判别条件的**本地镜像** —— 我在 [2159] 主动交出过这条裂缝:镜像是第二真源,
|
|
125
|
-
* core 哪天收紧一格(加 Windows 保留名、长度上限……)我方不会跟着动,而**试剂盒会继续绿**
|
|
126
|
-
* (它测的是「拒不拒」,不是「按同一张表拒」)。core 按请求追加了导出,镜像随之删除,裂缝闭合。
|
|
127
|
-
* 抛错文案与 core 逐字相同,所以 wire 与既有钉都不变。 */
|
|
128
55
|
isUnsafeRef(ref) {
|
|
129
|
-
// core 只给 assert 形(它就是判定本体);读面要的是谓词,由同一个 assert 派生 —— 仍是单源。
|
|
130
56
|
try {
|
|
131
57
|
assertSafeToolResultRef(ref);
|
|
132
58
|
return false;
|
|
@@ -138,49 +64,17 @@ export class SqlToolResultStore {
|
|
|
138
64
|
assertSafeRef(ref) {
|
|
139
65
|
assertSafeToolResultRef(ref);
|
|
140
66
|
}
|
|
141
|
-
/**
|
|
142
|
-
* #119(core 5.26.0)—— 出处的**写面**。语义一律取 core 单源(`assertToolResultProvenanceMatch`),
|
|
143
|
-
* 本方法只负责把它落到 SQL 上:
|
|
144
|
-
* · **写一次选举同时定属主** —— 内容与属主是同一条 INSERT,不存在「内容写进去了属主还没跟上」的窗口
|
|
145
|
-
* (core 头注允许 file 双对象后端出现这个窗口,单行后端没有,别自造);
|
|
146
|
-
* · 抢输的那一方(IGNORE / DO NOTHING ⇒ affected=0)回读已存属主再判:同属主(或本次无出处)=
|
|
147
|
-
* 幂等空转;异属主 = `ToolResultRefConflictError` typed 拒。**绝不静默 keep-first**:ref 是主键,
|
|
148
|
-
* 静默空转会让第二位写者拿着自己的 ref 读回第一位的字节;
|
|
149
|
-
* · 无出处的行永久 unowned —— 后续带出处的 put 不回填(没有证据的收养),`ownerOf` 继续答 undefined。
|
|
150
|
-
*/
|
|
151
67
|
async put(ref, content, provenance) {
|
|
152
68
|
this.assertSafeRef(ref);
|
|
153
|
-
// Sanitize invalid UTF-16 (lone/half surrogates — a JS string CAN hold them, e.g. a binary-ish tool
|
|
154
|
-
// output) to U+FFFD so the column accepts the FULL value via a Buffer utf8 round-trip (valid content is
|
|
155
|
-
// unchanged). Shared by both dialects.
|
|
156
69
|
let safe = Buffer.from(content, "utf8").toString("utf8");
|
|
157
|
-
// 🔴 Genuine algorithm divergence (not just SQL text): TiDB's `INSERT IGNORE` downgrades an invalid-byte
|
|
158
|
-
// rejection to a warning + SILENT TRUNCATION (the utf8 round-trip above is enough — TiDB never sees a NUL
|
|
159
|
-
// complaint). PG's `ON CONFLICT DO NOTHING` does NOT paper over an encoding error — it errors — and node-pg's
|
|
160
|
-
// TEXT columns additionally reject a bare NUL (u0000) that a lone-surrogate round-trip alone does NOT strip
|
|
161
|
-
// (codex H4). `pgSanitizeText` double-scans (NUL + lone surrogate → U+FFFD) so PG never sees either. Never
|
|
162
|
-
// "unify" onto one engine's sanitize: TiDB doesn't need the NUL scan, and skipping it on PG reintroduces 22P05.
|
|
163
70
|
if (this.db.dialect === "pg")
|
|
164
71
|
safe = pgSanitizeText(safe);
|
|
165
|
-
// write-once / keep-first: a replay re-puts the SAME ref+content → IGNORE / DO NOTHING keeps the original row.
|
|
166
72
|
const owner = provenance !== undefined ? normalizeToolResultProvenance(provenance) : undefined;
|
|
167
73
|
const insert = async () => (await this.db.query(this.q("INSERT IGNORE INTO tool_result (ref, content, owner_session_id, owner_task_id, created_at) VALUES (?,?,?,?,?)", "INSERT INTO tool_result (ref, content, owner_session_id, owner_task_id, created_at) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (ref) DO NOTHING"), [ref, safe, owner?.sessionId ?? null, owner?.taskId ?? null, new Date()])).affected;
|
|
168
|
-
// 🔴 codex 复审(medium,已核真)——**抢输之后那一行可能已经不在了**。选举(INSERT)与观察(SELECT)
|
|
169
|
-
// 是两条语句,中间可以插进 TTL reap 或 §0.5 会话清除。那时 `readOwner` 回 `absent`,而 core 的比较器
|
|
170
|
-
// 对「缺一侧」是**不下判决**的(设计如此:无出处的行不许被收养)—— 于是 put 会**成功返回**,而库里
|
|
171
|
-
// 既没有内容也没有属主,调用方拿着一枚当场读不出来的 ref。
|
|
172
|
-
//
|
|
173
|
-
// 🔴 处置是**抛**,不是重投(codex 复审两轮的合取解;第一版写的重投被第二轮当场驳回,理由成立):
|
|
174
|
-
// 重投会让**删除输**。§0.5 会话清除 DELETE 掉这一行之后,一次重投就把它原地复活 —— 而 core 的部分
|
|
175
|
-
// offload 写点是不 await 的,能活过 active-run 围栏,于是被删会话的产物重新落库、留到 TTL 才消失,
|
|
176
|
-
// 直接违背 E21 的删除权保证。「少存一份可恢复窗口内的缓存」与「删了又回来」不是同一个量级的代价。
|
|
177
|
-
// 抛出去会走到 core 的 offload 失败臂 —— 它把这次卸载记成 "[offload LOST at write time: …]" 并保住
|
|
178
|
-
// 内联预览,模型看到的是实话。
|
|
179
74
|
if ((await insert()) > 0)
|
|
180
|
-
return;
|
|
75
|
+
return;
|
|
181
76
|
const stored = await this.readOwner(ref);
|
|
182
77
|
if (stored.present) {
|
|
183
|
-
// 抢输(或纯重放):属主判定归 core 的单源比较器 —— 「同属主」在两个 store 里不许有两种含义。
|
|
184
78
|
assertToolResultProvenanceMatch(ref, stored.owner, owner);
|
|
185
79
|
return;
|
|
186
80
|
}
|
|
@@ -188,121 +82,52 @@ export class SqlToolResultStore {
|
|
|
188
82
|
`(a concurrent TTL reap or session purge deleted it). Nothing was stored — reporting the loss rather than ` +
|
|
189
83
|
`re-inserting (which would resurrect a purged session's content) or returning a ref that reads back empty.`);
|
|
190
84
|
}
|
|
191
|
-
/** 一次回读同时回答两件事:**行在不在**(reap/purge 窗口的判别位)与**属主是谁**。两件事必须来自同一
|
|
192
|
-
* 条 SELECT —— 分两次问会重新引入它要消灭的那个窗口。 */
|
|
193
85
|
async readOwner(ref) {
|
|
194
86
|
const { rows } = await this.db.query(this.q("SELECT owner_session_id, owner_task_id FROM tool_result WHERE ref = ?", "SELECT owner_session_id, owner_task_id FROM tool_result WHERE ref = $1"), [ref]);
|
|
195
87
|
const row = rows[0];
|
|
196
88
|
if (!row)
|
|
197
89
|
return { present: false, owner: undefined };
|
|
198
|
-
// 🔴 缺席的判别位**只有 SQL NULL**(codex 复审 round2 [medium],已核真):曾经写成
|
|
199
|
-
// `sessionId.length === 0` 也算无属主 —— 那会把一个 `sessionId: ""` 的合法(core 的
|
|
200
|
-
// `normalizeToolResultProvenance` 不拒空串)出处在往返中悄悄折成 unowned,与内存后端的逐字比较分家;
|
|
201
|
-
// `taskId: ""` 折成缺席更糟 —— 授权面会从「task 级」放宽成「整条 session」。字符串一律原样还原。
|
|
202
90
|
const sessionId = row.owner_session_id;
|
|
203
91
|
if (typeof sessionId !== "string")
|
|
204
|
-
return { present: true, owner: undefined };
|
|
92
|
+
return { present: true, owner: undefined };
|
|
205
93
|
const taskId = row.owner_task_id;
|
|
206
|
-
// 键**缺席**而不是 present-as-undefined:core 的比较器把「缺 taskId」与「显式 undefined」当同一个值,
|
|
207
|
-
// 但调用方(和本仓的 toEqual 钉)看得见键在不在,落库的 NULL 只能还原成缺席形。
|
|
208
94
|
return { present: true, owner: typeof taskId === "string" ? { sessionId, taskId } : { sessionId } };
|
|
209
95
|
}
|
|
210
|
-
/**
|
|
211
|
-
* #119 —— 出处的**读面**。未知 ref 与「存了但无属主」两种情况都答 `undefined`:读面对二者一视同仁
|
|
212
|
-
* (fail-closed,谁都没被授权),所以这里也不必把它们分开报。
|
|
213
|
-
*/
|
|
214
96
|
async ownerOf(ref) {
|
|
215
97
|
if (this.isUnsafeRef(ref))
|
|
216
|
-
return undefined;
|
|
98
|
+
return undefined;
|
|
217
99
|
return (await this.readOwner(ref)).owner;
|
|
218
100
|
}
|
|
219
101
|
async get(ref, opts = {}) {
|
|
220
|
-
// D1 读面半条(RB-266「the read face degrades instead」):unsafe ref 在读面**降级**返回 undefined
|
|
221
|
-
// (该 ref 写入面就被拒 ⇒ 必不在库),不把驱动层错误(PG 22021 对 NUL)漏给调用方。
|
|
222
102
|
if (this.isUnsafeRef(ref))
|
|
223
103
|
return undefined;
|
|
224
104
|
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
225
105
|
const hasLimit = opts.limit !== undefined && Number.isFinite(opts.limit);
|
|
226
106
|
const limit = hasLimit ? Math.max(0, Math.floor(opts.limit)) : 0;
|
|
227
|
-
// One round-trip: SUBSTRING is 1-indexed (offset+1); CHAR_LENGTH gives the full (character) length for the
|
|
228
|
-
// slice header. With no limit, return the remainder from `offset`.
|
|
229
107
|
const sql = hasLimit
|
|
230
108
|
? this.q("SELECT SUBSTRING(content, ?, ?) AS slice, CHAR_LENGTH(content) AS total FROM tool_result WHERE ref = ?", "SELECT SUBSTRING(content FROM $1::int FOR $2::int) AS slice, CHAR_LENGTH(content) AS total FROM tool_result WHERE ref = $3")
|
|
231
109
|
: this.q("SELECT SUBSTRING(content, ?) AS slice, CHAR_LENGTH(content) AS total FROM tool_result WHERE ref = ?", "SELECT SUBSTRING(content FROM $1::int) AS slice, CHAR_LENGTH(content) AS total FROM tool_result WHERE ref = $2");
|
|
232
110
|
const params = hasLimit ? [offset + 1, limit, ref] : [offset + 1, ref];
|
|
233
111
|
const { rows } = await this.db.query(sql, params);
|
|
234
112
|
if (rows.length === 0)
|
|
235
|
-
return undefined;
|
|
113
|
+
return undefined;
|
|
236
114
|
return { content: String(rows[0].slice ?? ""), offset, totalChars: Number(rows[0].total) };
|
|
237
115
|
}
|
|
238
|
-
/** TTL reap: delete results older than `cutoffMs`. Returns rows removed.
|
|
239
|
-
* C6: a real DB error must NOT collapse into the same `0` a legitimate "nothing was old enough" returns —
|
|
240
|
-
* those are different facts (query failed vs. query succeeded on an empty set) and folding them together
|
|
241
|
-
* makes a stuck/broken reaper indistinguishable from a healthy quiet one. Matches every sibling reaper's
|
|
242
|
-
* form (roster-store-sql.ts `reapOlderThan`, checkpoint-store-sql.ts `reapExpired`,
|
|
243
|
-
* workflow-journal-store-sql.ts `reapExpired`): let the error propagate — the periodic-sweep call site
|
|
244
|
-
* (boot/reapers.ts) already wraps this call in `.catch(() => undefined)` so the reap loop itself never dies. */
|
|
245
116
|
async reapOlderThan(cutoffMs) {
|
|
246
117
|
const { affected } = await this.db.query(this.q("DELETE FROM tool_result WHERE created_at < ?", "DELETE FROM tool_result WHERE created_at < $1"), [new Date(cutoffMs)]);
|
|
247
118
|
return affected;
|
|
248
119
|
}
|
|
249
|
-
/**
|
|
250
|
-
* E21 (§0.5 session delete) — purge offloaded tool results for one session. Selection is by the row's
|
|
251
|
-
* **recorded provenance** (`owner_session_id`, the #119 column `put` writes in the same statement as the
|
|
252
|
-
* content) and by NOTHING else — **zero ref-derived arms**(codex R1-[high] 收窄:连单射新前缀也不作
|
|
253
|
-
* 属主证据,理由在方法体内逐字引 core 顶注)。Returns `{deleted, unattributable}`; why NO prefix
|
|
254
|
-
* (legacy `_` or injective `~`) is a deletion selector is inline below.
|
|
255
|
-
*
|
|
256
|
-
* ⚠️ 旧注两处已作废(重扫二轮更正,留档防复辟):① 「core namespaces every ref as `tr_<sid>_<callId>`」——
|
|
257
|
-
* 5.26.0 起是 `~` 分隔的单射四段形,`_` 形只剩存量;② 「there is no `session_id` (or `owner`) column to
|
|
258
|
-
* guard on」—— #119 起**有**(`owner_session_id` / `owner_task_id`,boot 期还有拒启断言把没升级的表挡在
|
|
259
|
-
* 外面),所以「按出处选行」不再是「awkward ref-correlated EXISTS」,它就是本方法现在的主选择器。
|
|
260
|
-
*
|
|
261
|
-
* 路由侧的 owner 门照旧在(DELETE 路由先 `ownerOf` 对属主、同一协调器里先跑 owner-guarded 的 run 账本
|
|
262
|
-
* 删除),但它不再是唯一防线:LIKE 转义只挡「精心构造的 id 把前缀撑宽」,挡不住「**合法**的邻会话 id
|
|
263
|
-
* 恰好是本会话 id 的扩展」——那一条现在由出处判定挡。
|
|
264
|
-
*/
|
|
265
120
|
async deleteBySession(sessionId) {
|
|
266
|
-
// 🔴 **选行只按记录下来的出处,一个 ref 也不解析**(重扫二轮红先修 + codex 复审 R1-[high] 收窄)。
|
|
267
|
-
// core `tool-result-store.d.ts` 对本操作写死了三条,逐字照做:
|
|
268
|
-
// ① 「**selection is by RECORDED PROVENANCE, never by parsing the ref.** A ref is an opaque handle:
|
|
269
|
-
// its session segment may be folded, refs minted before the injective form decompose two ways
|
|
270
|
-
// (`tr_team_blue_x` is both ("team","blue_x") and ("team_blue","x")), **and a caller may mint its
|
|
271
|
-
// own**. Prefix-matching a ref therefore both misses rows and reaches rows of a NEIGHBOURING
|
|
272
|
-
// session, and over-deletion here destroys a live session's readable bytes」;
|
|
273
|
-
// ② 「**`taskId` is ignored in the match**」——`{sessionId, taskId}` 是同一会话的窄化,不是别的属主;
|
|
274
|
-
// ③ 「**an UNOWNED entry is never deleted** … It is counted instead (`unattributable`) so the caller
|
|
275
|
-
// learns the deletion was incomplete rather than being told a clean "done"」。
|
|
276
|
-
//
|
|
277
|
-
// 旧形拿两条 LIKE 前缀全删,踩的正是 ①:提交面对 session id **只限长、不限形**(`server.ts` 的
|
|
278
|
-
// "a client MAY supply an ARBITRARY id … LENGTH-ONLY — NOT a uuidv7 shape check"),而 `_` 在 core 的
|
|
279
|
-
// `NATIVE_REF_CHARSET`(`/^[A-Za-z0-9_.-]+$/`)里 ⇒ 会话 `<uuid>_sub` 合法,它的产物铸出
|
|
280
|
-
// `tr_<uuid>_sub~c1~s<hash>`;删父会话 `<uuid>`(过得了 DELETE 路由的 uuidv7 门)时旧前缀 LIKE 命中它,
|
|
281
|
-
// 一条**活着的**邻会话的卸载全文被销毁,还被算进本次的 `deleted`。
|
|
282
|
-
// 🔴 **新前缀也不作属主证据**(codex R1-[high],验真后收):`tr_<sid>~` 的单射只保证「core 用这个
|
|
283
|
-
// sessionId 铸出来的 ref 长这样」,**不**保证「长这样的 ref 一定是 core 用这个 sessionId 铸的」——
|
|
284
|
-
// `put(ref, content)` 的 ref 是 opaque handle、两参形合法、调用方可自铸(core 顶注 ① 逐字点名)。
|
|
285
|
-
// 拿它删无出处行 = 又一次「按 ref 猜属主」,只是猜得像一点。
|
|
286
|
-
//
|
|
287
|
-
// ⇒ `deleted` = `owner_session_id` 命中的行(ref 形状无关,会话 id 触发 `refSegment` 折叠时也不漏);
|
|
288
|
-
// `unattributable` = 库里**所有**无出处行的计数(与 core `InMemoryToolResultStore.deleteBySession`
|
|
289
|
-
// 逐字同口径:它遍历全表,`provenance === undefined` 的一律 `unattributable++` 且**跳过不删**)。
|
|
290
|
-
// 这个数是「本店有多少行没人认领」这一**全库**事实,读法 = 「这次会话删除不可能是完整的」,
|
|
291
|
-
// 不是「本会话残留了 N 行」。存量无出处行的实体清理归 TTL(`reapOlderThan`)。
|
|
292
|
-
// ⚠️ 登记后续件:`owner_session_id` 上没有索引(本表只有 PK(ref) 与 created_at 索引),删与计数都是
|
|
293
|
-
// 全表扫。会话删除是管理面低频动作,先取正确与完整;要提速得改 DDL + 重生成两方言 baseline,那是独立一车。
|
|
294
121
|
const deletion = await this.db.query(this.q("DELETE FROM tool_result WHERE owner_session_id = ?", "DELETE FROM tool_result WHERE owner_session_id = $1"), [sessionId]);
|
|
295
122
|
const { rows } = await this.db.query(this.q("SELECT COUNT(*) AS n FROM tool_result WHERE owner_session_id IS NULL", "SELECT COUNT(*) AS n FROM tool_result WHERE owner_session_id IS NULL"));
|
|
296
123
|
return { deleted: deletion.affected, unattributable: Number(rows[0]?.n ?? 0) };
|
|
297
124
|
}
|
|
298
125
|
}
|
|
299
|
-
/** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
|
|
300
126
|
export class TiDBToolResultStore extends SqlToolResultStore {
|
|
301
127
|
constructor(pool) {
|
|
302
128
|
super(mysqlDriver(pool));
|
|
303
129
|
}
|
|
304
130
|
}
|
|
305
|
-
/** PostgreSQL binding — historical class name + ctor shape preserved. */
|
|
306
131
|
export class PgToolResultStore extends SqlToolResultStore {
|
|
307
132
|
constructor(pool) {
|
|
308
133
|
super(pgDriver(pool));
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord } from "@sema-agent/core";
|
|
2
2
|
export const USAGE_WINDOW_TABLE = "usage_window";
|
|
3
|
-
/** record 列 → UsageWindowRecord。坏形 fail-loud(治理窗算错=计费面撒谎;与快照/凭据类 store 的
|
|
4
|
-
* onCorruptRead 披露座不同族——这里没有「跳过一行继续」的安全语义,账本坏=停)。 */
|
|
5
3
|
function parseRecord(raw, key) {
|
|
6
4
|
let v;
|
|
7
5
|
try {
|
|
@@ -14,8 +12,6 @@ function parseRecord(raw, key) {
|
|
|
14
12
|
if (typeof v !== "object" || v === null || !Array.isArray(r.slots) || !Array.isArray(r.buckets)) {
|
|
15
13
|
throw new Error(`usage_window record for key "${key}" has an invalid shape (expected {slots[],buckets[]}) — refusing to mis-count a governance window`);
|
|
16
14
|
}
|
|
17
|
-
// 深度对齐 core FileUsageWindowStore(validateSlots/validateBuckets):内层坏形若放过,非数值
|
|
18
|
-
// tokens 参与累加得 "0oops" ⇒ exhausted:false,超额账户被判未耗尽。逐条校验,任一不合即 fail-loud。
|
|
19
15
|
const badShape = (detail) => new Error(`usage_window record for key "${key}" has an invalid shape (${detail}) — refusing to mis-count a governance window`);
|
|
20
16
|
const finite = (x) => typeof x === "number" && Number.isFinite(x);
|
|
21
17
|
const slots = r.slots.map((s) => {
|
|
@@ -62,7 +58,6 @@ export class TiDBUsageWindowStore {
|
|
|
62
58
|
const c = await this.pool.getConnection();
|
|
63
59
|
try {
|
|
64
60
|
await c.beginTransaction();
|
|
65
|
-
// ensure-first:幂等建空行(EMPTY record)⇒ 下一步 FOR UPDATE 行恒在(锁语义干净,无 gap-lock 死锁形)。
|
|
66
61
|
await c.query(`INSERT IGNORE INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES (?, ?, ?)`, [
|
|
67
62
|
key,
|
|
68
63
|
JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
|
|
@@ -70,7 +65,6 @@ export class TiDBUsageWindowStore {
|
|
|
70
65
|
]);
|
|
71
66
|
const [rows] = (await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ? FOR UPDATE`, [key]));
|
|
72
67
|
if (rows.length === 0) {
|
|
73
|
-
// 本表无删除臂(不同于 mailbox 的 drop/reap),ensure 后空 = 真障碍 fail-loud。
|
|
74
68
|
throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
|
|
75
69
|
}
|
|
76
70
|
const record = parseRecord(rows[0].record, key);
|
|
@@ -101,8 +95,6 @@ export class PgUsageWindowStore {
|
|
|
101
95
|
const c = await this.pool.connect();
|
|
102
96
|
try {
|
|
103
97
|
await c.query("BEGIN");
|
|
104
|
-
// ensure-first(TiDB 同注):ON CONFLICT DO NOTHING 幂等建空行,再锁——插入不带锁,锁只从
|
|
105
|
-
// FOR UPDATE 来(复审靶1 的 PG 交错:并发首充双双 ensure,后到方 DO NOTHING 后必须真锁行)。
|
|
106
98
|
await c.query(`INSERT INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES ($1, $2, $3) ON CONFLICT (usage_key) DO NOTHING`, [
|
|
107
99
|
key,
|
|
108
100
|
JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
|
|
@@ -1,63 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/102 (K-8 CC full-body) — the WebSearch BACKEND (BRAIN / model-service leg) that core leaves
|
|
3
|
-
* deployment-injected (`WebSearchConfig.search`; core ships NONE, the same boundary as the model gateway). Wiring a
|
|
4
|
-
* backend here is what makes `assembleFullBodyTools({ webSearch })` actually mount the WebSearch tool.
|
|
5
|
-
*
|
|
6
|
-
* Multi-provider, picked by `WEB_SEARCH_PROVIDER`:
|
|
7
|
-
* - `brave` — Brave Web Search API (GET, `X-Subscription-Token` header)
|
|
8
|
-
* - `tavily` — Tavily Search API (POST, `Authorization: Bearer`, native include/exclude_domains)
|
|
9
|
-
* - `searxng` — a self-hosted SearXNG instance (GET `?format=json`, no auth)
|
|
10
|
-
*
|
|
11
|
-
* Each returns `{ title, url, snippet }[]`; core does the rest — it `delimitUntrusted`-fences the (UNTRUSTED) results
|
|
12
|
-
* and re-enforces the `allowed_domains`/`blocked_domains` FLOOR on them, so honoring `opts` here is an optimization,
|
|
13
|
-
* not a correctness requirement (a backend MAY ignore it). The API key comes from deployment env — it never enters
|
|
14
|
-
* the model prompt or the tool args. `effect:"read"` (idempotent) is core's concern; this is just the transport.
|
|
15
|
-
*
|
|
16
|
-
* ── 立案:装配层四条缺口([2176] 我方认领,2026-08-01 调研,**尚未实施**)────────────────────
|
|
17
|
-
*
|
|
18
|
-
* ① **`opts` 仍有 brave 腿不消费**(只剩 `brave` 的 switch 分支不传 `opts`;`searxng` 腿已随 ② 的
|
|
19
|
-
* 还债改动接住 `opts` 并透传给 core 的 adapter,由它把 `allowedDomains` 原生下推成 `site:` 前缀。
|
|
20
|
-
* 立案时的原话是「`opts` 只有 tavily 腿在消费(`brave`/`searxng` 都不传)」,② 落地后已部分还清)。
|
|
21
|
-
* 定性:**优化缺失,不是正确性缺口** —— 上面那句「core 再执行一次 FLOOR」经亲验属实
|
|
22
|
-
* (core `dist/tools/web.js` 的 `webSearchResultAllowed(url, allowed, blocked)`,按 hostname
|
|
23
|
-
* 逐条过滤 blocked/allowed)。所以模型请求的域限制**不会**被静默丢弃。
|
|
24
|
-
* 但代价真实:后端搜回一堆注定被 core 丢掉的结果 ⇒ 白花配额、白等延迟,极端情况下
|
|
25
|
-
* 「限域搜索」返回 0 条(拿回来的 N 条全不在 allowed 里)而后端其实能做原生过滤。
|
|
26
|
-
* ⚠️ 判据留痕:我第一轮把这条判成了「约束被静默丢弃」的安全缺口,是**亲读 core 结果侧代码**
|
|
27
|
-
* 才推翻的。「别处已经处理了」这种声明必须验证 —— 这次它是真的,但真假只能靠读。
|
|
28
|
-
*
|
|
29
|
-
* ② ~~**searxng 腿是自铸的第二真源**~~ —— ✅ **已还(2026-08-01,[2229] 裁定 searxng 为国内可达性
|
|
30
|
-
* 主路线之后)**:本腿改为消费 core 的 `createSearxngSearchBackend`,外层保住 maxResults 截断 /
|
|
31
|
-
* 统一形状 / 坏 payload 观测器三样(各有红先钉 + 变异实测),白得 `site:` 原生下推、`extraParams`、
|
|
32
|
-
* 自带 timeout。以下为原始立案文本,留档:
|
|
33
|
-
* core 2.10 起已导出 `createSearxngSearchBackend`
|
|
34
|
-
* (+`SearxngBackendOptions`),而且比本文件的实现多三样:`allowedDomains` 拼 `site:` 原生下推、
|
|
35
|
-
* `extraParams`(实例特定的 `engines=`/`language=` 等)、**自带 timeout**
|
|
36
|
-
* (`AbortSignal.any([signal, timeout])`;本文件的 searxng 腿只吃外部 signal)。
|
|
37
|
-
* 我在黑板 [2190] 已公开表态「按 core 的接、不再自铸第二真源」—— 这条是待兑现的债。
|
|
38
|
-
* 换的时候要核对的等价点:本文件的 `normalize()` 统一形状 + `max` 截断 +
|
|
39
|
-
* `setWebSearchBadPayloadObserver` 观测器,core 的 adapter 都没有,得在外层补回来。
|
|
40
|
-
*
|
|
41
|
-
* ③ **四级优先级语义未定**(env / config-center / per-scenario / per-request 谁压谁)。
|
|
42
|
-
* 🔴 三问里「谁被伤」这条决定了默认值:一个**只配了模型 key** 的既有部署,若新语义让某一级
|
|
43
|
-
* 能自行打开搜索,它就在升级后**凭空长出一条出网工具** —— 那是安全姿态变更,不是特性。
|
|
44
|
-
* ⇒ 语义定稿前,任何新的开启通道**默认 OFF**;开启必须是部署方的显式动作。
|
|
45
|
-
*
|
|
46
|
-
* ④ ~~**探活 verb 缺席**~~ —— ✅ **已还(2026-08-01)**:`shouldProbeWebSearchOnBoot` 门控 +
|
|
47
|
-
* `main.ts` 消费 core 的 `probeSearchBackend`。**默认 OFF**(开着=给每个既有部署的每次启动凭空
|
|
48
|
-
* 加一次出网)、**只认明确真值**(含糊值当没开,别替部署方猜)、**失败只 warn 不拒启**(功能型
|
|
49
|
-
* 能力缺席 ⇒ 降级)、**不 await**(拖住 boot 就把可选工具变成启动依赖)。以下为原始立案文本,留档:
|
|
50
|
-
* core 2.10 起导出 `probeSearchBackend(search, {timeoutMs}) →
|
|
51
|
-
* {ok:true,results:n} | {ok:false,error}`,正好填这条:部署方现在只能靠「跑一个真任务」
|
|
52
|
-
* 验证搜索配对不对。挂哪儿(boot 期一次性 warn / `/health` 子字段 / 显式端点)未定 ——
|
|
53
|
-
* 注意 boot 期探活会给每次启动加一次出网,多租/离线部署要能关。
|
|
54
|
-
*/
|
|
55
1
|
import { createSearxngSearchBackend } from "@sema-agent/core";
|
|
56
2
|
const DEFAULT_MAX_RESULTS = 10;
|
|
57
3
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
58
4
|
const BRAVE_BASE = "https://api.search.brave.com/res/v1/web/search";
|
|
59
5
|
const TAVILY_BASE = "https://api.tavily.com/search";
|
|
60
|
-
/** Combine the caller's AbortSignal with a per-search timeout into one signal; `done()` clears the timer + listener. */
|
|
61
6
|
function withTimeout(signal, ms) {
|
|
62
7
|
const ctrl = new AbortController();
|
|
63
8
|
const onAbort = () => ctrl.abort();
|
|
@@ -74,13 +19,6 @@ function withTimeout(signal, ms) {
|
|
|
74
19
|
async function errorText(res) {
|
|
75
20
|
return (await res.text().catch(() => "")).slice(0, 200);
|
|
76
21
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Normalize an UNTRUSTED provider results payload: guard a non-array (a misconfigured proxy / buggy self-hosted
|
|
79
|
-
* SearXNG could return a non-list or null elements), skip null/non-object rows, coerce every field to a string, drop
|
|
80
|
-
* url-less rows, cap to max. `pick` maps each provider's row to the common shape (Brave→description, others→content).
|
|
81
|
-
*/
|
|
82
|
-
/** LOW (SILENT-FALLBACK P1): a provider returning a non-array `results` used to silently become zero results
|
|
83
|
-
* for the model. main.ts wires this to web_search_bad_payload_total{provider} + a warn. */
|
|
84
22
|
let onBadPayload;
|
|
85
23
|
export function setWebSearchBadPayloadObserver(fn) {
|
|
86
24
|
onBadPayload = fn;
|
|
@@ -91,18 +29,18 @@ function normalize(raw, pick, max, provider) {
|
|
|
91
29
|
try {
|
|
92
30
|
onBadPayload?.(provider);
|
|
93
31
|
}
|
|
94
|
-
catch {
|
|
32
|
+
catch { }
|
|
95
33
|
}
|
|
96
|
-
return [];
|
|
97
|
-
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
98
36
|
const out = [];
|
|
99
37
|
for (const row of raw) {
|
|
100
38
|
if (!row || typeof row !== "object")
|
|
101
|
-
continue;
|
|
39
|
+
continue;
|
|
102
40
|
const f = pick(row);
|
|
103
41
|
const url = typeof f.url === "string" ? f.url : "";
|
|
104
42
|
if (!url)
|
|
105
|
-
continue;
|
|
43
|
+
continue;
|
|
106
44
|
out.push({ title: typeof f.title === "string" ? f.title : "", url, snippet: typeof f.snippet === "string" ? f.snippet : "" });
|
|
107
45
|
if (out.length >= max)
|
|
108
46
|
break;
|
|
@@ -114,8 +52,7 @@ async function braveSearch(fetchImpl, cfg, query, max, signal) {
|
|
|
114
52
|
throw new Error("WEB_SEARCH_API_KEY is required for the brave provider");
|
|
115
53
|
const url = new URL(cfg.endpoint || BRAVE_BASE);
|
|
116
54
|
url.searchParams.set("q", query);
|
|
117
|
-
url.searchParams.set("count", String(Math.min(20, max)));
|
|
118
|
-
// Brave has no native include/exclude_domains param — core re-enforces the domain floor on the results.
|
|
55
|
+
url.searchParams.set("count", String(Math.min(20, max)));
|
|
119
56
|
const res = await fetchImpl(url, { headers: { Accept: "application/json", "X-Subscription-Token": cfg.apiKey }, signal });
|
|
120
57
|
if (!res.ok)
|
|
121
58
|
throw new Error(`brave search failed (${res.status}): ${await errorText(res)}`);
|
|
@@ -127,7 +64,7 @@ async function tavilySearch(fetchImpl, cfg, query, max, signal, opts) {
|
|
|
127
64
|
throw new Error("WEB_SEARCH_API_KEY is required for the tavily provider");
|
|
128
65
|
const body = { query, max_results: Math.min(20, max) };
|
|
129
66
|
if (opts?.allowedDomains?.length)
|
|
130
|
-
body.include_domains = opts.allowedDomains.slice(0, 300);
|
|
67
|
+
body.include_domains = opts.allowedDomains.slice(0, 300);
|
|
131
68
|
if (opts?.blockedDomains?.length)
|
|
132
69
|
body.exclude_domains = opts.blockedDomains.slice(0, 150);
|
|
133
70
|
const res = await fetchImpl(cfg.endpoint || TAVILY_BASE, {
|
|
@@ -141,27 +78,9 @@ async function tavilySearch(fetchImpl, cfg, query, max, signal, opts) {
|
|
|
141
78
|
const data = (await res.json());
|
|
142
79
|
return normalize(data.results, (r) => ({ title: r.title, url: r.url, snippet: r.content }), max, "tavily");
|
|
143
80
|
}
|
|
144
|
-
/**
|
|
145
|
-
* SearXNG 腿 —— **消费 core 的 `createSearxngSearchBackend`,不再自铸**(#81② 还债;[2190] 表态、
|
|
146
|
-
* [2229] 裁定 searxng 是国内可达性主路线之后该还的那笔)。
|
|
147
|
-
*
|
|
148
|
-
* core 的 adapter 比本文件此前的手写实现多三样,换过来就白得:
|
|
149
|
-
* · `allowedDomains` **原生下推**成 `site:a OR site:b` 前缀(此前本腿完全忽略 opts —— core 工具层
|
|
150
|
-
* 有结果侧 FLOOR 兜底所以不是正确性缺口,但白搜一趟、白花配额,极端情况下限域搜索回 0 条);
|
|
151
|
-
* · `extraParams`(实例特定的 `engines=` / `language=` 等)——一键装 SearXNG 的向导要用;
|
|
152
|
-
* · **自带 timeout**(`AbortSignal.any([signal, timeout])`;此前本腿只吃外部 signal)。
|
|
153
|
-
*
|
|
154
|
-
* ⚠️ core adapter **不管**这三样,必须留在外层(换真源最容易丢的就是它们,已上红先钉):
|
|
155
|
-
* ① `maxResults` 截断 ② 统一形状 `{title,url,snippet}` ③ 坏 payload 观测器。
|
|
156
|
-
*/
|
|
157
81
|
async function searxngSearch(fetchImpl, cfg, query, max, signal, timeoutMs, opts) {
|
|
158
82
|
if (!cfg.endpoint)
|
|
159
83
|
throw new Error("WEB_SEARCH_ENDPOINT (the SearXNG instance URL) is required for the searxng provider");
|
|
160
|
-
// 🔴 timeoutMs 必须显式传:core 的 adapter 内部是 `options.timeoutMs ?? 10_000`,不传就用它自己的
|
|
161
|
-
// 10 秒。外层 createWebSearchBackend 那道 withTimeout 只在**更短**时先到 —— 部署方把
|
|
162
|
-
// WEB_SEARCH_TIMEOUT_MS 设成大于 10s 的值(慢实例/自建 SearXNG 的常见需要)会被**静默封顶在 10s**,
|
|
163
|
-
// 而 3.18.0 的 CHANGELOG 还把 core「自带 timeout」当纯收益卖(见 3.20.0 的更正段)。
|
|
164
|
-
// 两层同值不冲突:内层是 adapter 自己的 AbortSignal.any,外层是本文件的 withTimeout。
|
|
165
84
|
const search = createSearxngSearchBackend(cfg.endpoint, {
|
|
166
85
|
fetchImpl,
|
|
167
86
|
timeoutMs,
|
|
@@ -172,25 +91,18 @@ async function searxngSearch(fetchImpl, cfg, query, max, signal, timeoutMs, opts
|
|
|
172
91
|
rows = await search(query, signal, opts);
|
|
173
92
|
}
|
|
174
93
|
catch (e) {
|
|
175
|
-
// core 对「results 不是数组」抛具名错;本腿的既有契约是**不抛给上层、走观测器**(review LOW:
|
|
176
|
-
// 畸形 payload 不该变成裸 TypeError 上抛)。保持既有契约,把它翻译回观测器 + 空结果。
|
|
177
94
|
const msg = e instanceof Error ? e.message : String(e);
|
|
178
95
|
if (/no results array/i.test(msg)) {
|
|
179
96
|
try {
|
|
180
97
|
onBadPayload?.("searxng");
|
|
181
98
|
}
|
|
182
|
-
catch {
|
|
99
|
+
catch { }
|
|
183
100
|
return [];
|
|
184
101
|
}
|
|
185
102
|
throw e;
|
|
186
103
|
}
|
|
187
|
-
// core 已回 `{title,url,snippet}` 形;仍过一遍 normalize —— ① 截断 ② 形状兜底 ③ 观测器同源。
|
|
188
104
|
return normalize(rows, (r) => ({ title: r.title, url: r.url, snippet: r.snippet }), max, "searxng");
|
|
189
105
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Build the `WebSearchConfig` (a `search` fn + `maxResults`) for `assembleFullBodyTools({ webSearch })`. The API key
|
|
192
|
-
* stays captured in this closure — it is never surfaced to the model or the tool args.
|
|
193
|
-
*/
|
|
194
106
|
export function createWebSearchBackend(cfg) {
|
|
195
107
|
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
196
108
|
const maxResults = Math.max(1, Math.min(20, cfg.maxResults ?? DEFAULT_MAX_RESULTS));
|
|
@@ -213,22 +125,8 @@ export function createWebSearchBackend(cfg) {
|
|
|
213
125
|
};
|
|
214
126
|
return { search, maxResults };
|
|
215
127
|
}
|
|
216
|
-
/**
|
|
217
|
-
* `WEB_SEARCH_SEARXNG_PARAMS` / `settings.webSearch.searxngParams` 的解析 —— SearXNG 实例侧参数
|
|
218
|
-
* (`engines=` / `language=` 等)。
|
|
219
|
-
*
|
|
220
|
-
* 🔴 [2240] cli 逐串直证:3.18.0 我加了 `searxngParams` 字段和消费点,**却没开任何配置入口** ——
|
|
221
|
-
* 两条门都没有位置填它,于是我在 [2239] 说的「这条是给你们的」是句空话。单测直接构造
|
|
222
|
-
* `WebSearchBackendConfig` 传进去,**天然绕过了配置入口这一层**,于是「能力在」与「配得进」之间
|
|
223
|
-
* 断开,而两边的绿都是真的。判据:**按消费端实际能填的那个入口验,不是按自己构造的对象验。**
|
|
224
|
-
*
|
|
225
|
-
* 形状:`k=v` 用 `;` 分隔(`engines=bing,duckduckgo;language=zh-CN`)——值里本来就常含逗号,
|
|
226
|
-
* 所以分隔符取 `;` 而不是 `,`。整串解析不出任何一对 ⇒ **返回 undefined,键整个不铸**
|
|
227
|
-
* (不产出空对象:空对象会让下游以为「配了但是空的」,与本仓「缺席就要缺席得干净」同源)。
|
|
228
|
-
*/
|
|
229
128
|
export function parseSearxngParams(raw) {
|
|
230
129
|
if (typeof raw === "object" && raw !== null) {
|
|
231
|
-
// settings 门传的是已解析的表;只收「字符串→字符串」,任一值非字符串 ⇒ 整表不铸(不半吞)。
|
|
232
130
|
const entries = Object.entries(raw);
|
|
233
131
|
if (entries.length === 0 || entries.some(([, v]) => typeof v !== "string"))
|
|
234
132
|
return undefined;
|
|
@@ -248,44 +146,26 @@ export function parseSearxngParams(raw) {
|
|
|
248
146
|
}
|
|
249
147
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
250
148
|
}
|
|
251
|
-
/**
|
|
252
|
-
* #81④ 探活门控 —— **默认 OFF**,显式开才探。
|
|
253
|
-
*
|
|
254
|
-
* 为什么默认 OFF:探活是一次**真出网**。开着就等于给每个既有部署的每次启动凭空加一次外呼 ——
|
|
255
|
-
* 多租/离线部署不能被这样动。这与 #81③「任何新的开启通道默认 OFF」同源:
|
|
256
|
-
* 一个只配了模型 key 的部署,不该因为升级而多出网络行为。
|
|
257
|
-
*
|
|
258
|
-
* 为什么只认明确真值:`"yes"`/`"on"` 这类**含糊值当没开**。含糊值上放行 = 替部署方猜意图,
|
|
259
|
-
* 而猜错的方向是「凭空出网」。缺席要缺席得干净,含糊也一样。
|
|
260
|
-
*/
|
|
261
149
|
export function shouldProbeWebSearchOnBoot(env = process.env) {
|
|
262
150
|
const raw = env.WEB_SEARCH_PROBE_ON_BOOT?.trim().toLowerCase();
|
|
263
151
|
return raw === "true" || raw === "1";
|
|
264
152
|
}
|
|
265
|
-
/** Parse the deployment env into a backend config; `undefined` when no provider is set (⇒ WebSearch not assembled). */
|
|
266
153
|
export function webSearchConfigFromEnv(env = process.env) {
|
|
267
154
|
const provider = env.WEB_SEARCH_PROVIDER?.trim().toLowerCase();
|
|
268
155
|
if (provider !== "brave" && provider !== "tavily" && provider !== "searxng")
|
|
269
156
|
return undefined;
|
|
270
|
-
const searxngParams = parseSearxngParams(env.WEB_SEARCH_SEARXNG_PARAMS);
|
|
157
|
+
const searxngParams = parseSearxngParams(env.WEB_SEARCH_SEARXNG_PARAMS);
|
|
271
158
|
const maxRaw = Number(env.WEB_SEARCH_MAX_RESULTS ?? "");
|
|
272
159
|
const timeoutRaw = Number(env.WEB_SEARCH_TIMEOUT_MS ?? "");
|
|
273
160
|
return {
|
|
274
161
|
provider,
|
|
275
162
|
...(env.WEB_SEARCH_API_KEY ? { apiKey: env.WEB_SEARCH_API_KEY } : {}),
|
|
276
163
|
...(env.WEB_SEARCH_ENDPOINT ? { endpoint: env.WEB_SEARCH_ENDPOINT } : {}),
|
|
277
|
-
...(searxngParams ? { searxngParams } : {}),
|
|
164
|
+
...(searxngParams ? { searxngParams } : {}),
|
|
278
165
|
...(Number.isFinite(maxRaw) && maxRaw > 0 ? { maxResults: Math.floor(maxRaw) } : {}),
|
|
279
166
|
...(Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? { timeoutMs: Math.floor(timeoutRaw) } : {}),
|
|
280
167
|
};
|
|
281
168
|
}
|
|
282
|
-
/**
|
|
283
|
-
* Parse an UNTRUSTED per-request `body.settings.webSearch` (the user's shell settings — `{provider, apiKey?, endpoint?,
|
|
284
|
-
* maxResults?}`) into a backend config. Returns `undefined` for a missing/invalid provider — NEVER throws, so a bad
|
|
285
|
-
* per-request config falls back to the deployment-env backend rather than failing the task. 🔒 The CALLER gates this to
|
|
286
|
-
* the single-user host lane (a per-request `endpoint`/`apiKey` is a capability config; on multi-tenant a tenant could
|
|
287
|
-
* point `searxng` at an internal URL = SSRF, so multi-tenant uses ONLY the deployment-env backend — design/107 axis).
|
|
288
|
-
*/
|
|
289
169
|
export function webSearchConfigFromSettings(raw) {
|
|
290
170
|
if (!raw || typeof raw !== "object")
|
|
291
171
|
return undefined;
|
|
@@ -298,8 +178,6 @@ export function webSearchConfigFromSettings(raw) {
|
|
|
298
178
|
provider,
|
|
299
179
|
...(typeof s.apiKey === "string" && s.apiKey ? { apiKey: s.apiKey } : {}),
|
|
300
180
|
...(typeof s.endpoint === "string" && s.endpoint ? { endpoint: s.endpoint } : {}),
|
|
301
|
-
// [2240]:与 endpoint **同门同辖域** —— 安全边界不在字段粒度,而在调用方把整条门 gate 到
|
|
302
|
-
// 单用户 host lane(见本函数头注的 SSRF 段)。单独把它挡在门外是个没有理由的例外。
|
|
303
181
|
...(() => {
|
|
304
182
|
const p = parseSearxngParams(s.searxngParams);
|
|
305
183
|
return p ? { searxngParams: p } : {};
|