@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
package/dist/config.js
CHANGED
|
@@ -3,12 +3,12 @@ import { join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, isThinkingLevel, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveLockedKeys } from "@sema-agent/core";
|
|
5
5
|
import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/registry-core";
|
|
6
|
-
import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
|
|
6
|
+
import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
|
|
7
7
|
import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
8
|
-
import { isV2ScopeKey } from "./memory-scope.js";
|
|
8
|
+
import { isV2ScopeKey } from "./memory-scope.js";
|
|
9
9
|
import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
|
|
10
|
-
import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
|
|
11
|
-
import { RETENTION_MODES } from "./config-types.js";
|
|
10
|
+
import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
|
|
11
|
+
import { RETENTION_MODES } from "./config-types.js";
|
|
12
12
|
export { RETENTION_MODES } from "./config-types.js";
|
|
13
13
|
function csv(name) {
|
|
14
14
|
return (process.env[name] ?? "")
|
|
@@ -17,11 +17,6 @@ function csv(name) {
|
|
|
17
17
|
.filter(Boolean);
|
|
18
18
|
}
|
|
19
19
|
const AUTONOMY_MODES = ["read-only", "ask", "plan", "auto"];
|
|
20
|
-
/** Parse the AUTONOMY env into a validated autonomy mode. Unset/empty → undefined (unmanaged → no extra
|
|
21
|
-
* tightening). An UNKNOWN value FAILS at startup rather than silently becoming a no-op (a typo'd `AUTONOMY=readonly`
|
|
22
|
-
* must not silently leave a deployment ungoverned — fail-loud, same discipline as numEnv). Exported so the HOT
|
|
23
|
-
* config overlay (config-center/facade.ts `applyRuntimeHot`) can re-derive the ENV BASELINE to revert to when center
|
|
24
|
-
* stops managing `autonomy` (a stale center override must not stick — see applyRuntimeHot). */
|
|
25
20
|
export function parseAutonomy(raw) {
|
|
26
21
|
const v = raw?.trim();
|
|
27
22
|
if (!v)
|
|
@@ -31,10 +26,6 @@ export function parseAutonomy(raw) {
|
|
|
31
26
|
}
|
|
32
27
|
return v;
|
|
33
28
|
}
|
|
34
|
-
/** design/166-T1 — parse `USAGE_WINDOWS`(单键 JSON 数组;逐窗散键在窗数可变时组合爆炸,JSON 是唯一
|
|
35
|
-
* 封闭形)。unset/空串 → undefined(关)。fail-loud 族(numEnv 同纪律):坏 JSON/非数组/空数组/
|
|
36
|
-
* 非正有限整数/未知 anchor 全部 boot throw 点名——「设了但无意义」静默关掉=部署方以为有治理实际没有。
|
|
37
|
-
* server 严于 core(core 允 maxTokens=0=恒拒锅;这里要求正整数,0 配置显然是笔误不是意图)。 */
|
|
38
29
|
export function parseUsageWindows(raw) {
|
|
39
30
|
const v = raw?.trim();
|
|
40
31
|
if (!v)
|
|
@@ -59,9 +50,6 @@ export function parseUsageWindows(raw) {
|
|
|
59
50
|
});
|
|
60
51
|
return Object.freeze(out);
|
|
61
52
|
}
|
|
62
|
-
/** Resolve a CSV of env-var NAMES into a {NAME: value} map from THIS process's env (out-of-band secret
|
|
63
|
-
* forwarding into a sandbox). A name with no value set is skipped (not forwarded as undefined). The values
|
|
64
|
-
* are secrets — they live only in the returned map (in-memory), never logged. */
|
|
65
53
|
function resolveEnvForward(csvNames) {
|
|
66
54
|
const out = {};
|
|
67
55
|
for (const name of csvNames.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
@@ -80,17 +68,12 @@ function env(name, fallback) {
|
|
|
80
68
|
}
|
|
81
69
|
return v;
|
|
82
70
|
}
|
|
83
|
-
/** 出厂 skill 目录的缺省 = 包内 `skills/`(相对本模块解析),不是 cwd 相对的裸 "skills"。
|
|
84
|
-
* 真 bug(live 战役 2026-07-15 抓获):npm 全局装的 CLE 从用户 cwd 起引擎,裸 "skills" 解析到
|
|
85
|
-
* cwd/skills(不存在)⇒ skills:0,所有烤入 skill 在 npm 分发面全空转(bench 日志 skills:0 即此)。
|
|
86
|
-
* dist/config.js 与 skills/ 同在包根下的 ../skills;src 跑测试时 ../skills 同样是仓根 skills(一致)。
|
|
87
|
-
* SKILLS_DIR 显式设仍覆盖此缺省;Docker(WORKDIR 包根+COPY skills)解析到 /app/skills 不破。 */
|
|
88
71
|
function defaultSkillsDir() {
|
|
89
72
|
try {
|
|
90
73
|
return fileURLToPath(new URL("../skills", import.meta.url));
|
|
91
74
|
}
|
|
92
75
|
catch {
|
|
93
|
-
return "skills";
|
|
76
|
+
return "skills";
|
|
94
77
|
}
|
|
95
78
|
}
|
|
96
79
|
const RETIRED_ENVS = [
|
|
@@ -99,8 +82,7 @@ const RETIRED_ENVS = [
|
|
|
99
82
|
["CONFIG_CENTER_URL", "SEMA_REGISTRY_URL", "renamed"], ["CONFIG_CENTER_TOKEN", "SEMA_REGISTRY_TOKEN", "renamed"],
|
|
100
83
|
["CONFIG_CENTER_DRY_RUN", "SEMA_REGISTRY_DRY_RUN", "renamed"], ["CONFIG_CENTER_WORKER", "SEMA_REGISTRY_WORKER", "renamed"],
|
|
101
84
|
["ANTHROPIC_BASEURL", "ANTHROPIC_BASE_URL", "renamed"],
|
|
102
|
-
["REGISTRY_ADMIN_TOKEN", "SEMA_REGISTRY_ADMIN_TOKEN", "renamed"],
|
|
103
|
-
// [2354] OA 场景整体退役(clay 令 2026-08-01):配置本身作废,无替代名 ⇒ removed 族。
|
|
85
|
+
["REGISTRY_ADMIN_TOKEN", "SEMA_REGISTRY_ADMIN_TOKEN", "renamed"],
|
|
104
86
|
["OA_API_BASEURL", "the OA scenario was removed in 5.0.0", "removed"],
|
|
105
87
|
["OA_SERVICE_TOKEN", "the OA scenario was removed in 5.0.0", "removed"],
|
|
106
88
|
["OA_ISSUE_TOKEN", "the OA scenario was removed in 5.0.0", "removed"],
|
|
@@ -126,21 +108,9 @@ function rejectRetiredEnvs() {
|
|
|
126
108
|
}
|
|
127
109
|
throw new Error(`retired env name(s) set ([2354] 兼容面全清,5.0.0 起拒启): ${parts.join(" | ")}.`);
|
|
128
110
|
}
|
|
129
|
-
/** Numeric env with validation: a non-numeric value FAILS at startup instead of silently becoming `NaN`
|
|
130
|
-
* (which e.g. slips past the `runStaleSec` liveness guard and silently disables the reaper). */
|
|
131
111
|
export function numEnv(name, fallback) {
|
|
132
112
|
return parseNumOrFail(name, env(name, fallback));
|
|
133
113
|
}
|
|
134
|
-
/**
|
|
135
|
-
* 数值 env 的核心判据,**接受 raw 值**(而非自己去读 process.env),这样注入式配置也能复用同一条判据。
|
|
136
|
-
*
|
|
137
|
-
* 提出来的理由(2026-08-01 gap-sweep):`leader/wire.ts` 的四个资源旋钮各自手写了
|
|
138
|
-
* `Number(x) || default`,因为 numEnv 只会读 process.env、够不到它们注入进来的 env 对象。
|
|
139
|
-
* 结果是同一个失败模式在另一处复发,而且**回落方向更坏**:那边落到的是慷慨的默认值,
|
|
140
|
-
* 症状从「设了没生效」变成「想收紧却被松绑」。判据只有一份,谁都能调,才不会各写各的。
|
|
141
|
-
*
|
|
142
|
-
* `undefined`/空串 ⇒ 交给调用方的 fallback 语义处理(本函数只判「给了值但不是数」)。
|
|
143
|
-
*/
|
|
144
114
|
export function parseNumOrFail(name, raw) {
|
|
145
115
|
if (raw === undefined)
|
|
146
116
|
return NaN;
|
|
@@ -149,25 +119,12 @@ export function parseNumOrFail(name, raw) {
|
|
|
149
119
|
throw new Error(`env ${name}="${raw}" must be a number`);
|
|
150
120
|
return n;
|
|
151
121
|
}
|
|
152
|
-
/**
|
|
153
|
-
* `parseNumOrFail` layered with a non-negative floor — the SAME judgment `leader/wire.ts`'s (private,
|
|
154
|
-
* file-local) `parseNumOrFailNonNegative` already applies to its four resource knobs, exported here so a
|
|
155
|
-
* second call site (bake-runner's heartbeat/idle-poll/disk-guard knobs, gap-sweep 2026-08-01) can reuse the
|
|
156
|
-
* one shared primitive instead of re-deriving its own "reject a bad number" scheme. A negative value is
|
|
157
|
-
* JS-truthy, so a bare `|| default` fallback lets it straight through; for a duration knob that means
|
|
158
|
-
* `clock.sleep(-N)` fires ~immediately (the same tight-loop failure NaN causes), and for a threshold knob
|
|
159
|
-
* (`freeGb < minFreeGb`) a negative floor makes the comparison vacuously true/false depending on sign —
|
|
160
|
-
* either way the guard it configures goes silently slack. `undefined`/unset stays NaN (unchanged) so
|
|
161
|
-
* existing `|| fallback` chains built on `parseNumOrFail` are untouched by this stricter sibling.
|
|
162
|
-
*/
|
|
163
122
|
export function parseNumOrFailNonNegative(name, raw) {
|
|
164
123
|
const n = parseNumOrFail(name, raw);
|
|
165
124
|
if (n < 0)
|
|
166
125
|
throw new Error(`env ${name}=${n} must not be negative`);
|
|
167
126
|
return n;
|
|
168
127
|
}
|
|
169
|
-
/** Numeric env with a [min,max] bound (BL-17): an out-of-range value FAILS at startup instead of being
|
|
170
|
-
* silently clamped/applied (e.g. a multi-hour MCP_ELICITATION_TTL_MS would silently never expire). */
|
|
171
128
|
function numEnvBounded(name, fallback, min, max) {
|
|
172
129
|
const n = numEnv(name, fallback);
|
|
173
130
|
if (n < min || n > max)
|
|
@@ -178,55 +135,19 @@ const CONFIG_WARNINGS = [];
|
|
|
178
135
|
export function drainConfigWarnings() {
|
|
179
136
|
return CONFIG_WARNINGS.splice(0, CONFIG_WARNINGS.length);
|
|
180
137
|
}
|
|
181
|
-
/**
|
|
182
|
-
* #210 —— 轮换钥两口(APPROVAL_HMAC_KEYS / PRINCIPAL_JWT_PUBKEYS)的诊断落点。
|
|
183
|
-
*
|
|
184
|
-
* 这两口的 fail-closed 方向本来就是对的(坏形 ⇒ 空钥集 ⇒ 门关着),本批**一个字都不动它**;补的是
|
|
185
|
-
* 「丢弃零留痕」:轮换新钥时写错一个字段,那一条被静默滤掉,旧钥还在于是一切照常,直到旧钥退役当天
|
|
186
|
-
* 全量回决一起 401 —— 故障与配置错误之间隔着几周,没有任何线索指回来。
|
|
187
|
-
*
|
|
188
|
-
* 走 CONFIG_WARNINGS(不是 throw):拒启会把一台**本来还能用旧钥服务**的 worker 直接打死,而这两口的
|
|
189
|
-
* 语义就是「集合里能用几把用几把」。`raw` 只写条目序号 + 缺陷字段名 —— 值不进日志(HMAC 那把是对称
|
|
190
|
-
* 密钥),`used` 恒不带(这不是夹取形,没有"采用值"可言)。
|
|
191
|
-
*
|
|
192
|
-
* 返回一只闭包(捕获 env 名)⇒ 按仓规命名 `announce*` 而非 `build*`。
|
|
193
|
-
*/
|
|
194
138
|
function announceKeySetRejection(envName) {
|
|
195
139
|
return (r) => CONFIG_WARNINGS.push({
|
|
196
140
|
env: envName,
|
|
197
141
|
raw: "<withheld: this env holds credential material>",
|
|
198
|
-
// ⚠️ 整份坏 ≠ 掉了一条(codex 自审 R3-F1 真修):`index: "-"` 说的是**整个** key-set 没解析出来,
|
|
199
|
-
// 于是信任锚**全空** —— 直连门整条关死、所有回决当场失效;而 `entry-rejected` 只是轮换里少进来
|
|
200
|
-
// 一把钥,旧钥还在服务。两者是不同量级的事故,告警规则必须分得开,不能挤在一个事件名下。
|
|
201
142
|
kind: r.index === "-" ? "set-rejected" : "entry-rejected",
|
|
202
143
|
detail: r.index === "-" ? r.reason : `entry #${r.index}: ${r.reason}`,
|
|
203
144
|
});
|
|
204
145
|
}
|
|
205
|
-
/** Boot NOTICES ([792]④): non-error decisions loadConfig makes that the operator should see once (e.g. the
|
|
206
|
-
* MODEL_PROVIDER inference below). Same pre-logger collection pattern as CONFIG_WARNINGS, but each entry
|
|
207
|
-
* carries its own event name + fields (the warnings drain has one fixed "invalid, using default" message
|
|
208
|
-
* which would misdescribe an inference). main.ts drains + logs right after createLogger. */
|
|
209
146
|
const CONFIG_NOTICES = [];
|
|
210
147
|
export function drainConfigNotices() {
|
|
211
148
|
return CONFIG_NOTICES.splice(0, CONFIG_NOTICES.length);
|
|
212
149
|
}
|
|
213
|
-
/** #157([2762]§五,F 类观测,零行为变化)—— 本次 boot 的执行车道落点 + 它的来源。
|
|
214
|
-
* 为什么单开一格:REMOTE_EXEC 未设/空串时的落点由 CONFIG_PROVIDER 隐式决定(local ⇒ host 真机,其余 ⇒
|
|
215
|
-
* 进程内 stub),而这个落点此前在日志里零留痕 —— 运维拿到一台 worker,无从确认「它跑的是哪条执行通道、
|
|
216
|
-
* 是显式配的还是默认落的」。与 CONFIG_WARNINGS / CONFIG_NOTICES 同为 pre-logger 收集(config 解析早于
|
|
217
|
-
* logger 建立),但级别是 **info**:这是每次 boot 都成立的常态事实,不是异常,而 CONFIG_NOTICES 那一格
|
|
218
|
-
* 固定按 warn 排空。
|
|
219
|
-
* 单槽(不是队列):一次 boot 只有一个落点;重复 loadConfig(测试)覆写即可,不会堆积。
|
|
220
|
-
* ⚠️ 写入时机=装配尾(见 loadConfig),不是解析途中:写早了,任何**后置**拒启(limits 域数值、跨域
|
|
221
|
-
* 不变量)都会留下一条「其实没启起来」的落点;而 loadConfig 开头的清槽保证失败的一轮不会把**上一轮**
|
|
222
|
-
* 的落点串给下一次排空(交叉复审 medium,2026-08-06)。
|
|
223
|
-
* ⚠️ 边界:记的是 **ENV 解析出的**落点。local 形的 `config.d/remote-exec.json` 覆写发生在 main.ts 的
|
|
224
|
-
* applyLocalRemoteExec(晚于本行排空),它自己另记 `remote_exec_from_file` —— 两行合读才是最终 lane。 */
|
|
225
150
|
let EXEC_LANE_NOTICE;
|
|
226
|
-
/** 从**已装配好的** remoteExec 派生落点一行(而不是在 REMOTE_EXEC 那条三元链的各臂里分别记账):
|
|
227
|
-
* 分臂记账要靠「校验过了 ⇒ 必落对应那一臂」这条人肉维护的同源假设,链子哪天多一臂就悄悄失真;
|
|
228
|
-
* 取装配结果则天然跟随。`in-process` = REMOTE_EXEC 未设的进程内 stub 姿态,沿用既有词表
|
|
229
|
-
* (config-center/types.ts 的 provider 词表、run-local 的 `?? "in-process"` 同名)。 */
|
|
230
151
|
function buildExecLaneNotice(remoteExec) {
|
|
231
152
|
const explicit = process.env.REMOTE_EXEC !== undefined && process.env.REMOTE_EXEC !== "";
|
|
232
153
|
return {
|
|
@@ -234,14 +155,7 @@ function buildExecLaneNotice(remoteExec) {
|
|
|
234
155
|
source: explicit ? "explicit-env" : process.env.CONFIG_PROVIDER === "local" ? "default-local-provider" : "default-unset",
|
|
235
156
|
};
|
|
236
157
|
}
|
|
237
|
-
/** F10 (codex audit): drain + log BOTH pre-logger diagnostic collectors (S20 soft-knob warnings + [792]④ boot
|
|
238
|
-
* notices) in one place. EVERY entrypoint that calls loadConfig() then builds a logger must call this right
|
|
239
|
-
* after (main.ts, run-local.ts) — a new entry that forgets it would silently drop operator-facing diagnostics
|
|
240
|
-
* (run-local historically did exactly that), so the drain lives beside the collectors, not in one entrypoint. */
|
|
241
158
|
export function logConfigDiagnostics(logger) {
|
|
242
|
-
// #210:四种诊断各有自己的事件名 —— 「invalid, using the default」对一个**合法但越界**的值、对一个被
|
|
243
|
-
// 保留的空串、对一条被丢弃的钥,都是错的描述,而运维的告警规则正是按事件名写的。穷尽 switch:词表加
|
|
244
|
-
// 一个种类而这里漏接 ⇒ `never` 赋值编译红(CLAUDE.md 闭集词表纪律)。
|
|
245
159
|
for (const w of drainConfigWarnings()) {
|
|
246
160
|
const kind = w.kind ?? "invalid-default";
|
|
247
161
|
switch (kind) {
|
|
@@ -271,22 +185,10 @@ export function logConfigDiagnostics(logger) {
|
|
|
271
185
|
}
|
|
272
186
|
for (const n of drainConfigNotices())
|
|
273
187
|
logger.warn(n.event, n.fields);
|
|
274
|
-
// #157 F 类观测:执行车道落点一行(见 EXEC_LANE_NOTICE 头注)。无条件发 —— boot 期本来就只跑一次,
|
|
275
|
-
// 不需要 once-per-hit 抑制。先取后清:排空语义与上面两格一致(重复调用不重放同一条)。`info` 与
|
|
276
|
-
// `debug` 同为可选,理由同上 —— 老 embedder 传进来的 warn-only logger 仍能编译、仍拿得到警告。
|
|
277
188
|
const laneNotice = EXEC_LANE_NOTICE;
|
|
278
189
|
EXEC_LANE_NOTICE = undefined;
|
|
279
190
|
if (laneNotice && logger.info)
|
|
280
191
|
logger.info("exec_lane_selected", { lane: laneNotice.lane, source: laneNotice.source });
|
|
281
|
-
// design/158 N9 (polarity self-report): a `*_ENABLED` suffix does NOT carry its default — the repo grew four
|
|
282
|
-
// dialects for it (opt-in / opt-out / posture tri-state / negative name). Rather than force operators to read
|
|
283
|
-
// config.ts to learn what a knob does when unset, boolEnv-parsed knobs register themselves and we print one
|
|
284
|
-
// debug line per knob: name → polarity → effective value → where that value came from. `debug` is optional so
|
|
285
|
-
// an older embedder-supplied logger shape (warn-only) still compiles and still gets the warnings above.
|
|
286
|
-
// 复审 2026-07-29 #6 — the table covers a LOAD, not the catalog: five boolEnv calls sit inside conditional
|
|
287
|
-
// config shapes (E2B_ALLOW_NET only when set; K8S_INSECURE_TLS / DOCKER_DROP_CAPS only under their provider
|
|
288
|
-
// branch; MODEL_DEGRADE_REACTIVE / MODEL_DEGRADE_TO_VISION only when MODEL_DEGRADE_TO is set) and register
|
|
289
|
-
// only when that shape is active — absence of a row means "not a live knob in this config", not "off".
|
|
290
192
|
if (logger.debug) {
|
|
291
193
|
for (const k of configKnobTable()) {
|
|
292
194
|
logger.debug("config_knob_polarity", {
|
|
@@ -298,11 +200,6 @@ export function logConfigDiagnostics(logger) {
|
|
|
298
200
|
}
|
|
299
201
|
}
|
|
300
202
|
}
|
|
301
|
-
/** F8 (codex audit): first NON-EMPTY env among aliased names — "" = unset, the same convention `env()` uses.
|
|
302
|
-
* A plain `??` chain treats an EMPTY first choice as SET and shadows a configured fallback (e.g.
|
|
303
|
-
* `ANTHROPIC_BASEURL=""` from a compose `${VAR:-}` template hiding a real `ANTHROPIC_BASE_URL`, or an empty
|
|
304
|
-
* `ANTHROPIC_API_KEY` suppressing the whole route despite a valid `ANTHROPIC_AUTH_TOKEN`). Shared by the
|
|
305
|
-
* MODEL_PROVIDER inference AND the anthropic route assembly so the two predicates can never drift. */
|
|
306
203
|
function firstSetEnv(...names) {
|
|
307
204
|
for (const n of names) {
|
|
308
205
|
const v = process.env[n];
|
|
@@ -311,22 +208,6 @@ function firstSetEnv(...names) {
|
|
|
311
208
|
}
|
|
312
209
|
return undefined;
|
|
313
210
|
}
|
|
314
|
-
/**
|
|
315
|
-
* #210(黑板 [3425]③)—— **E 类**旋钮的唯一夹取口:`numEnv` 的坏值拒启 + 越界**夹取并点名**。
|
|
316
|
-
*
|
|
317
|
-
* 为什么抽一只而不是 24 处各写各的:改前这 24 处清一色是 `Math.max(floor, numEnv(...))`,夹取本身是
|
|
318
|
-
* 刻意的(存量部署配了越界值也不该拒启),但**夹取事件零留痕** —— operator 把 `DRAIN_GRACE_MS` 设成
|
|
319
|
-
* `100`(以为单位是秒)后,进程按 10s 排空、日志里一个字都没有,直到某次滚动升级把在飞任务砍了才回头
|
|
320
|
-
* 查。24 处手写 `CONFIG_WARNINGS.push` 必然漂(有人忘记带 `used`、有人写错 env 名),判据只有一份才咬得住。
|
|
321
|
-
*
|
|
322
|
-
* 返回纯数值 ⇒ 按仓规叫 `clamp*` 而非 `create*`。
|
|
323
|
-
*
|
|
324
|
-
* ⚠️ `max` 缺省 `+Infinity` 而**不是**一个大整数上帽(codex 自审 R2-F2 真修):这 22 处里只有
|
|
325
|
-
* APPROVAL_AUTO_BUDGET 原本真有 `Math.min` 上帽,其余全是「只有地板」。给它们塞一个有限上界等于凭空长出
|
|
326
|
-
* 一条上限 —— 一个拿超大有限值当「实际上无限」哨兵的存量部署会被无预告改小,而本批对外宣称的正是「夹取
|
|
327
|
-
* 行为逐字保留」。上界只写在原本就有上界的那一处。
|
|
328
|
-
* (`Infinity` 本身进不来:`numEnv` 的 `Number.isFinite` 门已经把 `1e400` 拒在启动期。)
|
|
329
|
-
*/
|
|
330
211
|
function clampEnvWithWarn(name, fallback, min, max = Number.POSITIVE_INFINITY) {
|
|
331
212
|
const n = numEnv(name, fallback);
|
|
332
213
|
const used = Math.min(max, Math.max(min, n));
|
|
@@ -334,49 +215,26 @@ function clampEnvWithWarn(name, fallback, min, max = Number.POSITIVE_INFINITY) {
|
|
|
334
215
|
CONFIG_WARNINGS.push({ env: name, raw: env(name, fallback), used: String(used), kind: "clamped" });
|
|
335
216
|
return used;
|
|
336
217
|
}
|
|
337
|
-
/**
|
|
338
|
-
* #210 —— `numEnv` 的「fallback 不是字符串而是一个**已算出来的数**」变体。用在 cheap-model 的
|
|
339
|
-
* 窗口/上限两键上:未设时继承**主模型**的对应值,而不是继承一个字面量默认。
|
|
340
|
-
*
|
|
341
|
-
* 改前是 `process.env.X ? Number(X) : main` —— 真值门本身没错(空串已经落在「继承主模型」那一侧,本函数
|
|
342
|
-
* 逐字保留),错的是给了值之后的裸 `Number()`:`MODEL_CHEAP_CONTEXT_WINDOW=128k` 会静默铸出 NaN 窗口,
|
|
343
|
-
* 而窗口是被拿去做大小比较的,NaN 让每一处比较恒假 ⇒ 窗口守卫整条消失。
|
|
344
|
-
*
|
|
345
|
-
* ⚠️ cheap 的**单价**四键不走这里,走 {@link cheapCostEnv}:它们改前是 `?? ` 形,空串会落 0 而不是继承,
|
|
346
|
-
* 那条既有语义必须逐字保留(见该函数头注)。两族的空串语义不同源,别合并。
|
|
347
|
-
*/
|
|
348
218
|
function numEnvOr(name, fallback) {
|
|
349
219
|
const raw = process.env[name];
|
|
350
220
|
if (raw === undefined || raw === "")
|
|
351
221
|
return fallback;
|
|
352
222
|
return parseNumOrFail(name, raw);
|
|
353
223
|
}
|
|
354
|
-
/**
|
|
355
|
-
* #210 —— cheap 单价四键**专用**的变体(codex 自审 R1-F2 真修)。
|
|
356
|
-
*
|
|
357
|
-
* 与 `numEnvOr` 只差空串一条,而这条差别是有理由的:这四键改前写的是 `Number(env ?? main)`,空串会走进
|
|
358
|
-
* `Number("")` = **0**,也就是「这条车道按零计价」——那是一个**已经在生效**的语义,不是坏值。把它归一
|
|
359
|
-
* 成「继承主模型单价」会让一台存量部署的 cheap 车道账面单价从 0 跳到 PRO 价,配额可能当场把在跑的任务
|
|
360
|
-
* 拒掉。律条治的是**坏值静默**,不是顺手改既有语义,所以行为逐字保留、改为响亮:空串照旧落 0,但
|
|
361
|
-
* CONFIG_WARNINGS 点名(它几乎总是 compose `${VAR:-}` 漏配的产物,而不是有人真想要免费计价)。
|
|
362
|
-
*/
|
|
363
224
|
function cheapCostEnv(name, mainPrice) {
|
|
364
225
|
const raw = process.env[name];
|
|
365
226
|
if (raw === undefined)
|
|
366
227
|
return mainPrice;
|
|
367
228
|
if (raw === "") {
|
|
368
229
|
CONFIG_WARNINGS.push({ env: name, raw: "", used: "0", kind: "empty-preserved" });
|
|
369
|
-
return 0;
|
|
230
|
+
return 0;
|
|
370
231
|
}
|
|
371
232
|
return parseNumOrFail(name, raw);
|
|
372
233
|
}
|
|
373
|
-
/** #210 —— 「真值门 + 裸 `Number()`」那一族(执行车道八旋钮)的收口:门照旧(未设/空串 ⇒ 键缺席,
|
|
374
|
-
* 与改前逐字相同),但**给了值**就必须是数,否则 boot 拒。NaN 超时进 adapter = 守卫整条消失,而车道
|
|
375
|
-
* 旋钮在安全轴上(CLAUDE.md「执行车道禁静默 fail-open」)。 */
|
|
376
234
|
function optNumEnvStrict(name) {
|
|
377
235
|
const raw = process.env[name];
|
|
378
236
|
if (!raw)
|
|
379
|
-
return undefined;
|
|
237
|
+
return undefined;
|
|
380
238
|
return parseNumOrFail(name, raw);
|
|
381
239
|
}
|
|
382
240
|
function optFinitePositiveEnv(name) {
|
|
@@ -385,27 +243,18 @@ function optFinitePositiveEnv(name) {
|
|
|
385
243
|
return undefined;
|
|
386
244
|
const n = Number(raw);
|
|
387
245
|
if (!(Number.isFinite(n) && n > 0)) {
|
|
388
|
-
CONFIG_WARNINGS.push({ env: name, raw });
|
|
246
|
+
CONFIG_WARNINGS.push({ env: name, raw });
|
|
389
247
|
return undefined;
|
|
390
248
|
}
|
|
391
249
|
return n;
|
|
392
250
|
}
|
|
393
|
-
/** Keyed by canonical name so a knob read twice in one load (e.g. DURABLE_APPROVAL, once for the D-G boot
|
|
394
|
-
* invariant and once for the returned field) registers one row, not two. Reset per `loadConfig()`. */
|
|
395
251
|
const CONFIG_KNOBS = new Map();
|
|
396
|
-
/** The registered boolean knobs, sorted by name — the data behind `logConfigDiagnostics`'s polarity table. */
|
|
397
252
|
export function configKnobTable() {
|
|
398
253
|
return [...CONFIG_KNOBS.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
399
254
|
}
|
|
400
255
|
function registerKnob(rec) {
|
|
401
256
|
CONFIG_KNOBS.set(rec.name, rec);
|
|
402
257
|
}
|
|
403
|
-
/** #241-rc2([3740]③→[3742] 裁定,双臂不留静默):布尔 env 的**闭集词表**解析。
|
|
404
|
-
* 放宽臂=收 true/false/1/0/yes/no/on/off(大小写不敏感)——`=1` 是运维直觉写法,意图明确按意图执行
|
|
405
|
-
* (operator-knob 宪法方向);拒启臂=未知词**抛错**(boot 拒启带词表指路,照 #186 响亮拒先例)——
|
|
406
|
-
* 行为承重键(DURABLE_APPROVAL 决定 checkpointStore 存在性 ⇒ /v1/approvals 路由存在性)静默回默认的
|
|
407
|
-
* 真实后果是「审批面整条静默缺席」([3740]② cli/test 真机链)。返回 undefined = raw 未设/空串(沿用
|
|
408
|
-
* `env()` 的空串=未设约定),由调用方走默认/legacy 分派。 */
|
|
409
258
|
const BOOL_TRUE_WORDS = new Set(["true", "1", "yes", "on"]);
|
|
410
259
|
const BOOL_FALSE_WORDS = new Set(["false", "0", "no", "off"]);
|
|
411
260
|
function parseBoolWord(name, raw) {
|
|
@@ -421,47 +270,23 @@ function parseBoolWord(name, raw) {
|
|
|
421
270
|
`decides whether the checkpoint store — and with it /v1/approvals — exists at all).`);
|
|
422
271
|
}
|
|
423
272
|
function boolEnv(name, def) {
|
|
424
|
-
// #241-rc2:词表解析(见 parseBoolWord 顶注)。未知词在 parseBoolWord 里抛 = boot 拒启——旧形的
|
|
425
|
-
// 「CONFIG_WARNINGS 一条 warn + 回默认」臂就此退役(懒读族的防重复 push 守卫随臂一起退役:未知词
|
|
426
|
-
// 现在到不了任何 push)。
|
|
427
273
|
const parsed = parseBoolWord(name, process.env[name]);
|
|
428
274
|
const value = parsed ?? def;
|
|
429
275
|
const source = parsed !== undefined ? "env" : "default";
|
|
430
276
|
registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source });
|
|
431
277
|
return value;
|
|
432
278
|
}
|
|
433
|
-
/** design/158 N8 (D-family) + server 3.0.0 墓碑 —— a knob whose CANONICAL name is positive (`X_ENABLED`,
|
|
434
|
-
* default ON) and whose retired negative name (`X_DISABLED`) is now a **fail-loud tombstone**.
|
|
435
|
-
*
|
|
436
|
-
* Why the flip at all: `X_DISABLED !== "true"` is a double negative at the read site, so the four knobs that
|
|
437
|
-
* used it (`PROJECT_MEMORY`, `CONFIG_LKG`, `HOST_BG`, `HOST_EXEC_SPOOL`) could not be read for their default
|
|
438
|
-
* without inverting twice in your head, and `projectMemoryDisabled` was the ONE negative boolean on a
|
|
439
|
-
* `ServiceConfig` that otherwise carries 16 positive `*Enabled`/`*Allowed` fields.
|
|
440
|
-
*
|
|
441
|
-
* 🔴 3.0.0([1989],clay 令「不做兼容」):旧名不再有兼容窗——**设了就拒启**。这个函数保留 legacy 形参
|
|
442
|
-
* 不是为了继续读它,而正是为了**认出它在场并拒绝启动**(把墓碑放在唯一知道这对名字关系的地方)。
|
|
443
|
-
*
|
|
444
|
-
* Resolution order:
|
|
445
|
-
* 1. `X_ENABLED` set to a literal ⇒ that.
|
|
446
|
-
* 2. `X_ENABLED` set to a non-literal ⇒ default + warning (the tombstone is NOT consulted — a typo in the
|
|
447
|
-
* new name must not turn into a boot refusal about an env the operator never set).
|
|
448
|
-
* 3. `X_ENABLED` unset ∧ `X_DISABLED` set(**任何值**,含 `"false"`)⇒ throw,文案指路新名。选拒启不选
|
|
449
|
-
* 静默忽略:静默会让升级部署的旧开关名义在、实际归默认——比拒启危险,且下游冒烟测试测不出来。
|
|
450
|
-
* 4. neither ⇒ default. */
|
|
451
279
|
export function boolEnvWithLegacyNegated(name, legacyNegatedName, def) {
|
|
452
280
|
const raw = process.env[name];
|
|
453
281
|
const legacy = process.env[legacyNegatedName];
|
|
454
282
|
let value;
|
|
455
283
|
let source;
|
|
456
|
-
const parsedCanonical = parseBoolWord(name, raw);
|
|
284
|
+
const parsedCanonical = parseBoolWord(name, raw);
|
|
457
285
|
if (parsedCanonical !== undefined) {
|
|
458
286
|
value = parsedCanonical;
|
|
459
287
|
source = "env";
|
|
460
288
|
}
|
|
461
289
|
else if (legacy !== undefined && legacy !== "") {
|
|
462
|
-
// server 3.0.0([1989],clay 令「不做兼容」):负名旧 env = fail-loud 墓碑。设了(任何值,含
|
|
463
|
-
// "false"——设它的人同样以为它还生效)即拒启并指路新名。选拒启不选静默忽略:静默会让升级部署的
|
|
464
|
-
// 旧开关名义在、实际归默认——比拒启危险,而且下游/运维的冒烟测试测不出来。
|
|
465
290
|
throw new Error(`${legacyNegatedName} was removed in server 3.0.0 — set ${name}=${legacy !== "true"} instead ` +
|
|
466
291
|
`(positive-name knob, default ${def ? "on" : "off"}; migration table in USAGE.md)`);
|
|
467
292
|
}
|
|
@@ -472,20 +297,9 @@ export function boolEnvWithLegacyNegated(name, legacyNegatedName, def) {
|
|
|
472
297
|
registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source });
|
|
473
298
|
return value;
|
|
474
299
|
}
|
|
475
|
-
/** LKG (last-known-good) persistence of the remote effective-config fetch. Read LAZILY at both main.ts sites
|
|
476
|
-
* rather than lifted onto `ServiceConfig`, so this stays a pure env knob; `loadConfig` pre-registers it (below)
|
|
477
|
-
* so its polarity row and any deprecation notice appear at boot with everything else. */
|
|
478
300
|
export const configLkgEnabled = () => boolEnvWithLegacyNegated("CONFIG_LKG_ENABLED", "CONFIG_LKG_DISABLED", true);
|
|
479
|
-
/** The `host` exec lane's background-shell capability kill switch (read per capability probe in remote-env-host.ts). */
|
|
480
301
|
export const hostBackgroundShellEnabled = () => boolEnvWithLegacyNegated("HOST_BG_ENABLED", "HOST_BG_DISABLED", true);
|
|
481
|
-
/** The `host` exec lane's spool-file stdio form; false ⇒ the pre-1.226 pipe fallback (read per exec). */
|
|
482
302
|
export const hostExecSpoolEnabled = () => boolEnvWithLegacyNegated("HOST_EXEC_SPOOL_ENABLED", "HOST_EXEC_SPOOL_DISABLED", true);
|
|
483
|
-
/** Enum env with validation: a typo (e.g. SESSION_BACKEND misspelled as `Tidb`) FAILS at startup instead of being silently
|
|
484
|
-
* cast to the union type and degrading to the default backend (dropping the persistence the operator asked for). */
|
|
485
|
-
/** #246:READ_DENY_PATTERNS 的 env 形=JSON 数组,元素为 `"pattern"` 裸字符串简写或 {pattern, caseSensitive?}
|
|
486
|
-
* (与 core `ReadDenyEntry = string | {…}` 词表同宽——合并码扫 C6:USAGE 承诺了简写而 parser 只收对象形,
|
|
487
|
-
* 批内自相矛盾)。boot 即用 core 的 `compileReadDeny` 预验(引擎同一套规则,宿主不自造第二份校验语义)
|
|
488
|
-
* ——JSON 坏形/编不过一律拒启(响亮:安全面追加表静默折默认=静默丢保护,#157 禁令方向)。 */
|
|
489
303
|
function parseReadDenyPatternsEnv(raw) {
|
|
490
304
|
let arr;
|
|
491
305
|
try {
|
|
@@ -498,7 +312,7 @@ function parseReadDenyPatternsEnv(raw) {
|
|
|
498
312
|
throw new Error('READ_DENY_PATTERNS must be a JSON array of "pattern" strings or {pattern, caseSensitive?} objects');
|
|
499
313
|
const entries = arr.map((it, i) => {
|
|
500
314
|
if (typeof it === "string")
|
|
501
|
-
return { pattern: it, caseSensitive: false };
|
|
315
|
+
return { pattern: it, caseSensitive: false };
|
|
502
316
|
if (typeof it !== "object" || it === null || typeof it.pattern !== "string") {
|
|
503
317
|
throw new Error(`READ_DENY_PATTERNS[${i}] must be a string or an object with a string 'pattern'`);
|
|
504
318
|
}
|
|
@@ -507,21 +321,9 @@ function parseReadDenyPatternsEnv(raw) {
|
|
|
507
321
|
throw new Error(`READ_DENY_PATTERNS[${i}].caseSensitive must be boolean when present`);
|
|
508
322
|
return { pattern: it.pattern, caseSensitive: cs === true };
|
|
509
323
|
});
|
|
510
|
-
compileReadDeny(entries);
|
|
324
|
+
compileReadDeny(entries);
|
|
511
325
|
return entries;
|
|
512
326
|
}
|
|
513
|
-
/**
|
|
514
|
-
* #253 件 G5(core 5.33.0 #245,clay 裁定件)—— 内建 deny 表管理面两键的 env 形:**逗号分隔的名字表**。
|
|
515
|
-
*
|
|
516
|
-
* 形状选择的理由:两个值域都是**短的、闭集或表内的名字**(四个档名 / 内建表的行名),JSON 数组在这里
|
|
517
|
-
* 只会让运维多写一层引号;`READ_DENY_PATTERNS` 用 JSON 是因为它的元素是**带可选字段的对象**。
|
|
518
|
-
*
|
|
519
|
-
* 🔴 **空串拒启,不折成 `[]`**:core 把 `tiers: []` 定义成一个合法姿态(「一档内建都不要」),但一个
|
|
520
|
-
* **空 env**(`READ_DENY_BUILTIN_TIERS=` / 变量没展开)在运维现场几乎总是手滑,而它的后果是**整张内建
|
|
521
|
-
* 敏感路径表被关掉** —— 让手滑与显式意图长得一样,正是 #157「安全轴上禁静默 fail-open」要禁的形。
|
|
522
|
-
* 要那个姿态就显式写 `none`(本仓的哨兵词,不是 core 词表的一员 —— 它在解析层就被翻成 `[]`)。
|
|
523
|
-
* exclude 侧没有对应的哨兵:一个「什么都不删」的 exclude 就是**不设这个 env**。
|
|
524
|
-
*/
|
|
525
327
|
function parseNameListEnv(name, raw, opts) {
|
|
526
328
|
const parts = raw.split(",").map((s) => s.trim());
|
|
527
329
|
if (opts?.emptySentinel !== undefined && parts.length === 1 && parts[0] === opts.emptySentinel)
|
|
@@ -534,15 +336,7 @@ function parseNameListEnv(name, raw, opts) {
|
|
|
534
336
|
}
|
|
535
337
|
return names;
|
|
536
338
|
}
|
|
537
|
-
/**
|
|
538
|
-
* #253 件 G5 —— 两键的**引擎规则预验**:交给 core 自己的 `resolveReadDenyBuiltins`(闭集档名 + 表内行名
|
|
539
|
-
* 两类坏值它都响亮抛),宿主**不自造第二份判定语义**(与 `READ_DENY_PATTERNS` 走 `compileReadDeny` 的
|
|
540
|
-
* 姿势逐字同款)。抛出的文案前面缀上出问题的 env 名 —— core 的原文点名的是那个非法值,运维还需要知道
|
|
541
|
-
* 是哪一个变量写错了。
|
|
542
|
-
*/
|
|
543
339
|
function assertReadDenyBuiltinsResolvable(tiers, exclude) {
|
|
544
|
-
// 分两次校验(而不是一次喂两键):core 的文案点名的是**那个非法值**,而运维还需要知道是**哪个变量**
|
|
545
|
-
// 写错了 —— 一次喂两键就只能在文案里同时挂两个名字,把排查面又糊回去。
|
|
546
340
|
const check = (which, config) => {
|
|
547
341
|
try {
|
|
548
342
|
resolveReadDenyBuiltins(config);
|
|
@@ -562,39 +356,24 @@ function enumEnv(name, fallback, allowed) {
|
|
|
562
356
|
throw new Error(`env ${name}="${v}" must be one of: ${allowed.join(", ")}`);
|
|
563
357
|
return v;
|
|
564
358
|
}
|
|
565
|
-
/** A single HTTP header-name env: it is both read as a request header AND interpolated into the CORS
|
|
566
|
-
* `access-control-allow-headers` list, so a value with a comma/space would split that list (and isn't a
|
|
567
|
-
* valid header name anyway). Reject anything but header-token chars at startup (council). */
|
|
568
359
|
function headerNameEnv(name, fallback) {
|
|
569
360
|
const v = env(name, fallback);
|
|
570
361
|
if (!/^[A-Za-z0-9_-]+$/.test(v))
|
|
571
362
|
throw new Error(`env ${name}="${v}" must be a single header name ([A-Za-z0-9_-]+)`);
|
|
572
363
|
return v;
|
|
573
364
|
}
|
|
574
|
-
/**
|
|
575
|
-
* Accept an SSH private key in whatever single-line form a secret store can hold. A PEM key is multi-line, but
|
|
576
|
-
* sema-registry's `POST /api/config/secrets` rejects newlines — so let the value arrive base64-encoded (the
|
|
577
|
-
* recommended self-serve form) or with escaped `\n`, and reconstruct the real PEM here for ssh2.
|
|
578
|
-
*/
|
|
579
365
|
function normalizeSshPrivateKey(raw) {
|
|
580
366
|
const v = raw.trim();
|
|
581
367
|
if (v.includes("BEGIN ") && v.includes("PRIVATE KEY")) {
|
|
582
|
-
return v.includes("\n") ? raw : v.replace(/\\n/g, "\n");
|
|
368
|
+
return v.includes("\n") ? raw : v.replace(/\\n/g, "\n");
|
|
583
369
|
}
|
|
584
370
|
try {
|
|
585
|
-
return Buffer.from(v, "base64").toString("utf8");
|
|
371
|
+
return Buffer.from(v, "base64").toString("utf8");
|
|
586
372
|
}
|
|
587
373
|
catch {
|
|
588
374
|
return raw;
|
|
589
375
|
}
|
|
590
376
|
}
|
|
591
|
-
/**
|
|
592
|
-
* Build `Model.extraBody` (core 1.60 generation-tuning passthrough) from env, or undefined when nothing
|
|
593
|
-
* is set (so a deployment that sets none is byte-for-byte unaffected). MODEL_EXTRA_BODY (raw JSON) is the
|
|
594
|
-
* general escape hatch; MODEL_FREQUENCY_PENALTY / MODEL_PRESENCE_PENALTY are typed conveniences merged on
|
|
595
|
-
* top (they win over the same key in MODEL_EXTRA_BODY). Throws on malformed JSON — fail loud at boot, not
|
|
596
|
-
* a silently-ignored tuning knob.
|
|
597
|
-
*/
|
|
598
377
|
function buildExtraBody() {
|
|
599
378
|
const body = {};
|
|
600
379
|
const raw = process.env.MODEL_EXTRA_BODY;
|
|
@@ -611,9 +390,6 @@ function buildExtraBody() {
|
|
|
611
390
|
}
|
|
612
391
|
Object.assign(body, parsed);
|
|
613
392
|
}
|
|
614
|
-
// #210 A 档:与本函数自己的纪律对齐(坏 JSON 已经是 boot throw)—— 裸 `Number()` 的 NaN 会原样进
|
|
615
|
-
// `extraBody`,`JSON.stringify` 把它写成 `null`,网关要么 400 要么按缺省跑,两种都不是 operator 配的
|
|
616
|
-
// 那个值,而且哪一种都不会说出来。
|
|
617
393
|
const fp = process.env.MODEL_FREQUENCY_PENALTY;
|
|
618
394
|
const pp = process.env.MODEL_PRESENCE_PENALTY;
|
|
619
395
|
if (fp !== undefined && fp !== "")
|
|
@@ -622,24 +398,10 @@ function buildExtraBody() {
|
|
|
622
398
|
body.presence_penalty = parseNumOrFail("MODEL_PRESENCE_PENALTY", pp);
|
|
623
399
|
return Object.keys(body).length > 0 ? body : undefined;
|
|
624
400
|
}
|
|
625
|
-
/** A-065 P2-6:967000 的适用面是 **model-id 表**,不是窗宽几何——core `context-edit.d.ts` 原文:CC 的
|
|
626
|
-
* 1M autocompact 特判是 "sonnet-5-only … a per-model config delivery, **not geometry**"。判 sonnet-5 系
|
|
627
|
-
* 容忍两种部署 id 形:日期后缀(claude-sonnet-5-20260101)与网关路由前缀(anthropic/claude-sonnet-5)。 */
|
|
628
401
|
function isSonnet5Family(id) {
|
|
629
402
|
const bare = id.slice(id.lastIndexOf("/") + 1);
|
|
630
403
|
return bare === "claude-sonnet-5" || bare.startsWith("claude-sonnet-5-");
|
|
631
404
|
}
|
|
632
|
-
/** [818]① 1M dual-window (core 1.289 `Model.autoCompactTokens`): an EXPLICIT positive value wins (env
|
|
633
|
-
* `MODEL_AUTO_COMPACT_TOKENS` / center roster `autoCompactTokens`); else the **model-id table** applies —
|
|
634
|
-
* sonnet-5-family id ∧ contextWindow>=1e6 ⇒ 967000 (the faithful shape of CC's single-entry per-model
|
|
635
|
-
* config; a small-window sonnet is NOT derived — a field above the physical window is undefined core
|
|
636
|
-
* behavior); anything else ⇒ REMOVED (a spread-inherited value from a bigger-window base model would
|
|
637
|
-
* mis-trigger a small window). ⚠️ A-065 P2-6 rescinded the old `contextWindow>=1e6 ⇒ 967000` derivation:
|
|
638
|
-
* it handed a sonnet-5-only constant to ANY 1M model with no corpus/upstream backing (the earlier note
|
|
639
|
-
* here claiming this was "the exact CC alignment core recommends embedders set" mis-read core's per-model
|
|
640
|
-
* advice as a width rule). Returns a discriminant so the env lane can turn the removed implicit value into
|
|
641
|
-
* a loud CONFIG_NOTICES line ("removed-1m"); the center lane's roster culture is explicit declaration.
|
|
642
|
-
* Shared by the env lane (main + cheap models) and the center lane (sema-registry toModel). */
|
|
643
405
|
export function applyAutoCompactWindow(m, explicit) {
|
|
644
406
|
const target = m;
|
|
645
407
|
if (explicit !== undefined && Number.isFinite(explicit) && explicit > 0) {
|
|
@@ -654,35 +416,13 @@ export function applyAutoCompactWindow(m, explicit) {
|
|
|
654
416
|
delete target.autoCompactTokens;
|
|
655
417
|
return was1m ? "removed-1m" : "removed";
|
|
656
418
|
}
|
|
657
|
-
/** 域:store(持久化)—— DB 引擎三态、session 后端、SQL coords、快照 BLOB / SendUserFile 对象存储。 */
|
|
658
419
|
function parseStoreDomain(ctx) {
|
|
659
|
-
const { requirePrincipal } = ctx;
|
|
660
|
-
// `local` (clay 2026-06-25 seamless local↔cloud) = the DB-less in-memory/file StoreBackend, so a locally-run HTTP
|
|
661
|
-
// service serves the same contract a cloud worker does. It has no SQL host/coords (createStoreBackend builds it
|
|
662
|
-
// without tidb/pg), so it's always "reachable". (Parsed FIRST so the session/memory posture defaults below can
|
|
663
|
-
// key off the chosen engine.) Wording (clay 2026-07-06): canonical value `mysql` = any MySQL-protocol server
|
|
664
|
-
// (MySQL / TiDB / MariaDB). 🔴 A-054.6: this comment used to end "`tidb` accepted as a back-compat alias" — that
|
|
665
|
-
// has been FALSE since [2354] retired the public name (see the enum 19 lines below: `tidb` is not in the closed
|
|
666
|
-
// set, so passing it as this variable's value fails boot). The internal "tidb" label survives only in the sessionBackend mapping and
|
|
667
|
-
// the SQL coords, which are NOT the public env vocabulary. TiDB itself stays fully supported and is the recommended
|
|
668
|
-
// MySQL-protocol engine when semantic memory is on (native VECTOR(dim) recall) — you select it with `mysql`.
|
|
669
|
-
// clay 拍(2026-07-27,[1845] 桌面撞 501 案后):**裸 boot 默认 durable(local file 店)**。
|
|
670
|
-
// 旧默认「纯内存」让每个宿主(cli fixture/桌面/web BFF)都要手工记得 DB_BACKEND=local,漏设 =
|
|
671
|
-
// 「跑完的任务重启就丢 + durable runs 面 501」——单机是主流形,直觉预期是留得住。三态:
|
|
672
|
-
// - 未设 ⇒ local(单用户形;多租户例外见下)。默认推导的 local 在 main.ts 侧 mkdir/ensure 失败
|
|
673
|
-
// 时**降级 memory + warn + gauge**(auto-DB 不可达先例),显式 DB_BACKEND=local 才 fail-loud。
|
|
674
|
-
// - DB_BACKEND=memory ⇒ 显式纯内存 opt-out(CI/临时 server「不留盘上残余」的出口)。
|
|
675
|
-
// - 多租户裸 boot(REQUIRE_PRINCIPAL=true 且未设)⇒ 保持 memory + notice:local 与多租户互斥
|
|
676
|
-
// (main.ts 硬拒,owner map 无租户隔离),默认不得把多租户裸 boot 变成拒启。
|
|
420
|
+
const { requirePrincipal } = ctx;
|
|
677
421
|
const dbBackendSet = !!process.env.DB_BACKEND;
|
|
678
|
-
// 「裸 boot」判定必须**无任何 SQL 信号**才成立——SESSION_BACKEND=mysql/auto 或 TIDB/MYSQL/PG coords
|
|
679
|
-
// 在场时,dbBackend 的旧默认 "mysql" 是**引擎选择器**(auto 探测/coords 构建都 key 它),默认切
|
|
680
|
-
// local 会把这些既有形错切到文件店(回归钉在 config-coverage)。
|
|
681
422
|
const sqlSignal = (!!process.env.SESSION_BACKEND && process.env.SESSION_BACKEND !== "memory") ||
|
|
682
423
|
!!(process.env.MYSQL_HOST || process.env.PG_HOST);
|
|
683
424
|
const bareBoot = !dbBackendSet && !sqlSignal;
|
|
684
425
|
const multiTenantBareBoot = bareBoot && requirePrincipal;
|
|
685
|
-
// [2354]:公名枚举不再收 "tidb"(enumEnv 对越集值自带拒启);内部逻辑不变。
|
|
686
426
|
const dbBackend = enumEnv("DB_BACKEND", bareBoot ? (multiTenantBareBoot ? "memory" : "local") : "mysql", ["mysql", "pg", "local", "memory"]);
|
|
687
427
|
if (multiTenantBareBoot) {
|
|
688
428
|
CONFIG_NOTICES.push({
|
|
@@ -692,74 +432,42 @@ function parseStoreDomain(ctx) {
|
|
|
692
432
|
},
|
|
693
433
|
});
|
|
694
434
|
}
|
|
695
|
-
// Asymmetry收编 (clay 2026-07-06, overseas-pilot pit #1): an EXPLICIT `DB_BACKEND=mysql/pg` is the operator
|
|
696
|
-
// asking for a durable SQL deployment — default sessions onto it (fail-fast, same as an explicit
|
|
697
|
-
// SESSION_BACKEND=mysql) instead of silently keeping the in-memory default ("DB_BACKEND=pg engaged nothing,
|
|
698
|
-
// session:memory, PG 0 tables"). DB_BACKEND=local keeps its own coercion in main.ts (it needs the built
|
|
699
|
-
// backend); an UNSET DB_BACKEND keeps the memory default (no surprise DB requirement on a bare boot).
|
|
700
|
-
// `mysql` is accepted as an alias of the internal durable label `tidb` (see ServiceConfig.sessionBackend).
|
|
701
435
|
const sqlEngineExplicit = !!process.env.DB_BACKEND && dbBackend !== "local" && dbBackend !== "memory";
|
|
702
|
-
const sessionBackendRaw = enumEnv("SESSION_BACKEND", sqlEngineExplicit ? "mysql" : "memory", ["memory", "mysql", "auto"]);
|
|
436
|
+
const sessionBackendRaw = enumEnv("SESSION_BACKEND", sqlEngineExplicit ? "mysql" : "memory", ["memory", "mysql", "auto"]);
|
|
703
437
|
const sessionBackend = sessionBackendRaw === "mysql" ? "tidb" : sessionBackendRaw;
|
|
704
|
-
// P0.5 variant-2: the file-backed `local` backend's data root. Same tail as main.ts's `localRoot` and
|
|
705
|
-
// run-local.ts's `root` (CONFIG_LOCAL_DIR → AGENT_DATA_DIR → ~/.ai-agent), so the HTTP service + a run-local
|
|
706
|
-
// on one box open ONE boot-locked data dir. `LOCAL_DATA_ROOT` is an additional highest-priority override
|
|
707
|
-
// (the explicit "the local backend's data lives HERE" knob). Only the `local` backend consumes it
|
|
708
|
-
// (createStoreBackend), but it's always resolved (cheap, env-deterministic).
|
|
709
|
-
// ⚠️ 缝合审 M6:上面那句「same resolution」**只在没设 LOCAL_DATA_ROOT 时成立** —— 旧注释断言了无条件
|
|
710
|
-
// 相同,那是假的。设了它就只有数据根跟着走,config.d/remote-exec.json 仍从配置根读 ⇒ 两根分家。
|
|
711
|
-
// 不改解析(改了会让现有部署的 config.d 当场失踪),改成分家时 boot 期出声:见 `splitLocalRoots`。
|
|
712
438
|
const localDataRoot = process.env.LOCAL_DATA_ROOT || process.env.CONFIG_LOCAL_DIR || process.env.AGENT_DATA_DIR || join(homedir(), ".ai-agent");
|
|
713
|
-
// `auto` builds the DB coords only when a host is actually configured — otherwise it stays memory with no
|
|
714
|
-
// probe. (Explicit mysql always builds them so a missing host fails loudly.) `needsDb` is engine-agnostic;
|
|
715
|
-
// the connection coords are built for whichever engine `dbBackend` selects (MYSQL_* preferred, TIDB_* alias).
|
|
716
439
|
const dbHostSet = dbBackend === "pg" ? !!process.env.PG_HOST : !!process.env.MYSQL_HOST;
|
|
717
440
|
const needsTidb = sessionBackend === "tidb" || (sessionBackend === "auto" && dbHostSet);
|
|
718
441
|
const needsDb = needsTidb;
|
|
719
442
|
return {
|
|
720
443
|
sessionBackend,
|
|
721
|
-
sessionCacheTtlSec: numEnv("SESSION_CACHE_TTL_SEC", "300"),
|
|
722
|
-
// B1(/health 活体 store 探测):后台探针间隔;0=显式关,负值/非数响亮拒(fail-loud 族)。
|
|
723
|
-
// 只在 SQL 后端接线(main.ts 门 backend.kind!=="local"),local/memory 部署恒不挂环。
|
|
444
|
+
sessionCacheTtlSec: numEnv("SESSION_CACHE_TTL_SEC", "300"),
|
|
724
445
|
storeProbeIntervalMs: numEnvBounded("STORE_PROBE_INTERVAL_MS", "15000", 0, 3_600_000),
|
|
725
|
-
rewindSnapshotMaxMb: optFinitePositiveEnv("REWIND_SNAPSHOT_MAX_MB"),
|
|
446
|
+
rewindSnapshotMaxMb: optFinitePositiveEnv("REWIND_SNAPSHOT_MAX_MB"),
|
|
726
447
|
dbBackend,
|
|
727
448
|
dbBackendExplicit: dbBackendSet,
|
|
728
449
|
localDataRoot,
|
|
729
450
|
tidb: needsDb && dbBackend === "mysql"
|
|
730
451
|
? {
|
|
731
452
|
host: env("MYSQL_HOST"),
|
|
732
|
-
// #210 A 档:裸 Number ⇒ NaN 端口直进 driver(行为因驱动而异,多数是「连不上」而不是「说清
|
|
733
|
-
// 为什么」)。区间 [1,65535] 一并钉死 —— 端口的合法域是全域公理,不是本仓口味。
|
|
734
453
|
port: numEnvBounded("MYSQL_PORT", "3306", 1, 65535),
|
|
735
454
|
user: env("MYSQL_USER"),
|
|
736
455
|
password: env("MYSQL_PASSWORD", ""),
|
|
737
456
|
database: env("MYSQL_DATABASE"),
|
|
738
|
-
// 鲁棒性批5 A2(2026-08-05):此前裸 `Number(process.env.MYSQL_POOL_SIZE)` ——一个手滑值(如
|
|
739
|
-
// "20;drop") → NaN,driver 的 `connectionLimit` 直接拿到 NaN 而不是走池大小默认,行为因驱动
|
|
740
|
-
// 而异(未必 fail-loud)。optFinitePositiveEnv 与 dbQueryTimeoutMs 同款:非法值回默认(undefined
|
|
741
|
-
// = driver 默认池大小)+ S20 boot 警告,合法值照常生效。
|
|
742
457
|
connectionLimit: optFinitePositiveEnv("MYSQL_POOL_SIZE"),
|
|
743
458
|
}
|
|
744
459
|
: undefined,
|
|
745
460
|
pg: needsDb && dbBackend === "pg"
|
|
746
461
|
? {
|
|
747
462
|
host: env("PG_HOST"),
|
|
748
|
-
port: numEnvBounded("PG_PORT", "5432", 1, 65535),
|
|
463
|
+
port: numEnvBounded("PG_PORT", "5432", 1, 65535),
|
|
749
464
|
user: env("PG_USER"),
|
|
750
465
|
password: env("PG_PASSWORD", ""),
|
|
751
466
|
database: env("PG_DATABASE"),
|
|
752
|
-
// 同 MYSQL_POOL_SIZE 族(鲁棒性批5 A2)。
|
|
753
467
|
connectionLimit: optFinitePositiveEnv("PG_POOL_SIZE"),
|
|
754
468
|
}
|
|
755
469
|
: undefined,
|
|
756
|
-
// S9 deeper fix: per-query DB timeout (see the interface doc). Soft knob — a typo degrades to the default
|
|
757
|
-
// posture (pool-wide off / counter-family 30s) with an S20 boot warning, never NaN into a driver timer.
|
|
758
470
|
dbQueryTimeoutMs: optFinitePositiveEnv("DB_QUERY_TIMEOUT_MS"),
|
|
759
|
-
// E19/2c snapshot-BLOB object-store offload (clay 2026-06-26). Reuses the SAME adapter-held MinIO creds as the k8s
|
|
760
|
-
// workspace-snapshot lane (secret minio-agent-worker); all three keys required to enable, else the file-snapshot
|
|
761
|
-
// stores keep the SQL `snapshot_blob` default (byte-identical to today). Bucket = SESSION_SNAPSHOT_BUCKET (default
|
|
762
|
-
// "session-snapshots"); keyPrefix optional (default "blobs/" in MinioBlobBackend).
|
|
763
471
|
snapshotBlobStore: process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
764
472
|
? {
|
|
765
473
|
endpoint: process.env.MINIO_ENDPOINT,
|
|
@@ -768,17 +476,11 @@ function parseStoreDomain(ctx) {
|
|
|
768
476
|
secretKey: process.env.MINIO_SECRET_KEY,
|
|
769
477
|
...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
|
|
770
478
|
...(process.env.SESSION_SNAPSHOT_PREFIX ? { keyPrefix: process.env.SESSION_SNAPSHOT_PREFIX } : {}),
|
|
771
|
-
// Fail-SAFE parse (finding 20): a non-numeric/non-positive value → omit (the MinioBlobBackend default 3600s
|
|
772
|
-
// applies), never NaN (which would sign every URL with X-Amz-Expires=NaN → total snapshot outage).
|
|
773
479
|
...((ttl) => (ttl !== undefined ? { presignTtlSec: ttl } : {}))(optFinitePositiveEnv("SESSION_SNAPSHOT_TTL_SEC")),
|
|
774
480
|
}
|
|
775
481
|
: undefined,
|
|
776
|
-
// (a+c)(clay 2026-07-27)snapshot_blob 包墙修复两旋钮——语义见 ServiceConfig 字段注释。
|
|
777
482
|
snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
|
|
778
483
|
snapshotBlobAllowSql: boolEnv("SNAPSHOT_BLOB_ALLOW_SQL_BYTES", false),
|
|
779
|
-
// SendUserFile 双轨公网直链(同三键开闸;S3_ENDPOINT 兼容位=外接 S3 场景内网/公网同址)。
|
|
780
|
-
// `||` 非 `??`(配置面自查 2026-07-14):env 惯例(env() helper 同款)空串=未设——`?? ` 会让
|
|
781
|
-
// 一个显式置空的 MINIO_ENDPOINT/S3_PUBLIC_ENDPOINT 把后备名吞掉(compose `${X:-}` 透传空串是常态)。
|
|
782
484
|
sendUserFile: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT) && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
783
485
|
? {
|
|
784
486
|
endpoint: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT),
|
|
@@ -792,42 +494,21 @@ function parseStoreDomain(ctx) {
|
|
|
792
494
|
accessKey: process.env.MINIO_ACCESS_KEY,
|
|
793
495
|
secretKey: process.env.MINIO_SECRET_KEY,
|
|
794
496
|
...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
|
|
795
|
-
// fail-SAFE:非法值回落 0(永久轨缺省),不 NaN 进签名。
|
|
796
497
|
defaultTtlSec: ((v) => (v !== undefined && Number.isInteger(v) && v >= 0 ? v : 0))(process.env.SEND_USER_FILE_URL_TTL !== undefined ? Number(process.env.SEND_USER_FILE_URL_TTL) : undefined),
|
|
797
498
|
}
|
|
798
499
|
: undefined,
|
|
799
500
|
};
|
|
800
501
|
}
|
|
801
|
-
/** 域:model(模型面)—— 网关坐标、Anthropic 路线、韧性旋钮、主/廉价 model entry、role 表、降级梯。 */
|
|
802
502
|
function parseModelDomain() {
|
|
803
|
-
// npm 卫生纪律:默认值不烤内网坐标 — localhost 占位(公开分发正确缺省);真网关一律显式 env。
|
|
804
503
|
const gatewayBaseUrl = env("MODEL_GATEWAY_BASEURL", "http://127.0.0.1:8000/v1");
|
|
805
|
-
// server 3.0.0([1992] 真机 400 案根治):MODEL_ID **无出厂缺省**。旧缺省是一个只在开发环境存在的
|
|
806
|
-
// 模型名,对任何外部部署都必炸——且炸在离根因最远的地方(gateway 400 + title hook 连环告警),
|
|
807
|
-
// 用户看不出是没配模型。fail-loud at boot 指路配置,与 env 墓碑同哲学:让问题在最早、最清楚处现形。
|
|
808
504
|
const modelId = env("MODEL_ID", "");
|
|
809
505
|
if (modelId === "") {
|
|
810
506
|
throw new Error("MODEL_ID is required (no factory default since server 3.0.0) — set MODEL_ID=<model name your gateway serves>, " +
|
|
811
507
|
"or configure models via the sema-registry catalog; the old silent default was an internal-only model name " +
|
|
812
508
|
"that made every external deployment fail far from the cause (gateway 400)");
|
|
813
509
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
// normalizes the cache-hit-rate denominator per api family (`cacheFamilyOf` picks the fold formula from
|
|
817
|
-
// `Model.api`) — a wrong api picks the wrong family, so the normalized total (core 3.0.0: `totalInputTokens`,
|
|
818
|
-
// core's own cacheHitRate = cachedTokens/totalInputTokens) comes out too small and cacheHitRate exceeds
|
|
819
|
-
// 100%. (Brains select by provider, not api, so this only fixes accounting.)
|
|
820
|
-
//
|
|
821
|
-
// Default inference ([792]④, core ruling 2026-07-14): an EXPLICIT MODEL_PROVIDER always wins (unchanged).
|
|
822
|
-
// When it is unset but the operator explicitly configured an Anthropic-protocol base URL AND a matching
|
|
823
|
-
// credential (the EXACT same `firstSetEnv` predicates the `anthropic` route assembly below uses — F8:
|
|
824
|
-
// non-empty selection, so an EMPTY first alias can't shadow a set fallback), the default is "anthropic" +
|
|
825
|
-
// a one-line boot notice — a clean-machine user pointing ANTHROPIC_BASE_URL at an Anthropic-compatible
|
|
826
|
-
// upstream must not have the model silently routed to the (unconfigured) openai gateway. Base URL alone or
|
|
827
|
-
// credential alone does NOT infer (a bare ANTHROPIC_API_KEY historically only assembles the cloud route for
|
|
828
|
-
// registry-declared anthropic models); neither present ⇒ "gateway", byte-identical to before.
|
|
829
|
-
const explicitProvider = process.env.MODEL_PROVIDER; // env() semantics: "" = unset
|
|
830
|
-
const inferredAnthropicBaseUrl = firstSetEnv("ANTHROPIC_BASE_URL"); // [2354]:无下划线非标名退役(墓碑)
|
|
510
|
+
const explicitProvider = process.env.MODEL_PROVIDER;
|
|
511
|
+
const inferredAnthropicBaseUrl = firstSetEnv("ANTHROPIC_BASE_URL");
|
|
831
512
|
const inferredAnthropicCred = firstSetEnv("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN");
|
|
832
513
|
const provider = (() => {
|
|
833
514
|
if (explicitProvider !== undefined && explicitProvider !== "")
|
|
@@ -845,12 +526,6 @@ function parseModelDomain() {
|
|
|
845
526
|
}
|
|
846
527
|
return "gateway";
|
|
847
528
|
})();
|
|
848
|
-
// Generation-tuning passthrough (core 1.60 `Model.extraBody`, merged via applyExtraBody — brain-owned
|
|
849
|
-
// keys like temperature/max_tokens ALWAYS win, core warns on collision; unset → request unchanged).
|
|
850
|
-
// frequency/presence penalty are the common case (suppress degenerate looping at the source — the
|
|
851
|
-
// prevention to core's degenerate-repetition safety net); MODEL_EXTRA_BODY is the JSON escape hatch
|
|
852
|
-
// (top_k, logit_bias, …). MUST be STATIC (stable key order per cache namespace, else the prefix cache
|
|
853
|
-
// breaks — design/9/31), so it's built once here from fixed env, never per-task.
|
|
854
529
|
const extraBody = buildExtraBody();
|
|
855
530
|
const thinkingDefault = (() => { const v = process.env.MODEL_DEFAULT_THINKING; return v && isThinkingLevel(v) && v !== "off" ? v : undefined; })();
|
|
856
531
|
const effortLevels = (process.env.MODEL_REASONING_EFFORT_LEVELS ?? "").split(",").map((s) => s.trim()).filter((s) => isThinkingLevel(s) && s !== "off");
|
|
@@ -859,27 +534,9 @@ function parseModelDomain() {
|
|
|
859
534
|
name: modelId,
|
|
860
535
|
api: provider === "anthropic" ? "anthropic-messages" : "openai-completions",
|
|
861
536
|
provider,
|
|
862
|
-
// Empty so the URL is owned by the BRAIN layer: each composed brain (gateway / failover fallback /
|
|
863
|
-
// Anthropic route) supplies its own baseUrl, and the brain resolves `model.baseUrl || config.baseUrl`.
|
|
864
|
-
// A non-empty value here would override every brain to one URL — breaking failover AND the Anthropic
|
|
865
|
-
// route (it would POST to the vLLM gateway). See design/15.
|
|
866
537
|
baseUrl: "",
|
|
867
|
-
reasoning: boolEnv("MODEL_REASONING", true),
|
|
868
|
-
|
|
869
|
-
// when body.images hits a text-only model) AND the vision flag on GET /v1/models. The env-lane default model
|
|
870
|
-
// declares vision via MODEL_VISION — DEFAULT "true" keeps the historical image-capable behavior, so set
|
|
871
|
-
// MODEL_VISION=false for a text-only model (e.g. deepseek) to make the precheck actually fire instead of letting
|
|
872
|
-
// images透传 to an opaque downstream-gateway 400. (The centralized config-center lane derives input from its own
|
|
873
|
-
// per-model vision flag — see config-center/facade.ts.)
|
|
874
|
-
input: boolEnv("MODEL_VISION", true) ? ["text", "image"] : ["text"], // #245
|
|
875
|
-
// Per-1M-token USD pricing (core `modelCostToPricing`: input→inputPer1M, …). Default 0 keeps the
|
|
876
|
-
// historical behavior (spend reads $0) — set MODEL_COST_* on a deploy to make `model_cost_micro_usd`
|
|
877
|
-
// + the Fleet card's costUsd real. Numbers are the gateway's contract price (e.g. the review gateway's
|
|
878
|
-
// deepseek-pro), owned by the operator — the center never stores them (design/15: secrets+billing in env).
|
|
879
|
-
// #210 A 档(黑板 [3425]③):四项单价与两项窗口一律 `numEnv`(坏值 boot 拒),不再裸 `Number()`。
|
|
880
|
-
// NaN 单价是本族里后果最重的一形:它顺着 `modelCostToPricing` 一路进 `costUsd`,而 `NaN > ceiling`
|
|
881
|
-
// 与 `NaN < ceiling` **同时为假** —— MAX_TASK_COST_USD / MAX_PRINCIPAL_COST_USD 两道天花板不是变松,
|
|
882
|
-
// 是**整条消失**,且账面读数就是空的,没有任何一处会响。
|
|
538
|
+
reasoning: boolEnv("MODEL_REASONING", true),
|
|
539
|
+
input: boolEnv("MODEL_VISION", true) ? ["text", "image"] : ["text"],
|
|
883
540
|
cost: {
|
|
884
541
|
input: numEnv("MODEL_COST_INPUT", "0"),
|
|
885
542
|
output: numEnv("MODEL_COST_OUTPUT", "0"),
|
|
@@ -887,79 +544,34 @@ function parseModelDomain() {
|
|
|
887
544
|
cacheWrite: numEnv("MODEL_COST_CACHE_WRITE", "0"),
|
|
888
545
|
},
|
|
889
546
|
contextWindow: numEnv("MODEL_CONTEXT_WINDOW", "262144"),
|
|
890
|
-
// [854]③a: env 车道缺省 4096 → 32000。4096 对 reasoning 模型太小(一次完整思考+答案都放不下,长输出
|
|
891
|
-
// 任务被腰斩;上游同类缺省 32000)。per-model clamp:缺省值不越过本 entry 自己声明的 contextWindow
|
|
892
|
-
// (操作员配了小窗口模型时,缺省输出上限不得超过窗口本身 —— env 车道 model entry 就在此装配,唯一可用
|
|
893
|
-
// 约束即 MODEL_CONTEXT_WINDOW)。显式 MODEL_MAX_TOKENS 一如既往原样生效(显式配置=操作员意图,不 clamp);
|
|
894
|
-
// catalog(config.d/models.json)与 center 车道不动(那是显式配置,BL-8 继承链读的是这里的最终值)。
|
|
895
547
|
maxTokens: process.env.MODEL_MAX_TOKENS !== undefined && process.env.MODEL_MAX_TOKENS !== ""
|
|
896
548
|
? parseNumOrFail("MODEL_MAX_TOKENS", process.env.MODEL_MAX_TOKENS)
|
|
897
549
|
: Math.min(32000, numEnv("MODEL_CONTEXT_WINDOW", "262144")),
|
|
898
|
-
// design/123 D2 (core 1.226): structural token-estimate coefficient (chars ≈ 1 token) feeding every
|
|
899
|
-
// non-usage-anchored estimate in core (compaction trigger fallback, clearStale/trim defenses, cut-point
|
|
900
|
-
// accounting). UNSET → omit = core default 4 (byte-compatible chars/4 heuristic). CJK-heavy models
|
|
901
|
-
// (deepseek on Chinese tasks) should set 2–3 — chars/4 underestimates Chinese by 60–75%, letting the
|
|
902
|
-
// request-layer defenses pass over-window requests. optFinitePositiveEnv: a typo'd value degrades to
|
|
903
|
-
// the safe core default rather than zeroing every structural estimate (soft knob, no fail-loud boot).
|
|
904
550
|
...(optFinitePositiveEnv("MODEL_CHARS_PER_TOKEN") !== undefined ? { charsPerToken: optFinitePositiveEnv("MODEL_CHARS_PER_TOKEN") } : {}),
|
|
905
|
-
// Cache family = the BRAIN's usage semantics (design/18): the Anthropic brain reports `input`
|
|
906
|
-
// EXCLUDING cached, the openai gateway brain INCLUDES it. Declare it EXPLICITLY (core 1.23
|
|
907
|
-
// `model.params.promptCacheFamily`) instead of leaving core to INFER it from `model.api` — cleaner,
|
|
908
|
-
// and `api` stays free to mean only "wire format". core warns + clamps if it's wrong (1.23).
|
|
909
551
|
params: { promptCacheFamily: provider === "anthropic" ? "input-excludes-cached" : "input-includes-cached" },
|
|
910
|
-
// design/111 L2②: defaultThinking = this model's DEFAULT tier (resolution chain `spec.thinking ?? role.thinking ??
|
|
911
|
-
// model.defaultThinking`, prepare-task). reasoningEffortLevels (openai endpoints only — Anthropic budget honors the
|
|
912
|
-
// full range) DECLARES which tiers the endpoint accepts so a higher requested tier clamps DOWN instead of 422'ing.
|
|
913
552
|
...(thinkingDefault ? { defaultThinking: thinkingDefault } : {}),
|
|
914
553
|
...(effortLevels.length > 0 && provider !== "anthropic" ? { compat: { reasoningEffortLevels: effortLevels } } : {}),
|
|
915
554
|
...(extraBody ? { extraBody } : {}),
|
|
916
555
|
};
|
|
917
|
-
// 1.24 role map: a catalog + role→model so subsystems resolve a ROLE (cost tiering) instead of a hard
|
|
918
|
-
// model name. An optional cheap model on the SAME gateway (MODEL_CHEAP_ID — just a different id) serves
|
|
919
|
-
// the `subagent`+`summarize` roles, so council lenses & compaction run cheap while the lead/arbiter/team
|
|
920
|
-
// stay on the main model. Unset MODEL_CHEAP_ID → every role resolves to the main model (unchanged).
|
|
921
556
|
const cheapId = process.env.MODEL_CHEAP_ID;
|
|
922
557
|
const cheapModel = cheapId
|
|
923
558
|
? {
|
|
924
559
|
...model,
|
|
925
560
|
id: cheapId,
|
|
926
561
|
name: cheapId,
|
|
927
|
-
// vision precheck (adversarial-review finding): the cheap model must declare its OWN vision
|
|
928
|
-
// capability. Spreading `...model` inherits the MAIN model's `input` modality, so a vision-capable main +
|
|
929
|
-
// text-only cheap (e.g. MODEL_CHEAP_ID=deepseek for subagent/summarize) would advertise vision on /v1/models
|
|
930
|
-
// AND defeat the precheck (config.models[cheapId] is user-pickable via body.model) — the SAME dead-code-on-a-lane
|
|
931
|
-
// class clay caught on the env main model. MODEL_CHEAP_VISION declares it (strict "true"); UNSET → inherit the
|
|
932
|
-
// main model's input (prior behavior, no change — the documented "same gateway, just a different id" case).
|
|
933
|
-
// #245:三态语义不变(缺席/空串 ⇒ 继承主模型),词面收编 parseBoolWord(未知词 boot 拒)。
|
|
934
562
|
input: ((v) => (v === undefined ? model.input : v ? ["text", "image"] : ["text"]))(parseBoolWord("MODEL_CHEAP_VISION", process.env.MODEL_CHEAP_VISION)),
|
|
935
|
-
// Own pricing: pricing is keyed by model id (budget.buildPricing), so without this the cheap model
|
|
936
|
-
// inherits the MAIN model's `cost` and subagent/summarize spend (council lenses, compaction) is
|
|
937
|
-
// metered at the PRO per-token price → cost metric + per-principal quota over-count. Set
|
|
938
|
-
// MODEL_CHEAP_COST_* to the real cheap price; unset → inherit the main cost (prior behavior, no change).
|
|
939
|
-
// #210 A 档:非数值 boot 拒(改前静默 NaN)。空串走 `cheapCostEnv` 的专门臂——它逐字保留改前的
|
|
940
|
-
// `Number("")`=0 并点名,理由见该函数头注(codex 自审 R1-F2)。
|
|
941
563
|
cost: {
|
|
942
564
|
input: cheapCostEnv("MODEL_CHEAP_COST_INPUT", model.cost.input),
|
|
943
565
|
output: cheapCostEnv("MODEL_CHEAP_COST_OUTPUT", model.cost.output),
|
|
944
566
|
cacheRead: cheapCostEnv("MODEL_CHEAP_COST_CACHE_READ", model.cost.cacheRead),
|
|
945
567
|
cacheWrite: cheapCostEnv("MODEL_CHEAP_COST_CACHE_WRITE", model.cost.cacheWrite),
|
|
946
568
|
},
|
|
947
|
-
// systematic-audit: the cheap model also inherited the MAIN model's contextWindow/maxTokens/reasoning verbatim
|
|
948
|
-
// via the `...model` spread — wrong if MODEL_CHEAP_ID points at a model with different limits/capability. Declare
|
|
949
|
-
// them per cheap (MODEL_CHEAP_*); UNSET → inherit main (the documented "same gateway, just a different id" case).
|
|
950
569
|
contextWindow: numEnvOr("MODEL_CHEAP_CONTEXT_WINDOW", model.contextWindow),
|
|
951
570
|
maxTokens: numEnvOr("MODEL_CHEAP_MAX_TOKENS", model.maxTokens),
|
|
952
|
-
// design/123 D2: charsPerToken per cheap slot — the spread inherits the MAIN model's coefficient,
|
|
953
|
-
// wrong when the cheap slot is a different tokenizer family (e.g. CJK-dense deepseek cheap under an
|
|
954
|
-
// anthropic main). MODEL_CHEAP_CHARS_PER_TOKEN overrides; UNSET → inherit main (same knob family).
|
|
955
571
|
...(optFinitePositiveEnv("MODEL_CHEAP_CHARS_PER_TOKEN") !== undefined ? { charsPerToken: optFinitePositiveEnv("MODEL_CHEAP_CHARS_PER_TOKEN") } : {}),
|
|
956
|
-
reasoning: parseBoolWord("MODEL_CHEAP_REASONING", process.env.MODEL_CHEAP_REASONING) ?? model.reasoning,
|
|
572
|
+
reasoning: parseBoolWord("MODEL_CHEAP_REASONING", process.env.MODEL_CHEAP_REASONING) ?? model.reasoning,
|
|
957
573
|
}
|
|
958
574
|
: undefined;
|
|
959
|
-
// [818]①/A-065 P2-6:1M dual-window is now the MODEL-ID table (sonnet-5 family ∧ >=1e6 ⇒ 967000, see
|
|
960
|
-
// applyAutoCompactWindow header — core's own words: "sonnet-5-only … per-model config delivery, not
|
|
961
|
-
// geometry"). MODEL_AUTO_COMPACT_TOKENS is the explicit knob for any other model; a 1M window that gets
|
|
962
|
-
// neither is announced loudly (the pre-P2-6 implicit 967000 must not vanish silently).
|
|
963
575
|
const mainAct = optFinitePositiveEnv("MODEL_AUTO_COMPACT_TOKENS");
|
|
964
576
|
if (applyAutoCompactWindow(model, mainAct) === "removed-1m") {
|
|
965
577
|
CONFIG_NOTICES.push({
|
|
@@ -969,89 +581,50 @@ function parseModelDomain() {
|
|
|
969
581
|
},
|
|
970
582
|
});
|
|
971
583
|
}
|
|
972
|
-
// cheap 槽走同一 id 表(自己的 id 自己查;sonnet 专属值不给非 sonnet cheap);无独立 env 旋钮——
|
|
973
|
-
// 1M cheap 罕见,需要时用 center roster 显式 autoCompactTokens。spread 脏字段照旧被删。
|
|
974
584
|
if (cheapModel)
|
|
975
585
|
applyAutoCompactWindow(cheapModel);
|
|
976
586
|
const models = { default: model, [model.id]: model };
|
|
977
587
|
if (cheapModel)
|
|
978
588
|
models[cheapModel.id] = cheapModel;
|
|
979
589
|
const cheapRef = cheapModel ? cheapModel.id : "default";
|
|
980
|
-
// M1(接入审计 2026-07-08):core verify 契约(verify.d.ts)把「verifier 必须异于 implementer」定为
|
|
981
|
-
// 🔴 部署契约——verifier 落 default(=implementer)时对抗验证者自评自批,去相关性名存实亡。
|
|
982
|
-
// `MODEL_VERIFIER=<catalog 模型 id>` 真兑现旧注释的 "operator can override"(此前只有注释没有机制);
|
|
983
|
-
// 值必须命中 catalog,typo 回落 default 并由 main.ts boot warn 曝光(连同"单模型部署未配 verifier"一起)。
|
|
984
590
|
const verifierEnv = process.env.MODEL_VERIFIER;
|
|
985
591
|
const verifierRef = verifierEnv && models[verifierEnv] ? verifierEnv : "default";
|
|
986
|
-
// rosterMarkerRoles 收敛(0.7 worker.model 真删同车):role→tier 的名单语义
|
|
987
|
-
// 单一来源 = registry-core 的 roster 标记常量(0.6.3 契约:ROSTER_PRIMARY_ROLES=["default","team","synthesize"],
|
|
988
|
-
// ROSTER_CHEAP_ROLES=["subagent","summarize"])——service 不再自带第二份「哪些 role 吃 cheap/primary」清单
|
|
989
|
-
// (双源并存期空转结束)。值仍指我方 catalog ref(cheap 槽未配 ⇒ cheapRef="default" 全落主模型,行为不变);
|
|
990
|
-
// `verifier` 是 service 专有 role(core verify 契约),不在 roster 标记语义内,保留显式。
|
|
991
592
|
const roles = {
|
|
992
|
-
verifier: verifierRef,
|
|
593
|
+
verifier: verifierRef,
|
|
993
594
|
};
|
|
994
595
|
for (const r of ROSTER_PRIMARY_ROLES)
|
|
995
596
|
roles[r] = "default";
|
|
996
597
|
for (const r of ROSTER_CHEAP_ROLES)
|
|
997
598
|
roles[r] = cheapRef;
|
|
998
|
-
// Developer-mode role preset (core 1.43, opt-in, default-neutral): attach the de-branded
|
|
999
|
-
// CODE_AGENT_PROMPT to the named roles via RoleSpec.systemPrompt — those roles run a coding prompt
|
|
1000
|
-
// (when the task doesn't supply its own systemPrompt) while every other role keeps the neutral
|
|
1001
|
-
// default. `MODEL_CODE_ROLES` unset → no change. e.g. MODEL_CODE_ROLES=default,subagent.
|
|
1002
599
|
for (const r of csv("MODEL_CODE_ROLES")) {
|
|
1003
600
|
const role = r;
|
|
1004
601
|
const cur = roles[role];
|
|
1005
602
|
if (cur === undefined)
|
|
1006
|
-
continue;
|
|
1007
|
-
// our role targets are model-name strings; keep an object target's fields if config ever sets one.
|
|
603
|
+
continue;
|
|
1008
604
|
roles[role] =
|
|
1009
605
|
typeof cur === "string"
|
|
1010
606
|
? { model: cur, systemPrompt: CODE_AGENT_PROMPT }
|
|
1011
607
|
: { ...cur, systemPrompt: CODE_AGENT_PROMPT };
|
|
1012
608
|
}
|
|
1013
|
-
// A2:词表验证**无条件**执行(此前它挂在下面 `MODEL_DEGRADE_TO ? {...}` 三元的真臂里,TO 缺席时
|
|
1014
|
-
// parseDegradeOn 根本不可达 ⇒ 拼错词静默无效),而 USAGE.md 的「拼错词 boot 拒启,不静默忽略」是对
|
|
1015
|
-
// MODEL_DEGRADE_ON 本身的无条件承诺:设了就验。TO 缺席 + ON 合法 ⇒ 照旧不装配 degrade(行为不变),
|
|
1016
|
-
// 也不报错——ON 只是把那份已在的意图写下来,不是把 degrade 打开。
|
|
1017
609
|
const degradeOn = parseDegradeOn(csv("MODEL_DEGRADE_ON"));
|
|
1018
610
|
return {
|
|
1019
611
|
gatewayBaseUrl,
|
|
1020
612
|
gatewayApiKey: process.env.MODEL_API_KEY,
|
|
1021
613
|
gatewayFallbackUrls: csv("MODEL_GATEWAY_FALLBACK_URLS"),
|
|
1022
|
-
// 缺席**不铸键**(不是铸个 2):见 config-types 的 `gatewayMaxRetries` 顶注 —— 缺席时 brain.ts 不把
|
|
1023
|
-
// 这个字段传给引擎,引擎默认当家。铸个默认值等于把「server 钉死引擎默认」这个病换个地方犯。
|
|
1024
614
|
...(process.env.GATEWAY_MAX_RETRIES ? { gatewayMaxRetries: numEnvBounded("GATEWAY_MAX_RETRIES", "2", 0, 20) } : {}),
|
|
1025
|
-
// Anthropic-compatible-ecosystem env compat (2026-07-03): the route now assembles when EITHER credential
|
|
1026
|
-
// is present — `ANTHROPIC_API_KEY` (x-api-key) or `ANTHROPIC_AUTH_TOKEN` (Authorization: Bearer; many such
|
|
1027
|
-
// providers accept only this). AUTH_TOKEN wins when both are set. Base URL reads the widely-adopted
|
|
1028
|
-
// standard name `ANTHROPIC_BASE_URL` too — the explicit sema name (no underscore) wins on a conflict.
|
|
1029
|
-
// F8: alias selection via `firstSetEnv` ("" = unset) — the SAME helper the provider inference above
|
|
1030
|
-
// consumes, so an empty first alias can neither suppress the route nor shadow the fallback base URL.
|
|
1031
615
|
anthropic: firstSetEnv("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN")
|
|
1032
616
|
? {
|
|
1033
617
|
apiKey: firstSetEnv("ANTHROPIC_API_KEY"),
|
|
1034
618
|
authToken: firstSetEnv("ANTHROPIC_AUTH_TOKEN"),
|
|
1035
619
|
baseUrl: firstSetEnv("ANTHROPIC_BASE_URL"),
|
|
1036
620
|
version: process.env.ANTHROPIC_VERSION,
|
|
1037
|
-
cacheBreakpoints: boolEnv("ANTHROPIC_CACHE_BREAKPOINTS", true),
|
|
1038
|
-
...(process.env.ANTHROPIC_MAX_RETRIES ? { maxRetries: numEnv("ANTHROPIC_MAX_RETRIES", "2") } : {}),
|
|
621
|
+
cacheBreakpoints: boolEnv("ANTHROPIC_CACHE_BREAKPOINTS", true),
|
|
622
|
+
...(process.env.ANTHROPIC_MAX_RETRIES ? { maxRetries: numEnv("ANTHROPIC_MAX_RETRIES", "2") } : {}),
|
|
1039
623
|
}
|
|
1040
624
|
: undefined,
|
|
1041
625
|
resilience: {
|
|
1042
|
-
// BEHAVIOR CHANGE ([880] stall-watchdog audit): connect/firstToken default ON. core's idle
|
|
1043
|
-
// timer only arms AFTER the first token (bumpIdle lives in sawContentToken), so with these
|
|
1044
|
-
// two off a stream that opens but never emits hangs unbounded — the one true blind spot the
|
|
1045
|
-
// fleet audit found. Values follow the [880] recommendation; =0 restores off.
|
|
1046
|
-
// 鲁棒性批1 A1(2026-08-03):这五个是 TASK_TIMEOUT_SEC 同族的**危险方向**键——NaN 在 core 的
|
|
1047
|
-
// `if (x && x > 0)` 门下判假 ⇒ 看门狗/熔断静默关(设了收紧值反而更松)。当年 gap-sweep 的棘轮
|
|
1048
|
-
// 正则只抓 process.env 直读拼法,这批「裸 Number 包 env() helper」从盲区漏网;门已补第二正则。
|
|
1049
626
|
connectTimeoutMs: numEnv("MODEL_CONNECT_TIMEOUT_MS", "30000"),
|
|
1050
627
|
firstTokenTimeoutMs: numEnv("MODEL_FIRST_TOKEN_TIMEOUT_MS", "120000"),
|
|
1051
|
-
// BEHAVIOR CHANGE (CC 196 parity): mid-stream stall watchdog default ON at 5min —
|
|
1052
|
-
// after output starts, 5min of zero deltas cancels the stream as a retryable [network] failure
|
|
1053
|
-
// (retry/failover recovers; without it a hung gateway pins the turn until the user Escapes).
|
|
1054
|
-
// MODEL_IDLE_TIMEOUT_MS=0 restores off.
|
|
1055
628
|
idleTimeoutMs: numEnv("MODEL_IDLE_TIMEOUT_MS", "300000"),
|
|
1056
629
|
circuitBreaker: boolEnv("MODEL_CIRCUIT_BREAKER", false),
|
|
1057
630
|
failureThreshold: numEnv("MODEL_CB_FAILURE_THRESHOLD", "5"),
|
|
@@ -1059,46 +632,25 @@ function parseModelDomain() {
|
|
|
1059
632
|
},
|
|
1060
633
|
model,
|
|
1061
634
|
models,
|
|
1062
|
-
modelApiKeyEnv: {},
|
|
1063
|
-
modelApiKeys: {},
|
|
1064
|
-
modelQuotaWeights: {},
|
|
1065
|
-
tiers: {},
|
|
1066
|
-
projects: {},
|
|
635
|
+
modelApiKeyEnv: {},
|
|
636
|
+
modelApiKeys: {},
|
|
637
|
+
modelQuotaWeights: {},
|
|
638
|
+
tiers: {},
|
|
639
|
+
projects: {},
|
|
1067
640
|
roles,
|
|
1068
|
-
// #233 / A-002.8 `@`-mention 允许名单:env lane 恒空(INERT = 全放行,ship-dark 缺省 = 旧行为);
|
|
1069
|
-
// applyEffective 从 models.atModelAllowlist 整表重算填充(非粘,见 config-types.ts 键注)。
|
|
1070
|
-
// 刻意**没有** env 腿:名单是 center 治理面的字段(registry-core 侧还有 name⊆catalog 的 cross-domain
|
|
1071
|
-
// 校验),给它开一条 env 后门 = 两个写者一个语义面,而 env 侧没有那道校验。
|
|
1072
641
|
atModelAllowlist: [],
|
|
1073
642
|
cascadeLadder: csv("MODEL_CASCADE_LADDER"),
|
|
1074
643
|
degrade: process.env.MODEL_DEGRADE_TO
|
|
1075
644
|
? {
|
|
1076
645
|
to: env("MODEL_DEGRADE_TO"),
|
|
1077
|
-
// #210 A 档:本族里最必须拒的一条 —— NaN 阈值让 `spent/budget >= atCostFraction` 恒假,**降级
|
|
1078
|
-
// 整条永久静默关闭**,而 operator 恰恰是配了 MODEL_DEGRADE_TO 才走到这里(他确信降级开着)。
|
|
1079
|
-
// 不加 [0,1] 上下界:>1 是「实际上别降级」的合法表达,收窄会破已配了它的存量部署,而律条要治的
|
|
1080
|
-
// 是坏值静默,不是把区间一起收了。
|
|
1081
646
|
atCostFraction: numEnv("MODEL_DEGRADE_AT_COST_FRACTION", "0.7"),
|
|
1082
647
|
reactive: boolEnv("MODEL_DEGRADE_REACTIVE", false),
|
|
1083
|
-
// core 5.1.0([2390]②):词表=DegradeReason 全集;缺席时 core 默认含 server_error(默认门变宽),
|
|
1084
|
-
// 本 env 是运维收窄/放宽的唯一旋钮。拼错词 boot 响亮拒——盲铸静默折无效成员是 TASK_TIMEOUT_SEC 同族病。
|
|
1085
|
-
// (验词在上方无条件做掉了,见 A2 注;这里只是取用已验过的结果。)
|
|
1086
648
|
downgradeOn: degradeOn,
|
|
1087
|
-
// vision precheck (adversarial-review finding): the degrade TARGET is an external gateway
|
|
1088
|
-
// model NOT in config.models, so resolveSpec's precheck (which only sees the picked model) can't see its
|
|
1089
|
-
// vision capability — a vision-capable main + text-only degrade target would let images reach the text-only
|
|
1090
|
-
// gateway as the opaque 400 the precheck prevents, AFTER admission, at runtime degrade. Declare it via
|
|
1091
|
-
// MODEL_DEGRADE_TO_VISION (strict "true"). DEFAULT false = assume the cheaper degrade target is text-only
|
|
1092
|
-
// (the common case, e.g. deepseek), so resolveSpec drops degrade for image-carrying tasks (main.ts).
|
|
1093
649
|
toSupportsImages: boolEnv("MODEL_DEGRADE_TO_VISION", false),
|
|
1094
650
|
}
|
|
1095
651
|
: undefined,
|
|
1096
652
|
};
|
|
1097
653
|
}
|
|
1098
|
-
/** 域:approval(审批/HITL 门)—— 审批名单与预算、durable 面、直连门开关与钥、敏感写门、elicitation。
|
|
1099
|
-
* 注:直连门的**激活**(directDoorActive)与 D-G §3 半开门不变量是跨域合取,留在装配层。 */
|
|
1100
|
-
/** MODEL_DEGRADE_ON 词表=core `DegradeReason` 全集(集合用该类型铸=锁步:core 删词此处 tsc 红,加词这里
|
|
1101
|
-
* 补一个词即可)。非法词 boot 响亮拒——盲铸静默折无效成员与 TASK_TIMEOUT_SEC 非法值折「无超时」同族。 */
|
|
1102
654
|
const DEGRADE_ON_WORDS = new Set(["breaker_open", "rate_limit", "budget", "server_error", "last_resort"]);
|
|
1103
655
|
function parseDegradeOn(words) {
|
|
1104
656
|
if (words.length === 0)
|
|
@@ -1109,19 +661,6 @@ function parseDegradeOn(words) {
|
|
|
1109
661
|
}
|
|
1110
662
|
return words;
|
|
1111
663
|
}
|
|
1112
|
-
/**
|
|
1113
|
-
* #186 — the CC-rule-form arm, split out because it is the ONE arm that also judges an **allow** list.
|
|
1114
|
-
*
|
|
1115
|
-
* The general "unmatchable name" judgment deliberately spares `allow` (a mistyped live name leaves the allowlist
|
|
1116
|
-
* effectively empty ⇒ everything is denied ⇒ fail-CLOSED and immediately visible). A CC-form entry is a different
|
|
1117
|
-
* animal: `allow: ["Bash(ps:*)"]` is what a CC user writes to get FEWER prompts, and what they actually get is
|
|
1118
|
-
* "only a tool literally named `Bash(ps:*)` may run" ⇒ Bash unavailable, with no warning anywhere. That is a
|
|
1119
|
-
* spelling-FAMILY error, not a typo, so it is refused on every list — with the two knobs that really do the job.
|
|
1120
|
-
*
|
|
1121
|
-
* 🔴 CC 形语义(参数级规则)**已裁不支持,此为终态**(clay 终裁 2026-08-16,sema-comms
|
|
1122
|
-
* audits/CLAY-RULINGS.md R-9;历史上已裁多遍,禁再上问)——响亮拒+指路即本面的完整承诺。
|
|
1123
|
-
* 判据只认括号的在场,残形(`Bash(ps`)同样拒——半个括号一样永不匹配活名。
|
|
1124
|
-
*/
|
|
1125
664
|
function ccRuleFormGuidance(name) {
|
|
1126
665
|
if (!name.includes("(") && !name.includes(")"))
|
|
1127
666
|
return undefined;
|
|
@@ -1154,8 +693,6 @@ export function findUnmatchableToolNames(names) {
|
|
|
1154
693
|
}
|
|
1155
694
|
const ns = protocolOf(name);
|
|
1156
695
|
if (ns === undefined) {
|
|
1157
|
-
// 5.3.0 协议表两行起,短名归属不再唯一(figma__x 可能是 mcp 短形或 a2a 短形)——指引枚举全表
|
|
1158
|
-
// 前缀(与 core 5.3.0 自侧同族修同口径),不再手写死 mcp__。
|
|
1159
696
|
if (name.includes("__"))
|
|
1160
697
|
out.push({ name, guidance: `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => `${e.prefix}${name}`).join(" or ")}` });
|
|
1161
698
|
continue;
|
|
@@ -1166,38 +703,14 @@ export function findUnmatchableToolNames(names) {
|
|
|
1166
703
|
}
|
|
1167
704
|
return out;
|
|
1168
705
|
}
|
|
1169
|
-
/** {@link findUnmatchableToolNames} 的成句形——三条腿的文案同源(只有前缀/出口不同)。 */
|
|
1170
706
|
export function formatUnmatchableToolNames(source, bad) {
|
|
1171
707
|
return (`${source} names tool(s) that can never match a live tool (these lists are RAW whole-name comparisons — core 5.0.0 RB-476 removed the alias/auto-prefix folds): ` +
|
|
1172
708
|
bad.map((b) => `${b.name} → ${b.guidance}`).join("; "));
|
|
1173
709
|
}
|
|
1174
|
-
/**
|
|
1175
|
-
* #151 流内审批协议(design/172)的九键段(车3 刀 3a 五键 + 车5 收敛器四键)。
|
|
1176
|
-
*
|
|
1177
|
-
* **总开关默认 true**(clay 裁 2026-08-08 走 (a),#164 翻真验证后)。⚠️ 版本坐标:翻转**已在树上生效**,
|
|
1178
|
-
* 发布线上它落在 7.4.0 之后的下一个发布(CHANGELOG `Unreleased` 段,预计 7.5.0)—— 所以一台自报 7.4.0
|
|
1179
|
-
* 的**已发布** worker 仍是 OFF,而拿 main 构建的 worker 已是 ON。别只按 `/health` 的版本号推默认。显式
|
|
1180
|
-
* `STREAM_APPROVAL_ENABLED=false` 是**唯一干净还原键** ⇒ 全链逐字回到翻转前行为(不发
|
|
1181
|
-
* approval_request、不落 ask 行、不起收敛器腿、窗仍是 `DEFAULT_APPROVAL_TTL_MS`)。同一裁定把窗默认
|
|
1182
|
-
* 从 60s 抬到 300000(= `DEFAULT_APPROVAL_TTL_MS` 的 5min),于是「开协议」不再顺带把窗砍短 ——
|
|
1183
|
-
* 当年默认 false + 60s 窗那一版的「谁需要 / 谁被伤 / 什么补偿」成文记录在 `config-types.ts` 的
|
|
1184
|
-
* `ServiceConfigFlat.streamApproval` 注里(读它时记得它记的是**翻转前**的前提)。
|
|
1185
|
-
* 窗/帽四键沿用 `numEnv` fail-loud 形(坏形启动期炸,不静默折 NaN);
|
|
1186
|
-
* 车5 的四键一律 **`numEnvBounded`**(§9 C6:batch 整数 ≥1 有上帽,grace/TTL 非负有界)——它们直接
|
|
1187
|
-
* 决定 reaper 每 tick 的库压与「多久算遗孤」,一个手滑的 `0` 或 `1e12` 都是运维事故面。
|
|
1188
|
-
*
|
|
1189
|
-
* 🔴 跨旋钮不变量(§9 C6,`#157` 三分类 P 族姿势):**`WINDOW + ADHOC_GRACE < ORPHAN_TTL`**。破坏形
|
|
1190
|
-
* **拒启** —— 判据 4 在 `expiresAtMs + ADHOC_GRACE` 触发而 `expiresAtMs ≈ createdAtMs + WINDOW`,判据 5
|
|
1191
|
-
* 在 `createdAtMs + ORPHAN_TTL` 触发;不等式不成立时判据 5 会先于判据 4 到,一条 adhoc 腿拿到的归因就成了
|
|
1192
|
-
* `orphan_ttl_exceeded`(「遗孤兜底」)而不是 `adhoc_leg_no_durable_domain`(「结构上无对账域」),
|
|
1193
|
-
* 审计面从此读不出真实成因。⚠️ **窗那一项不能省**(本注上一版只写 `ADHOC_GRACE < ORPHAN_TTL`,与下方
|
|
1194
|
-
* 真校验和它的 codex C7 论证矛盾):只比宽限会漏掉窗,例如 WINDOW 与 ORPHAN_TTL 等长时校验放行但遗孤
|
|
1195
|
-
* 兜底必定先到。默认值(300s + 60s vs 7d)自然满足;只有显式改坏才会撞上。
|
|
1196
|
-
*/
|
|
1197
710
|
function streamApprovalConfig() {
|
|
1198
711
|
const cfg = {
|
|
1199
|
-
enabled: boolEnv("STREAM_APPROVAL_ENABLED", true),
|
|
1200
|
-
windowMs: numEnv("STREAM_ASK_WINDOW_MS", "300000"),
|
|
712
|
+
enabled: boolEnv("STREAM_APPROVAL_ENABLED", true),
|
|
713
|
+
windowMs: numEnv("STREAM_ASK_WINDOW_MS", "300000"),
|
|
1201
714
|
replayMax: numEnv("STREAM_APPROVAL_REPLAY_MAX", "50"),
|
|
1202
715
|
admitMaxPerTask: numEnv("STREAM_APPROVAL_ADMIT_MAX_PER_TASK", "32"),
|
|
1203
716
|
admitMaxPerOwner: numEnv("STREAM_APPROVAL_ADMIT_MAX_PER_OWNER", "256"),
|
|
@@ -1206,30 +719,14 @@ function streamApprovalConfig() {
|
|
|
1206
719
|
adhocGraceMs: numEnvBounded("STREAM_APPROVAL_ADHOC_GRACE_MS", "60000", 0, 86_400_000),
|
|
1207
720
|
orphanTtlMs: numEnvBounded("STREAM_APPROVAL_ORPHAN_TTL_MS", String(7 * 24 * 60 * 60 * 1000), 60_000, 90 * 24 * 60 * 60 * 1000),
|
|
1208
721
|
};
|
|
1209
|
-
// 🔴 不变量的**正确形**(codex 交叉复审 C7,2026-08-06 验真):两条臂量的不是同一个起点 ——
|
|
1210
|
-
// 判据 4 在 `expiresAtMs + ADHOC_GRACE` 触发,而 `expiresAtMs ≈ createdAtMs + WINDOW`;判据 5 在
|
|
1211
|
-
// `createdAtMs + ORPHAN_TTL` 触发。只比 `ADHOC_GRACE < ORPHAN_TTL` 会漏掉窗:例如 WINDOW 与
|
|
1212
|
-
// ORPHAN_TTL 等长时校验放行,但遗孤兜底必定先到,adhoc 腿的归因被写成 `orphan_ttl_exceeded`。
|
|
1213
|
-
// 正确判据 = `WINDOW + ADHOC_GRACE < ORPHAN_TTL`(两边都是有界有限值,不会溢出)。
|
|
1214
722
|
if (cfg.windowMs + cfg.adhocGraceMs >= cfg.orphanTtlMs) {
|
|
1215
723
|
throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} + STREAM_APPROVAL_ADHOC_GRACE_MS=${cfg.adhocGraceMs} must be STRICTLY LESS than STREAM_APPROVAL_ORPHAN_TTL_MS=${cfg.orphanTtlMs} — the adhoc arm fires at (createdAt + window + grace) while the orphan backstop fires at (createdAt + ttl); if the backstop wins, every adhoc-leg row is attributed "orphan_ttl_exceeded" instead of "adhoc_leg_no_durable_domain" (attribution honesty, design/172 §6.1)`);
|
|
1216
724
|
}
|
|
1217
|
-
// 整数门(batch 是行数,不是时长):`numEnvBounded` 只管区间,`200.5` 会一路走到 SQL `LIMIT` 上。
|
|
1218
725
|
if (!Number.isInteger(cfg.reconcileBatch)) {
|
|
1219
726
|
throw new Error(`env STREAM_APPROVAL_RECONCILE_BATCH=${cfg.reconcileBatch} must be an INTEGER row count`);
|
|
1220
727
|
}
|
|
1221
728
|
return cfg;
|
|
1222
729
|
}
|
|
1223
|
-
/**
|
|
1224
|
-
* #280 R-13 C —— `UNATTENDED_APPROVAL_POLICY` 的闭集解析(#210 A 档:坏值**拒启**)。
|
|
1225
|
-
*
|
|
1226
|
-
* 词表 = `park`(缺省)| `deny`,**逐字精确匹配**(小写、两侧空白裁掉):`PARK` / `Deny` / `true` / `off`
|
|
1227
|
-
* 一律落到拒启臂 —— 与 `MANUAL_MODE_SHELL_GATE` 同姿势,理由逐字同(「看起来被接受了」= 它其实什么都没做,
|
|
1228
|
-
* 而这条轴决定的是权限门在无人值守时的终局方向,静默失效是运维面最贵的那一类)。
|
|
1229
|
-
* ⚠️ 与 `MANUAL_MODE_SHELL_GATE` 的一处**刻意不同**:本键**没有** `off` 词。它的两个值都是真姿态
|
|
1230
|
-
* (park / deny),不存在「这个旋钮不参与」的第三态;空串/未设 = 缺省 `park`,那是文档化的出厂姿态,
|
|
1231
|
-
* 不是「关掉」。语义与射程成文见 `config-types.ts` 的域注。
|
|
1232
|
-
*/
|
|
1233
730
|
function unattendedApprovalPolicyEnv() {
|
|
1234
731
|
const raw = (process.env.UNATTENDED_APPROVAL_POLICY ?? "").trim();
|
|
1235
732
|
if (raw === "")
|
|
@@ -1242,16 +739,10 @@ function unattendedApprovalPolicyEnv() {
|
|
|
1242
739
|
`Refusing to boot rather than treating a misspelled value as "unset": that runs the deployment on a policy the operator did not choose, with no trace anywhere`);
|
|
1243
740
|
}
|
|
1244
741
|
function parseApprovalDomain(ctx) {
|
|
1245
|
-
const { postureOn } = ctx;
|
|
1246
|
-
// design/80 D-G: the direct-connect approval door anchors (all must be present to ACTIVATE; fail-closed).
|
|
742
|
+
const { postureOn } = ctx;
|
|
1247
743
|
const directApprovalDoor = boolEnv("DIRECT_APPROVAL_DOOR", false);
|
|
1248
744
|
const dgHmacKeys = parseApprovalHmacKeys(process.env.APPROVAL_HMAC_KEYS, announceKeySetRejection("APPROVAL_HMAC_KEYS"));
|
|
1249
|
-
// DURABLE_APPROVAL 既是本域字段,也是直连门的硬前置 —— 那条前置的理由与断言在装配层的跨域不变量段。
|
|
1250
745
|
const durableApproval = boolEnv("DURABLE_APPROVAL", false);
|
|
1251
|
-
// [2380] core 5.0.0 RB-476:approval 名单里「永不匹配」的名字**装配期拒启**(带修正指引)。core 自己的
|
|
1252
|
-
// prepare 门(config.legacy_tool_name)在提交期才响——boot 期先拒,部署带失效名单不启动而不是首任务炸。
|
|
1253
|
-
// 名单 RAW 直传(canonicalToolName 折叠面已随 core 退役,见 approval.ts);判据单点见
|
|
1254
|
-
// {@link findUnmatchableToolNames}(env / 中心下发 / per-request 三腿共用同一条)。
|
|
1255
746
|
const rejectUnmatchableToolNames = (envName, names) => {
|
|
1256
747
|
const bad = findUnmatchableToolNames(names);
|
|
1257
748
|
if (bad.length > 0)
|
|
@@ -1263,14 +754,11 @@ function parseApprovalDomain(ctx) {
|
|
|
1263
754
|
approvalDeny: rejectUnmatchableToolNames("APPROVAL_DENY", csv("APPROVAL_DENY")),
|
|
1264
755
|
durableApproval,
|
|
1265
756
|
resourceSuspend: boolEnv("RESOURCE_SUSPEND", false),
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
approvalAutoBudget: clampEnvWithWarn("APPROVAL_AUTO_BUDGET", "0", 0, 10000), // clamp [0,10000]; 0 = off
|
|
757
|
+
resourceSuspendTtlSec: clampEnvWithWarn("RESOURCE_SUSPEND_TTL_SEC", "0", 0),
|
|
758
|
+
approvalAutoBudget: clampEnvWithWarn("APPROVAL_AUTO_BUDGET", "0", 0, 10000),
|
|
1269
759
|
approvalNeverAuto: rejectUnmatchableToolNames("APPROVAL_NEVER_AUTO", csv("APPROVAL_NEVER_AUTO")),
|
|
1270
|
-
approvalHmacKeys: dgHmacKeys,
|
|
760
|
+
approvalHmacKeys: dgHmacKeys,
|
|
1271
761
|
directApprovalDoor,
|
|
1272
|
-
// ③ sensitive-path write deny set: unset = core's recommended set; explicit value = full replacement
|
|
1273
|
-
// (comma-separated); "off"/empty = disabled. Set curation is core's (成文 core-side); server passes through.
|
|
1274
762
|
sensitiveWritePatterns: ((raw) => {
|
|
1275
763
|
if (raw === undefined)
|
|
1276
764
|
return [...RECOMMENDED_SENSITIVE_PATTERNS];
|
|
@@ -1279,23 +767,6 @@ function parseApprovalDomain(ctx) {
|
|
|
1279
767
|
return [];
|
|
1280
768
|
return trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
1281
769
|
})(process.env.SENSITIVE_WRITE_PATTERNS),
|
|
1282
|
-
// [1557]§四 → #153: opt-in only — the two real gate shapes ride; `off`/empty/unset leave the field ABSENT
|
|
1283
|
-
// (no key on the config object at all), so `applyRuntimeGovernance` never receives the knob and THIS knob
|
|
1284
|
-
// contributes nothing to the governance pass. Note that pass can still set `TaskSpec.shellGate` on its own
|
|
1285
|
-
// account — `AUTONOMY=ask` derives "always" independently, and (design/201) an explicit client permission
|
|
1286
|
-
// mode now supplies the BASE档 this knob tightens from.
|
|
1287
|
-
//
|
|
1288
|
-
// design/201 §4 —— 词表与非法值:合法值 = `always | classify | off | 空串/未设`,**其余 boot 拒启**。
|
|
1289
|
-
// 动机是把**既有**风险变响亮而不是防将来:改前任何手滑值(`clasify`/`true`/`ON`)与未设置等效,
|
|
1290
|
-
// 于是「我配了闸」的部署实际在无闸运行,且这件事零留痕。同层先例=`rejectUnmatchableToolNames`
|
|
1291
|
-
// (上方,解析层拒启;#177 的 `assertGuardPatternsUsable` 在 deployment-governance 层,不是本层)。
|
|
1292
|
-
//
|
|
1293
|
-
// 🔴 **与兄弟旋钮的语义不对称是刻意的,禁止将来"统一"**:`SENSITIVE_WRITE_PATTERNS=off` 是一根
|
|
1294
|
-
// **真放松杆**(清空 server 自己的产品默认推荐集 —— off = 对那份默认的显式退出);
|
|
1295
|
-
// `MANUAL_MODE_SHELL_GATE=off` 是 **no-op**(归一成键缺席),因为本键的缺省不属于 server 产品默认,
|
|
1296
|
-
// 而属于**车道翻译轴**(design/201 的 permissionMode 表),env 在这条轴上只有收紧半场、没有放松方向。
|
|
1297
|
-
// 把 off 做成"关闸"就等于给部署级旋钮开一条放松通道,那正是 #153 修掉的病。所以 off 不静默:
|
|
1298
|
-
// 归一的同时发一条 boot 诊断行(下面的 CONFIG_NOTICES),让运维不会按兄弟旋钮的直觉误读。
|
|
1299
770
|
...((raw) => {
|
|
1300
771
|
const trimmed = (raw ?? "").trim();
|
|
1301
772
|
if (trimmed === "always" || trimmed === "classify")
|
|
@@ -1319,7 +790,7 @@ function parseApprovalDomain(ctx) {
|
|
|
1319
790
|
`Refusing to boot rather than treating a misspelled value as "unset": that silently ran the deployment with NO shell gate while the operator believed one was configured`);
|
|
1320
791
|
})(process.env.MANUAL_MODE_SHELL_GATE),
|
|
1321
792
|
mcpElicitation: {
|
|
1322
|
-
enabled: boolEnv("MCP_ELICITATION_ENABLED", false),
|
|
793
|
+
enabled: boolEnv("MCP_ELICITATION_ENABLED", false),
|
|
1323
794
|
throttle: {
|
|
1324
795
|
maxConcurrentPerRun: numEnvBounded("MCP_ELICITATION_MAX_CONCURRENT", String(DEFAULT_ELICITATION_THROTTLE.maxConcurrentPerRun), 1, 64),
|
|
1325
796
|
maxTotalPerRun: numEnvBounded("MCP_ELICITATION_MAX_TOTAL", String(DEFAULT_ELICITATION_THROTTLE.maxTotalPerRun), 1, 10_000),
|
|
@@ -1327,54 +798,21 @@ function parseApprovalDomain(ctx) {
|
|
|
1327
798
|
ttlMs: numEnvBounded("MCP_ELICITATION_TTL_MS", String(DEFAULT_ELICITATION_THROTTLE.ttlMs), 1_000, 3_600_000),
|
|
1328
799
|
},
|
|
1329
800
|
},
|
|
1330
|
-
askQuestionEnabled: postureOn("ASK_QUESTION_ENABLED"),
|
|
1331
|
-
// #166 裁2:AskUserQuestion 裁量窗三旋钮,姿势照 MCP_ELICITATION_* 抄(bounded ⇒ 越界启动期拒,不静默
|
|
1332
|
-
// 夹取)。默认值 = 协调器出厂窗逐字(纯旋钮化,零行为改动)。
|
|
801
|
+
askQuestionEnabled: postureOn("ASK_QUESTION_ENABLED"),
|
|
1333
802
|
questionThrottle: {
|
|
1334
803
|
maxConcurrentPerRun: numEnvBounded("QUESTION_MAX_CONCURRENT_PER_RUN", String(DEFAULT_QUESTION_THROTTLE.maxConcurrentPerRun), 1, 64),
|
|
1335
804
|
maxTotalPerRun: numEnvBounded("QUESTION_MAX_TOTAL_PER_RUN", String(DEFAULT_QUESTION_THROTTLE.maxTotalPerRun), 1, 10_000),
|
|
1336
805
|
ttlMs: numEnvBounded("QUESTION_TTL_MS", String(DEFAULT_QUESTION_THROTTLE.ttlMs), 1_000, 3_600_000),
|
|
1337
806
|
},
|
|
1338
|
-
toolApprovalEnabled: postureOn("TOOL_APPROVAL_ENABLED"),
|
|
1339
|
-
// #154 车二 → #203:持久化权限规则车道的总开关。**默认 ON**(clay [3396] 裁),理由与它此前默认
|
|
1340
|
-
// OFF 的**唯一**前置(撤销面缺席)逐字见 `ServiceApprovalConfig.permissionRulesEnabled`。
|
|
1341
|
-
// **不**做 posture 派生:这条轴与「单用户交钥匙」无关(多租户同样要它),所以它是一条与部署形态
|
|
1342
|
-
// 正交的普通 opt-out 旋钮,而不是 `postureOn` 那一族。
|
|
807
|
+
toolApprovalEnabled: postureOn("TOOL_APPROVAL_ENABLED"),
|
|
1343
808
|
permissionRulesEnabled: boolEnv("PERMISSION_RULES_ENABLED", true),
|
|
1344
|
-
// 🔴 「运维**显式**表过态吗」是与取值分开的一位(同 `dbBackendExplicit` 的姿势)。它只有一个消费者:
|
|
1345
|
-
// boot 期的休眠行审计(`boot/permission-rules-audit.ts`)——对显式开过的部署说「N 条规则随默认 ON
|
|
1346
|
-
// 激活」是假话,那些行本来就是活的。判据与 `boolEnv` 认的字面量集**同源**(只有 "true"/"false"
|
|
1347
|
-
// 算表态;空串按未设,拼错的值走 boolEnv 的告警臂并落回默认 ⇒ 也不算表态)。
|
|
1348
|
-
// #245:显式表态判别随词表放宽——八词任一在场即「显式」(旧形只认 true/false 两词,=1 的显式关/开
|
|
1349
|
-
// 会被当「未表态」滑进派生腿,恰是 operator-knob 病的判别位变体);未知词由本体 boolEnv 先拒启。
|
|
1350
809
|
permissionRulesEnabledExplicit: parseBoolWord("PERMISSION_RULES_ENABLED", process.env["PERMISSION_RULES_ENABLED"]) !== undefined,
|
|
1351
|
-
// #151 车2(design/172 §3.3 D3):ToolApprovalCoordinator 窗长三元的安全余量。numEnv fail-loud 形,
|
|
1352
|
-
// 照邻居旋钮(approvalTimeoutSec 等)抄——坏形(非数字)在启动期炸,不静默折成 NaN。
|
|
1353
810
|
streamAskWindowMarginMs: numEnv("STREAM_ASK_WINDOW_MARGIN_MS", "10000"),
|
|
1354
|
-
// #151 车3 刀 3a + 车5(design/172 流内审批协议):九键。默认极性 / 窗值 / 解析形 / 跨旋钮不变量
|
|
1355
|
-
// **一处成文** = 本文件 `streamApprovalConfig()` 的头注,这里不复述 —— 复述的两份必然各自漂,这条注
|
|
1356
|
-
// 的上一版正是这么漂的(默认翻 ON 之后它还写着「总开关默认 false」整整一版)。
|
|
1357
|
-
// 三问记录(翻转前那一版)见 config-types.ts 的 StreamApprovalConfig。
|
|
1358
811
|
streamApproval: streamApprovalConfig(),
|
|
1359
|
-
// #280 R-13 C:无人值守政策(闭集 park|deny,缺省 park)。语义/三问/射程边界**一处成文** =
|
|
1360
|
-
// `config-types.ts` 的 `unattendedApprovalPolicy` 域注,这里只解析,不复述(复述的两份必然各自漂)。
|
|
1361
812
|
unattendedApprovalPolicy: unattendedApprovalPolicyEnv(),
|
|
1362
|
-
// [875]a 成文:0(缺省)= durable HITL 无限期等人,时间型 reapSuspended 不跑;file/memory lane 的回收
|
|
1363
|
-
// 探针只在 DURABLE_APPROVAL=true 时注入(无 durable 的部署两只 suspended 回收器恒 NO-OP,parked 行的
|
|
1364
|
-
// 恢复把手 = POST /v1/runs/:id/cancel,[868]①)。checkpoint 过期驱动的那只不受本旋钮门控。
|
|
1365
813
|
approvalTimeoutSec: numEnv("APPROVAL_TIMEOUT_SEC", "0"),
|
|
1366
814
|
};
|
|
1367
815
|
}
|
|
1368
|
-
/**
|
|
1369
|
-
* #210:MEMORY_ENGINE 的**双族**闭集(codex 自审 R1-F3 真修,把第一版的 `on|off` 两词放宽)。
|
|
1370
|
-
*
|
|
1371
|
-
* 病灶只在**关**的那一侧:改前判据 `!== "off"` 让 `OFF` / `false` / `disabled` / `0` 全部读成「开」——
|
|
1372
|
-
* operator 明确表达了「关掉记忆面」,引擎却照常往每个任务里注入记忆目录,而且零留痕。这一侧必须响亮。
|
|
1373
|
-
*
|
|
1374
|
-
* **开**的那一侧则相反:`true` / `1` / `yes` 改前就落在「开」上,意图与结果一致,把它们改成拒启是**纯
|
|
1375
|
-
* 粹的新增故障**——一台按 `MEMORY_ENGINE=true` 跑了很久的部署会在升级时 boot 红,而它的配置从来没错过。
|
|
1376
|
-
* 所以两族都收进词表,只有**两族都不认得**的词才拒(那种词无从推断意图,静默倒向任何一侧都是猜)。
|
|
1377
|
-
*/
|
|
1378
816
|
const MEMORY_ENGINE_ON_WORDS = ["on", "true", "1", "yes", "enabled"];
|
|
1379
817
|
const MEMORY_ENGINE_OFF_WORDS = ["off", "false", "0", "no", "disabled"];
|
|
1380
818
|
function parseMemoryEngineWord(word) {
|
|
@@ -1387,17 +825,6 @@ function parseMemoryEngineWord(word) {
|
|
|
1387
825
|
`(engine ON, the default) or ${MEMORY_ENGINE_OFF_WORDS.join(" | ")} (engine OFF). ` +
|
|
1388
826
|
`An unrecognized value used to read as "on", so a misspelled kill-switch silently kept memory injection running.`);
|
|
1389
827
|
}
|
|
1390
|
-
/**
|
|
1391
|
-
* #217:`MEMORY_PERSISTENCE_CAPABLE` 的**三态**读取(core 5.26.0 `TaskSpec.memoryPersistenceCapable`)。
|
|
1392
|
-
*
|
|
1393
|
-
* 与 {@link parseMemoryEngineWord} 共用同一份双族词表(operator 面写法一致,别让同一台机器上的两个记忆
|
|
1394
|
-
* 旋钮各认一套词),但**多一个态**:缺席/空串 ⇒ `undefined` ⇒ TaskSpec 上整键缺席 ⇒ core 走自己的推断
|
|
1395
|
-
* (三态语义的 absent 腿,零迁移)。空串等同未设,与 MEMORY_ENGINE 同理由(compose 的 `${X:-}` 模板天天产它)。
|
|
1396
|
-
*
|
|
1397
|
-
* 不认得的词 **拒启**而不是回落:这个键是「本部署能不能替用户记住事情」的真值来源,静默倒向任何一侧都
|
|
1398
|
-
* 是猜——倒向 `true` 会让一台其实存不下的部署继续给幻觉式回执(#148 原病),倒向 `false` 会让一台真有
|
|
1399
|
-
* 自定义持久通道的部署对用户说假话。
|
|
1400
|
-
*/
|
|
1401
828
|
function parseMemoryPersistenceCapable(raw) {
|
|
1402
829
|
if (raw === undefined)
|
|
1403
830
|
return undefined;
|
|
@@ -1413,18 +840,14 @@ function parseMemoryPersistenceCapable(raw) {
|
|
|
1413
840
|
`\`# Memory\` write instruction on a remote execution lane) or ${MEMORY_ENGINE_OFF_WORDS.join(" | ")} (it CANNOT — ` +
|
|
1414
841
|
`forces the read-only disclosure). Leave it UNSET to let the engine infer from the mounted roster.`);
|
|
1415
842
|
}
|
|
1416
|
-
/** #210:SANDBOX_PKG_SOURCE 的闭集校验(词表 = README/MIGRATION 成文的四词)。`none` 由调用点的三元
|
|
1417
|
-
* 链先摘走(字段整个缺席),留在这里的只可能是前三词或一个手滑值。 */
|
|
1418
843
|
const SANDBOX_PKG_SOURCES = ["global", "cn", "custom", "none"];
|
|
1419
844
|
function pkgSourceEnv() {
|
|
1420
|
-
const v = env("SANDBOX_PKG_SOURCE", "global");
|
|
845
|
+
const v = env("SANDBOX_PKG_SOURCE", "global");
|
|
1421
846
|
if (!SANDBOX_PKG_SOURCES.includes(v)) {
|
|
1422
847
|
throw new Error(`env SANDBOX_PKG_SOURCE="${v}" must be one of: ${SANDBOX_PKG_SOURCES.join(", ")}`);
|
|
1423
848
|
}
|
|
1424
849
|
return v;
|
|
1425
850
|
}
|
|
1426
|
-
/** #210:TOC 同步腿的一条**条数上限**。未设 ⇒ 键缺席(core 默认,改前行为);设了就必须是 ≥1 的整数,
|
|
1427
|
-
* 否则 boot 拒 —— 「设了上限却静默丢弃」等价于「无上限」,方向与 operator 的意图相反。 */
|
|
1428
851
|
function syncEntryCap(name, field) {
|
|
1429
852
|
const raw = process.env[name];
|
|
1430
853
|
if (raw === undefined || raw === "")
|
|
@@ -1434,28 +857,11 @@ function syncEntryCap(name, field) {
|
|
|
1434
857
|
throw new Error(`env ${name}="${raw}" must be an integer >= 1 (it is an entries-per-batch cap; an ignored cap means NO cap)`);
|
|
1435
858
|
return { [field]: n };
|
|
1436
859
|
}
|
|
1437
|
-
/**
|
|
1438
|
-
* #219(黑板 [3617]):`SEMA_PARENT_PID` —— 引擎 parent 监视的**唯一**开关(壳侧 cli 已确认这个名字)。
|
|
1439
|
-
*
|
|
1440
|
-
* 三态,与 `syncEntryCap` 同族(#210 A 档「坏值拒启」):
|
|
1441
|
-
* · 未设 / 空串 ⇒ **键缺席**,整件不装配(opt-in;存量部署逐字零变化,零 warn);
|
|
1442
|
-
* · 正整数 ⇒ 原样进配置;
|
|
1443
|
-
* · 其余(0 / 负数 / 小数 / 非数字 / 带单位后缀)⇒ **拒启**并指路。
|
|
1444
|
-
*
|
|
1445
|
-
* 为什么坏值不静默回默认:本键的"默认"就是**整件不装配**,于是静默回默认 = operator 设了监视、进程
|
|
1446
|
-
* 照常起、监视根本不在 —— 恰恰是这个键要防的那类"孤儿引擎"在无人知晓的情况下继续发生。而且坏值的
|
|
1447
|
-
* 另一半后果更隐蔽:`0` 在 `process.kill(0, 0)` 里的语义是**当前进程组**(探活恒成功=监视永不触发),
|
|
1448
|
-
* 负数是**进程组** id(探到的根本不是父进程);这两个值不是"差一点对",是语义完全不同的另一件事。
|
|
1449
|
-
*/
|
|
1450
860
|
function parentPidEnv() {
|
|
1451
861
|
const raw = process.env.SEMA_PARENT_PID;
|
|
1452
862
|
if (raw === undefined || raw.trim() === "")
|
|
1453
863
|
return {};
|
|
1454
864
|
const n = Number(raw);
|
|
1455
|
-
// 上界 = int32 最大值:`process.kill` 的 pid 形参是 int32,`2147483648` 抛的是 `ERR_INVALID_ARG_TYPE`
|
|
1456
|
-
// 而**不是** ESRCH(codex 复审 M,实测证实)。落在探活的"未知 errno ⇒ 保守判活"支上,后果是:配置
|
|
1457
|
-
// 过了校验、`parent_watch_armed` 照打、之后每一拍都被当成"父还活着" —— 监视永久失效。校验面必须只
|
|
1458
|
-
// 放行 process.kill 真吃得下的范围,才对得起"设了就一定在监视"这句承诺。
|
|
1459
865
|
if (!Number.isInteger(n) || n < 1 || n > 2_147_483_647) {
|
|
1460
866
|
throw new Error(`env SEMA_PARENT_PID="${raw}" must be a POSITIVE INTEGER process id in [1, 2147483647] — it is the pid this ` +
|
|
1461
867
|
`engine watches (the shell/supervisor that spawned it); 0 and negatives mean "process GROUP" to ` +
|
|
@@ -1464,28 +870,7 @@ function parentPidEnv() {
|
|
|
1464
870
|
}
|
|
1465
871
|
return { parentPid: n };
|
|
1466
872
|
}
|
|
1467
|
-
/**
|
|
1468
|
-
* #228(黑板 [3590]/[3606] 两裁):`MEMORY_EMBEDDER_*` 族 —— 记忆检索向量面的**唯一**开关。
|
|
1469
|
-
*
|
|
1470
|
-
* 三态(与 `MEMORY_SYNC_*` 半配拒启同族):
|
|
1471
|
-
* · 三键全缺席 ⇒ `undefined`,不注入 embedder,现状零变化(检索恒 lexical 词面档);
|
|
1472
|
-
* · 半配(1~2 键)⇒ **拒启并点名缺哪几键**(REQUIRED_ENVS_BY_LANE 的报文形)。静默忽略的后果不是
|
|
1473
|
-
* 功能缺席,而是 operator 以为向量面开着——检索照常出结果,差异只在答案质量里,零日志;
|
|
1474
|
-
* · 坏值(URL 不合法 / 非 http(s) / DIM 非正整数 / TIMEOUT 非法或越界)⇒ 拒启带指路(#210 A 档)。
|
|
1475
|
-
*
|
|
1476
|
-
* 另两条门(都是「配了但永远不生效」的死旋钮形,一律拒启而非 warn):
|
|
1477
|
-
* · `MEMORY_ENGINE_BACKEND` 必须是 `pg` —— tidb 记忆后端 v1 无向量档(传 embedder 它自己 fail-loud
|
|
1478
|
-
* 拒),file 引擎没有 embedder 接缝;
|
|
1479
|
-
* · 记忆引擎本身必须开着(`MEMORY_ENGINE != off`)。
|
|
1480
|
-
*
|
|
1481
|
-
* 维度守卫的**位置**(诚实记档):本仓建 pg 记忆表时不传 `memoryVector`(见 boot/stores.ts 的
|
|
1482
|
-
* `ensurePgMemoryEngineSchema(q)`),即 embedding 列是 `jsonb`、schema 侧**没有可对表的维度常量**;
|
|
1483
|
-
* 所以 boot 期能做的只有「DIM 是正整数」,真正的维度守卫在写入路径两处:① 本仓 embedder 拿到长度
|
|
1484
|
-
* 不符的响应直接抛(绝不截断/补零);② `PgMemoryEngineBackend.embeddingParam` 对长度不符的向量返回
|
|
1485
|
-
* null(该行退回词面档,不写坏向量)。
|
|
1486
|
-
*/
|
|
1487
873
|
function parseMemoryEmbedder(ctx) {
|
|
1488
|
-
// 空串 = 未设(compose `${X:-}` 模板天天产它,本仓一律同判)
|
|
1489
874
|
const endpoint = process.env.MEMORY_EMBEDDER_ENDPOINT || undefined;
|
|
1490
875
|
const model = process.env.MEMORY_EMBEDDER_MODEL || undefined;
|
|
1491
876
|
const dimRaw = process.env.MEMORY_EMBEDDER_DIM || undefined;
|
|
@@ -1498,14 +883,11 @@ function parseMemoryEmbedder(ctx) {
|
|
|
1498
883
|
];
|
|
1499
884
|
const missing = trio.filter(([, v]) => v === undefined).map(([name]) => name);
|
|
1500
885
|
if (missing.length === 3) {
|
|
1501
|
-
// 三键全缺席 = 文档化的默认姿态(不注入)。但**从属**两键单独在场 = 同款半配陷阱:operator 调了
|
|
1502
|
-
// 超时/配了 key,却一个向量都不会算(MEMORY_SYNC_TOKEN/SCOPE 无 URL 对称拒启的同款先例)。
|
|
1503
886
|
if (timeoutRaw !== undefined || apiKey !== undefined) {
|
|
1504
887
|
throw new Error(`MEMORY_EMBEDDER_TIMEOUT_MS/MEMORY_EMBEDDER_API_KEY are set but the embedder itself is not configured — refusing to start half-configured (set MEMORY_EMBEDDER_ENDPOINT, MEMORY_EMBEDDER_MODEL and MEMORY_EMBEDDER_DIM to enable the memory vector plane, or unset the other MEMORY_EMBEDDER_* keys)`);
|
|
1505
888
|
}
|
|
1506
889
|
return undefined;
|
|
1507
890
|
}
|
|
1508
|
-
// 三键缺任一(TS 也靠这一支收窄,不需要非空断言)
|
|
1509
891
|
if (endpoint === undefined || model === undefined || dimRaw === undefined) {
|
|
1510
892
|
throw new Error(`MEMORY_EMBEDDER_* is half-configured — missing ${missing.join(", ")}. The memory vector plane needs all three of MEMORY_EMBEDDER_ENDPOINT, MEMORY_EMBEDDER_MODEL, MEMORY_EMBEDDER_DIM; refusing to start rather than silently running lexical-only retrieval while the deployment believes vectors are on (#228).`);
|
|
1511
893
|
}
|
|
@@ -1517,9 +899,6 @@ function parseMemoryEmbedder(ctx) {
|
|
|
1517
899
|
if (proto !== "http:" && proto !== "https:") {
|
|
1518
900
|
throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${endpoint}" must be http(s) (got "${proto}") — the embedder leg is an HTTP POST to an OpenAI-compatible /embeddings face`);
|
|
1519
901
|
}
|
|
1520
|
-
// URL 里的 userinfo 一律拒(codex 二轮 F1):`https://user:pass@host/v1` 形把凭据种进一个会被
|
|
1521
|
-
// 传输层错误、日志、PatchReport 冲突文案反复复制的字符串里 —— 脱敏只能事后擦,拒收才是源头。
|
|
1522
|
-
// 需要 auth 就走 MEMORY_EMBEDDER_API_KEY(Bearer 头,不进 URL)。
|
|
1523
902
|
if (parsedEndpoint.username !== "" || parsedEndpoint.password !== "") {
|
|
1524
903
|
throw new Error("MEMORY_EMBEDDER_ENDPOINT must not carry credentials in the URL (user:password@host) — URL userinfo leaks into transport errors, logs and memory conflict reports. Use MEMORY_EMBEDDER_API_KEY (sent as a Bearer header) instead.");
|
|
1525
904
|
}
|
|
@@ -1537,48 +916,19 @@ function parseMemoryEmbedder(ctx) {
|
|
|
1537
916
|
endpoint,
|
|
1538
917
|
model,
|
|
1539
918
|
dimensions,
|
|
1540
|
-
// 活性保护:挂死的 embedder 会把整条记忆写入路径吊住(undici 默认 headers 超时 300s 太钝)。
|
|
1541
|
-
// 下界 1s(比这更短的向量调用只会全轮超时);上界 5min(再长就不是超时保护了)。
|
|
1542
919
|
timeoutMs: numEnvBounded("MEMORY_EMBEDDER_TIMEOUT_MS", "30000", 1000, 300_000),
|
|
1543
920
|
...(apiKey !== undefined ? { apiKey } : {}),
|
|
1544
921
|
};
|
|
1545
922
|
}
|
|
1546
|
-
/** 域:memory(记忆面 + TOC 同步腿)—— 记忆引擎开关/方言/根目录、scope 缺省、同步 client 三键、项目记忆。 */
|
|
1547
923
|
function parseMemoryDomain(ctx) {
|
|
1548
|
-
const { requirePrincipal } = ctx;
|
|
1549
|
-
// design/138 S1 (clay 2026-07-08): long-term memory = the injection-first file-based memory ENGINE
|
|
1550
|
-
// (core RunnerDeps.memoryBackend), default ON with MEMORY_ENGINE=off as the explicit kill-switch.
|
|
1551
|
-
// The legacy MEMORY_BACKEND/EMBEDDING_* store plane was dropped without migration (those env vars are
|
|
1552
|
-
// no longer read). Single-user gating lives in main.ts (the engine is never wired multi-tenant).
|
|
1553
|
-
// #210 A 档(黑板 [3425]③):判据从 `!== "off"` 改**闭集**(姿势仿下面的 MEMORY_ENGINE_BACKEND)。
|
|
1554
|
-
// 旧判据的病:任何非 `off` 的字面量都读成「开」—— `MEMORY_ENGINE=OFF` / `=false` / `=disabled` 是
|
|
1555
|
-
// operator 明确表达「关掉记忆面」的三种最常见写法,每一种都让引擎**照常开着**并继续往任务里注入记忆
|
|
1556
|
-
// 目录。这是「拼错的关词 = 静默不关」,与安全轴的词表纪律同族,所以拒启而不是 warn:一个自以为关了
|
|
1557
|
-
// 记忆的部署,和一个明知开着的部署,不是同一个隐私姿态。
|
|
1558
|
-
// ⚠️ 走 `env()` 而不是 `enumEnv()`:后者拿 `??` 取值,会把**空串**当成一个要比对词表的值 —— 而空串
|
|
1559
|
-
// 在本仓一律等同未设(compose 的 `${MEMORY_ENGINE:-}` 模板天天产它),改前 `"" !== "off"` 也确实读成
|
|
1560
|
-
// 「开」。词表化不该顺手把空串变成拒启。
|
|
924
|
+
const { requirePrincipal } = ctx;
|
|
1561
925
|
const memoryEngineEnabled = parseMemoryEngineWord(env("MEMORY_ENGINE", "on"));
|
|
1562
|
-
// S3-TOB(边界重切后的 backend 选择,设计 §1.3):记忆持久面方言。缺省 file=现状零变化;
|
|
1563
|
-
// pg/tidb=DB 真身(零卷主档)——多租户点亮的唯一门(显式 opt-in,复审 F5:绝不静默翻转)。
|
|
1564
926
|
const memoryEngineBackendRaw = process.env.MEMORY_ENGINE_BACKEND ?? "file";
|
|
1565
927
|
if (!["file", "pg", "tidb"].includes(memoryEngineBackendRaw)) {
|
|
1566
928
|
throw new Error(`MEMORY_ENGINE_BACKEND must be file|pg|tidb, got "${memoryEngineBackendRaw}"`);
|
|
1567
929
|
}
|
|
1568
930
|
const memoryEngineBackend = memoryEngineBackendRaw;
|
|
1569
|
-
// Single-user memory scope (hoisted from the config literal so the sync-scope default below can consume it):
|
|
1570
|
-
// explicit MEMORY_SCOPE always wins; default "local" only when the engine is on AND single-user (see the
|
|
1571
|
-
// literal-site comment on why multi-tenant/engine-off must stay undefined = memory dark).
|
|
1572
931
|
const memoryScope = process.env.MEMORY_SCOPE ?? (memoryEngineEnabled && !requirePrincipal ? "local" : undefined);
|
|
1573
|
-
// 142-S2.5-W1: TOC 同步 client 腿的三键(file memory 形态专用)。MEMORY_SYNC_URL 设了=开;
|
|
1574
|
-
// 缺省 undefined=纯本地现状零变化。半配置 fail-loud(memoryEngineBackend 同款姿势,绝不静默降级):
|
|
1575
|
-
// - URL 有 TOKEN 无 ⇒ throw(中心同步面要 Bearer,缺 token 的“开了但永远 401”是半配置陷阱);
|
|
1576
|
-
// - TOKEN/SCOPE 有 URL 无 ⇒ throw(operator 显然想开同步,静默不跑=同款陷阱,对称拒启);
|
|
1577
|
-
// - DB backend(pg/tidb)配了任意同步键 ⇒ throw(中心侧自己就是 POST /v1/memory/sync/:scope 的权威,
|
|
1578
|
-
// 不该再跑 client 腿——两个半场叠在一个进程是拓扑错误)。
|
|
1579
|
-
// scope 缺省 = formatUserScope(memoryScope)(中心侧 owner 门的 user 盘键铸法);memoryScope 本身已是
|
|
1580
|
-
// v2 typed key(user:/org:/proj:/userproj:,只可能来自 operator 显式 MEMORY_SCOPE)则原样直通——
|
|
1581
|
-
// 再包一层会铸出 `user:user%3A...` 双包键(诚实偏离,报告记档)。
|
|
1582
932
|
const memorySyncUrl = process.env.MEMORY_SYNC_URL || undefined;
|
|
1583
933
|
const memorySyncToken = process.env.MEMORY_SYNC_TOKEN || undefined;
|
|
1584
934
|
const memorySyncScopeRaw = process.env.MEMORY_SYNC_SCOPE || undefined;
|
|
@@ -1595,17 +945,10 @@ function parseMemoryDomain(ctx) {
|
|
|
1595
945
|
if (memorySyncToken === undefined) {
|
|
1596
946
|
throw new Error("MEMORY_SYNC_URL is set but MEMORY_SYNC_TOKEN is not — refusing to start half-configured (the central sync face requires a bearer token)");
|
|
1597
947
|
}
|
|
1598
|
-
// A-002.4:前缀判据的**唯一属主**是 memory-scope.ts(此处曾有一份逐字副本;两处咬合用途不同,
|
|
1599
|
-
// 但 core 加第五种 typed 前缀时必须一起动,否则同一个键在「挂 v2 标」和「算不算已是 v2 键」两处
|
|
1600
|
-
// 得出相反答案 ⇒ 这里会把它再包成 `user:user%3A…` 双包键)。
|
|
1601
948
|
const scope = memorySyncScopeRaw ?? (memoryScope !== undefined ? (isV2ScopeKey(memoryScope) ? memoryScope : formatUserScope(memoryScope)) : undefined);
|
|
1602
949
|
if (scope === undefined) {
|
|
1603
950
|
throw new Error("MEMORY_SYNC_URL is set but no sync scope is derivable (memory engine off or multi-tenant without an explicit scope) — set MEMORY_SYNC_SCOPE, or run the single-user file memory engine so MEMORY_SCOPE (default \"local\") provides one");
|
|
1604
951
|
}
|
|
1605
|
-
// #210 A 档:两条上限改**拒启**(与本块自己的「半配置 fail-loud」纪律同源)。旧形是
|
|
1606
|
-
// `Number.isInteger(v) && v>=1` 的三态过滤——不满足就**整个键消失**,而键消失 = core 走自己的默认
|
|
1607
|
-
// = 「没有这条上限」。也就是说 operator 设了一条**收紧**用的上限、结果得到的是最松的那一档,而且
|
|
1608
|
-
// 三种手滑(单位后缀 / 小数 / 0)都落到同一个静默出口。
|
|
1609
952
|
memorySync = {
|
|
1610
953
|
url: memorySyncUrl,
|
|
1611
954
|
token: memorySyncToken,
|
|
@@ -1614,57 +957,27 @@ function parseMemoryDomain(ctx) {
|
|
|
1614
957
|
...syncEntryCap("MEMORY_SYNC_MAX_PULL_ENTRIES", "maxPullEntries"),
|
|
1615
958
|
};
|
|
1616
959
|
}
|
|
1617
|
-
// design/158 B4 ② 的 D 家族四枚里,唯一落在 ServiceConfig 上的就是这枚(其余三枚只在使用点懒读,
|
|
1618
|
-
// 预登记留在 orchestration 域 —— 顺序与改前一致:本枚的弃用通知仍先于那三枚)。
|
|
1619
960
|
const projectMemoryEnabled = boolEnvWithLegacyNegated("PROJECT_MEMORY_ENABLED", "PROJECT_MEMORY_DISABLED", true);
|
|
1620
|
-
// design/170 件A(clay 三裁 [2687]):org 记忆准入四旋钮。模式=enumEnv 二值非布尔(N9),默认
|
|
1621
|
-
// enforce(裁2:audit-first 灰度步免除,audit 降级为运维诊断位);TTL/退避窗=numEnvBounded 启动期
|
|
1622
|
-
// 报错(env-numeric-fail-loud 门,F6:optFinitePositiveEnv 的静默回默认恰是要消灭的形)。单机目录
|
|
1623
|
-
// env 表在这里就整段校验(fail-loud 越早越好——一张半坏的授权表比拒绝启动更危险),config 上只存
|
|
1624
|
-
// 原文(纯数据;Map 在装配点重建)。
|
|
1625
961
|
const memoryOrgAdmissionMode = enumEnv("MEMORY_ORG_ADMISSION_MODE", "enforce", ["audit", "enforce"]);
|
|
1626
|
-
// 原文存 config(纯数据);整段 fail-loud 校验(parseOrgDirectoryStatic)在装配点——它传递值引
|
|
1627
|
-
// security.js(assertPrincipalShape),而 base config 层禁值引 auth 模块(design/158 A4 分层),
|
|
1628
|
-
// 校验不能放这里。装配点仍在启动期,坏表照样拒启动。
|
|
1629
962
|
const memoryOrgDirectoryJson = process.env.MEMORY_ORG_DIRECTORY_JSON || undefined;
|
|
1630
963
|
const memoryOrgGrantTtlMs = numEnvBounded("MEMORY_ORG_GRANT_TTL_MS", "60000", 1000, 3_600_000);
|
|
1631
964
|
const memoryOrgUnavailableBackoffMs = numEnvBounded("MEMORY_ORG_UNAVAILABLE_BACKOFF_MS", "10000", 500, 600_000);
|
|
1632
965
|
return {
|
|
1633
966
|
memoryEngineEnabled,
|
|
1634
967
|
memoryEngineDir: process.env.MEMORY_ENGINE_DIR || undefined,
|
|
1635
|
-
memoryEngineRemoteLaneAllowed: process.env.MEMORY_ENGINE_REMOTE_LANE === "allow",
|
|
1636
|
-
// A single-user deployment has no principal, so `memoryScopeFor` falls back to `config.memoryScope` — and if
|
|
1637
|
-
// THAT were unset the resolved scope would be undefined ⇒ resolveSpec builds NO `spec.memory` ⇒ the memory
|
|
1638
|
-
// engine never materializes (the "wired-not-triggered" gap a §4 real test hit on the legacy plane).
|
|
1639
|
-
// Default it to "local" for single-user (REQUIRE_PRINCIPAL !== "true") so TOC local memory works out-of-the-box
|
|
1640
|
-
// (CC-parity). Multi-tenant memory is DARK by design (design/138 S1: the file basement has no tenant isolation —
|
|
1641
|
-
// memoryScopeFor returns undefined there). An explicit MEMORY_SCOPE always wins.
|
|
968
|
+
memoryEngineRemoteLaneAllowed: process.env.MEMORY_ENGINE_REMOTE_LANE === "allow",
|
|
1642
969
|
memoryEngineBackend,
|
|
1643
|
-
memoryScope,
|
|
1644
|
-
// #217:三态 ⇒ 只有真表态才上键(缺席腿必须是**整键缺席**,不是 present-as-undefined —— 后者会被
|
|
1645
|
-
// resolve-spec 原样搬上 TaskSpec,把 core 的「absent = 各自推断」读成一个显式表态)。
|
|
970
|
+
memoryScope,
|
|
1646
971
|
...((v) => (v !== undefined ? { memoryPersistenceCapable: v } : {}))(parseMemoryPersistenceCapable(process.env.MEMORY_PERSISTENCE_CAPABLE)),
|
|
1647
972
|
...(memorySync ? { memorySync } : {}),
|
|
1648
|
-
// #228:向量面接线(三态解析 + 后端/引擎两门,全在 parseMemoryEmbedder 里 fail-loud)
|
|
1649
973
|
...((v) => (v !== undefined ? { memoryEmbedder: v } : {}))(parseMemoryEmbedder({ memoryEngineBackend, memoryEngineEnabled })),
|
|
1650
974
|
memoryOrgAdmissionMode,
|
|
1651
|
-
// #299(core 5.45.0 design/324):委派臂的证据标准。**缺席 = 整键缺席**(不是 present-as-undefined,
|
|
1652
|
-
// 也不是折成 core 的缺省词)—— 本仓不复制上游缺省,上游改缺省之日起这里不会成为第二份真源
|
|
1653
|
-
// (`READ_FACE` 同姿态,#246)。设了就必须是闭集两词之一:坏词在 `enumEnv` 里响亮拒(#210),
|
|
1654
|
-
// 而不是等到 core 的 prepare 门上才炸 —— 部署配错应当在**启动期**被点名,不是第一个任务失败时。
|
|
1655
975
|
...(process.env.MEMORY_DELEGATION_EVIDENCE !== undefined
|
|
1656
976
|
? { memoryDelegationEvidence: enumEnv("MEMORY_DELEGATION_EVIDENCE", "static-face", ["static-face", "attested-only"]) }
|
|
1657
977
|
: {}),
|
|
1658
|
-
// #307 件4(core 5.46.0 design/336 §13-3):provenance 总开关。与上一条**逐字同姿态** ——
|
|
1659
|
-
// 缺席=整键缺席(不复制上游缺省),设了就必须是闭集两词之一。坏值在 `enumEnv` 里**启动期**响亮拒:
|
|
1660
|
-
// `false`/`none`/`disabled` 这类"看起来像关掉"的写法若被静默折成缺省,一台自认为「暴露会话零收录」
|
|
1661
|
-
// 的部署其实在照常收录 —— 部署级旋钮坏值禁静默回默认(#210 + operator-knob-must-be-unconditional)。
|
|
1662
978
|
...(process.env.MEMORY_PROVENANCE !== undefined
|
|
1663
979
|
? { memoryProvenance: enumEnv("MEMORY_PROVENANCE", "carry", ["off", "carry"]) }
|
|
1664
980
|
: {}),
|
|
1665
|
-
// debt #322-B(core 5.48 C18):委派入口 caps。两 env 各自独立可设;设了=numEnvBounded 启动期
|
|
1666
|
-
// 响亮拒坏值(#210),缺席=键不铸(core 自缺省 CC parity 20/200,不复制上游缺省)。pair 合法性
|
|
1667
|
-
// (concurrent ≤ cumulative)归 core prepare 的 `config.delegation_entry_caps` 门,单判官不复刻。
|
|
1668
981
|
...(process.env.DELEGATION_MAX_CONCURRENT !== undefined || process.env.DELEGATION_MAX_PER_SESSION !== undefined
|
|
1669
982
|
? {
|
|
1670
983
|
delegationEntryCaps: {
|
|
@@ -1676,39 +989,16 @@ function parseMemoryDomain(ctx) {
|
|
|
1676
989
|
...(memoryOrgDirectoryJson !== undefined ? { memoryOrgDirectoryJson } : {}),
|
|
1677
990
|
memoryOrgGrantTtlMs,
|
|
1678
991
|
memoryOrgUnavailableBackoffMs,
|
|
1679
|
-
projectMemoryEnabled,
|
|
1680
|
-
syncImportLeaseStaleSec: clampEnvWithWarn("SYNC_IMPORT_LEASE_STALE_SEC", "600", 0),
|
|
992
|
+
projectMemoryEnabled,
|
|
993
|
+
syncImportLeaseStaleSec: clampEnvWithWarn("SYNC_IMPORT_LEASE_STALE_SEC", "600", 0),
|
|
1681
994
|
};
|
|
1682
995
|
}
|
|
1683
|
-
/**
|
|
1684
|
-
* design/170 件B/C/D —— 三条**部署治理声明**的 env 解析(auth 域的一段,fail-loud 全在这里)。
|
|
1685
|
-
*
|
|
1686
|
-
* 三条的词表真源都在 core(`ComplianceProfile` / `ComplianceCapability` / `LOCKED_KEY_REGISTRY`),本函数
|
|
1687
|
-
* 只负责「env 文本 → 闭集值」这一步,并且**每一步坏值都拒启**:这三根旋钮全在安全/治理轴上,一个静默
|
|
1688
|
-
* 不生效的档位或一个拼错的锁键,是「以为在管、其实没管」——比没配更贵(#210 A 档 + design/170 §3.4)。
|
|
1689
|
-
*
|
|
1690
|
-
* 半配也拒:`COMPLIANCE_ADDITIONAL_DENIES` 没有 profile 可收紧时是一句无处安放的话,静默丢掉它等于
|
|
1691
|
-
* 让 operator 以为多禁了一条(与 `MEMORY_EMBEDDER_*` 的半配拒启同族)。
|
|
1692
|
-
*/
|
|
1693
|
-
/** #270 车2 —— sweep 节律的上界(秒)。`setInterval` 的 delay 是 32 位有符号毫秒(2^31-1);越界会被
|
|
1694
|
-
* Node **重置成 1ms**,理由与拒启判据见调用点。导出供判据与文档单点引用。 */
|
|
1695
996
|
export const RETENTION_SWEEP_INTERVAL_MAX_SEC = Math.floor((2 ** 31 - 1) / 1000);
|
|
1696
997
|
function parseGovernanceDeclarations() {
|
|
1697
998
|
const out = { retentionSweep: { mode: "audit-only", intervalSec: 0 } };
|
|
1698
|
-
/**
|
|
1699
|
-
* codex 对抗复审 F1(high,验真后采纳)——**「未定义」与「定义了但是空」必须分开**。
|
|
1700
|
-
*
|
|
1701
|
-
* 病:模板化部署(k8s manifest、compose 的 `env_file`、`KEY=$(VAR)` 替换)极易产出一个**定义了但值为
|
|
1702
|
-
* 空**的变量。若照本仓其它 env 的惯例把它折成「未设」,一个明明写下了档位/锁声明的部署会**静默**以
|
|
1703
|
-
* 零档位零锁启动 —— 治理面静默失效正是这一族旋钮存在的理由,它不能自己犯这个病。
|
|
1704
|
-
*
|
|
1705
|
-
* 判据因此是:**未定义 = 文档化的缺省姿态(零 warn,#210「缺席不在律内」);已定义但解析不出内容 =
|
|
1706
|
-
* 配错了 ⇒ 拒启 + 指路「要缺省就 unset,别留空」。** 迁移代价为零:三根都是本批新增的旋钮,现存部署
|
|
1707
|
-
* 一个都没设过,所以这条更严的判据不会打到任何在跑的部署。
|
|
1708
|
-
*/
|
|
1709
999
|
const requireNonBlank = (name, raw) => {
|
|
1710
1000
|
if (raw === undefined)
|
|
1711
|
-
return undefined;
|
|
1001
|
+
return undefined;
|
|
1712
1002
|
const trimmed = raw.trim();
|
|
1713
1003
|
if (trimmed === "" || trimmed.split(",").every((t) => t.trim() === "")) {
|
|
1714
1004
|
throw new Error(`env ${name} is set but empty (or has no parseable value) — a governance declaration that arms nothing must not boot silently. ` +
|
|
@@ -1716,7 +1006,6 @@ function parseGovernanceDeclarations() {
|
|
|
1716
1006
|
}
|
|
1717
1007
|
return trimmed;
|
|
1718
1008
|
};
|
|
1719
|
-
// 件B:合规档位。profile 闭集由 core 的内建 deny 表的键集给出(词表单一真源,不在这里抄一份)。
|
|
1720
1009
|
const profileRaw = requireNonBlank("COMPLIANCE_PROFILE", process.env.COMPLIANCE_PROFILE) ?? "";
|
|
1721
1010
|
requireNonBlank("COMPLIANCE_ADDITIONAL_DENIES", process.env.COMPLIANCE_ADDITIONAL_DENIES);
|
|
1722
1011
|
const denyRaw = csv("COMPLIANCE_ADDITIONAL_DENIES");
|
|
@@ -1735,24 +1024,16 @@ function parseGovernanceDeclarations() {
|
|
|
1735
1024
|
else if (denyRaw.length > 0) {
|
|
1736
1025
|
throw new Error("COMPLIANCE_ADDITIONAL_DENIES is set but COMPLIANCE_PROFILE is not — additional denies only ever TIGHTEN a profile's builtin floor, so half of this configuration cannot be applied");
|
|
1737
1026
|
}
|
|
1738
|
-
// 件C:锁定层声明。校验走 core 的 `resolveLockedKeys`(注册表是它的,未知键抛 config.locked_registry)。
|
|
1739
1027
|
requireNonBlank("LOCKED_CONFIG_KEYS", process.env.LOCKED_CONFIG_KEYS);
|
|
1740
1028
|
const lockedRaw = csv("LOCKED_CONFIG_KEYS");
|
|
1741
1029
|
if (lockedRaw.length > 0) {
|
|
1742
|
-
resolveLockedKeys({ keys: lockedRaw });
|
|
1030
|
+
resolveLockedKeys({ keys: lockedRaw });
|
|
1743
1031
|
out.lockedConfigKeys = lockedRaw;
|
|
1744
1032
|
}
|
|
1745
|
-
// 件D:留存期。删数据的旋钮 ⇒ A 档拒启(`0` 合法:立即可删,是可评估的显式表达)。
|
|
1746
|
-
// ⚠️ 空串必须走上面那道门:`Number("")` 是 **0**(有限!)—— 折成「立即可删」比折成「未设」更坏。
|
|
1747
1033
|
const retentionRaw = requireNonBlank("RETENTION_MAX_AGE_DAYS", process.env.RETENTION_MAX_AGE_DAYS);
|
|
1748
1034
|
if (retentionRaw !== undefined) {
|
|
1749
1035
|
out.retentionPolicy = { maxAgeDays: parseNumOrFailNonNegative("RETENTION_MAX_AGE_DAYS", retentionRaw) };
|
|
1750
1036
|
}
|
|
1751
|
-
// ── #270 车2:执行面的两根旋钮(策略说"保留多久",这两根说"谁在什么节律上按它删")。
|
|
1752
|
-
//
|
|
1753
|
-
// 🔴 灰度档是**闭集**,拼错即拒(#157 词表纪律)。静默落回 `audit-only` 会让运维以为在删而其实没删;
|
|
1754
|
-
// 静默落成另一档就是反方向那件更坏的事。词表从 config-types 的 `RETENTION_MODES` 单点读,
|
|
1755
|
-
// 能力位/审计行的 `mode` 列与它同源。
|
|
1756
1037
|
const modeRaw = requireNonBlank("RETENTION_MODE", process.env.RETENTION_MODE);
|
|
1757
1038
|
if (modeRaw !== undefined) {
|
|
1758
1039
|
if (!RETENTION_MODES.includes(modeRaw)) {
|
|
@@ -1762,21 +1043,12 @@ function parseGovernanceDeclarations() {
|
|
|
1762
1043
|
}
|
|
1763
1044
|
out.retentionSweep.mode = modeRaw;
|
|
1764
1045
|
}
|
|
1765
|
-
// 🔴 节律必须是**非负整数秒**:`Math.round` 掉一个小数会把"配错了"变成"配得有点怪但跑起来了",而
|
|
1766
|
-
// 这条腿是每小时一拍的后台删除活 —— `0.5` 只可能是把毫秒/分钟当成秒写的手滑。`0` 是显式关(默认)。
|
|
1767
1046
|
const sweepRaw = requireNonBlank("RETENTION_SWEEP_INTERVAL_SEC", process.env.RETENTION_SWEEP_INTERVAL_SEC);
|
|
1768
1047
|
if (sweepRaw !== undefined) {
|
|
1769
1048
|
const n = parseNumOrFailNonNegative("RETENTION_SWEEP_INTERVAL_SEC", sweepRaw);
|
|
1770
1049
|
if (!Number.isInteger(n)) {
|
|
1771
1050
|
throw new Error(`env RETENTION_SWEEP_INTERVAL_SEC=${n} must be a whole number of seconds (0 = the lane is off)`);
|
|
1772
1051
|
}
|
|
1773
|
-
// 🔴 上界 = `setInterval` 的 32 位 delay 上限(codex 对抗复审 R1-[high],验真属实)。
|
|
1774
|
-
// 病:delay 超过 2^31-1 ms 时 Node 打一条 `TimeoutOverflowWarning` 并把它**重置成 1ms** —— 一个想
|
|
1775
|
-
// 「约 25 天扫一次」的 enforce 部署于是变成几乎连续扫库,而这条腿每一拍发的都是跨十余张表的
|
|
1776
|
-
// **破坏性**事务。夹取在这里同样是错的补偿(悄悄改小 = 换一个运维没要的策略),拒启才是
|
|
1777
|
-
// ——与 `RETENTION_MAX_AGE_DAYS` 那根「删数据的旋钮不许 NaN 流通」同一条判据。
|
|
1778
|
-
// 顺带这条上界也保住了 TTL 算术:`intervalSec*1000*RETENTION_LEASE_TTL_FACTOR + Date.now()` 在
|
|
1779
|
-
// 本上界内离 `Number.MAX_SAFE_INTEGER` 还差九个数量级。
|
|
1780
1052
|
if (n > RETENTION_SWEEP_INTERVAL_MAX_SEC) {
|
|
1781
1053
|
throw new Error(`env RETENTION_SWEEP_INTERVAL_SEC=${n} exceeds ${RETENTION_SWEEP_INTERVAL_MAX_SEC} (setInterval's 32-bit delay ceiling, ~24.8 days) — ` +
|
|
1782
1054
|
`Node would silently RESET an overflowing delay to 1ms, turning an "occasional" retention sweep into a nearly continuous stream of ` +
|
|
@@ -1786,22 +1058,12 @@ function parseGovernanceDeclarations() {
|
|
|
1786
1058
|
}
|
|
1787
1059
|
return out;
|
|
1788
1060
|
}
|
|
1789
|
-
/** 域:auth(身份/凭证/绑址)—— service token 面、CORS、principal 头与开关、JWT 锚、autonomy、绑址。 */
|
|
1790
1061
|
function parseAuthDomain(ctx) {
|
|
1791
|
-
const { requirePrincipal } = ctx;
|
|
1792
|
-
// design/80 D-G 直连门的 principal-JWT 锚:验签材料属 auth 域,门的激活与半开门不变量在装配层。
|
|
1062
|
+
const { requirePrincipal } = ctx;
|
|
1793
1063
|
const principalJwtPubkeys = parsePrincipalJwks(process.env.PRINCIPAL_JWT_PUBKEYS, announceKeySetRejection("PRINCIPAL_JWT_PUBKEYS"));
|
|
1794
1064
|
const principalJwtIss = process.env.PRINCIPAL_JWT_ISS || undefined;
|
|
1795
1065
|
const principalJwtAud = process.env.PRINCIPAL_JWT_AUD || undefined;
|
|
1796
|
-
|
|
1797
|
-
// `0` 在这条上是「无上限」的**显式表达**,于是 `Math.max(0, -1)` 把一个想收紧到「负数=全拒」的手滑
|
|
1798
|
-
// 值,悄悄翻译成了整条上限里**最松**的那一档。收紧意图落成最松结果 ⇒ 夹取在这里是错的补偿,拒启才是。
|
|
1799
|
-
// 复用既有原语 `parseNumOrFailNonNegative`(而不是 numEnvBounded 配一个假上界):它说的正是这条旋钮要
|
|
1800
|
-
// 的判据「非负」,报错文案也直说;凭空塞一个上帽是 R2-F2 那一类的同族错。
|
|
1801
|
-
const principalJwtMaxTtlSec = parseNumOrFailNonNegative("PRINCIPAL_JWT_MAX_TTL_SEC", env("PRINCIPAL_JWT_MAX_TTL_SEC", "120")); // 0 = no cap; default ≤120s
|
|
1802
|
-
// Per-system service credentials (token → system name). Parsed as a DOMAIN field (not inline in the return
|
|
1803
|
-
// literal) so the bake boot invariant at the assembly layer can assert the runner-credential→runnerPrincipal
|
|
1804
|
-
// mapping exists.
|
|
1066
|
+
const principalJwtMaxTtlSec = parseNumOrFailNonNegative("PRINCIPAL_JWT_MAX_TTL_SEC", env("PRINCIPAL_JWT_MAX_TTL_SEC", "120"));
|
|
1805
1067
|
const authTokens = Object.fromEntries((process.env.SERVICE_AUTH_TOKENS ?? "")
|
|
1806
1068
|
.split(",")
|
|
1807
1069
|
.map((s) => s.trim())
|
|
@@ -1809,8 +1071,6 @@ function parseAuthDomain(ctx) {
|
|
|
1809
1071
|
.map((s) => [s.slice(0, s.indexOf("=")).trim(), s.slice(s.indexOf("=") + 1).trim()])
|
|
1810
1072
|
.filter(([tok, sys]) => tok.length > 0 && sys.length > 0));
|
|
1811
1073
|
return {
|
|
1812
|
-
// [2354]:HOST 读取臂已删(zsh 恒设 HOST=机器名,不可墓碑——静默不读;cli/桌面 [2053] 起已传
|
|
1813
|
-
// BIND_HOST)。绑址唯一口=BIND_HOST;空串=未设。
|
|
1814
1074
|
bindHost: process.env.BIND_HOST || undefined,
|
|
1815
1075
|
bindHostSource: process.env.BIND_HOST ? "BIND_HOST" : undefined,
|
|
1816
1076
|
authToken: process.env.SERVICE_AUTH_TOKEN,
|
|
@@ -1819,11 +1079,7 @@ function parseAuthDomain(ctx) {
|
|
|
1819
1079
|
corsOrigins: (process.env.CORS_ORIGIN ?? "").split(",").map((s) => s.trim()).filter(Boolean),
|
|
1820
1080
|
principalHeader: headerNameEnv("PRINCIPAL_HEADER", "x-agent-principal"),
|
|
1821
1081
|
requirePrincipal,
|
|
1822
|
-
// Runtime governance second baton (center §10): AUTONOMY env for the env/local path; sema-registry overlays
|
|
1823
|
-
// it (applyRuntimeHot). commandPolicy is structured config — sema-registry-only (no flat env scalar form).
|
|
1824
1082
|
autonomy: parseAutonomy(process.env.AUTONOMY),
|
|
1825
|
-
// design/170 件B/C/D:三条部署治理声明(解析与 fail-loud 全在 parseGovernanceDeclarations;
|
|
1826
|
-
// 三键都是可选的,未配 = 键不在 = 现存部署逐字零影响)。
|
|
1827
1083
|
...parseGovernanceDeclarations(),
|
|
1828
1084
|
operatorPrincipals: csv("OPERATOR_PRINCIPALS"),
|
|
1829
1085
|
principalJwtPubkeys,
|
|
@@ -1832,53 +1088,25 @@ function parseAuthDomain(ctx) {
|
|
|
1832
1088
|
principalJwtMaxTtlSec,
|
|
1833
1089
|
};
|
|
1834
1090
|
}
|
|
1835
|
-
/** 域:orchestration(编排 + 执行车道)—— remoteExec 五车道/worktree 隔离、leader/router/scheduler/fork、
|
|
1836
|
-
* workflow 与后台 agent 的存留旋钮、沙箱面(pkg 源/env facts/LSP)、烤镜像门。 */
|
|
1837
1091
|
function parseOrchestrationDomain(ctx) {
|
|
1838
|
-
const { postureOn } = ctx;
|
|
1839
|
-
// workflowSizeGuideline: CC normalizes unknown→unrestricted (lenient), but an env knob typo
|
|
1840
|
-
// silently degrading to "no guidance" is the half-config trap — fail-loud here (memoryEngineBackend
|
|
1841
|
-
// posture; the ADVISORY nature is core's concern, the env spelling is ours).
|
|
1092
|
+
const { postureOn } = ctx;
|
|
1842
1093
|
const workflowSizeGuidelineRaw = process.env.WORKFLOW_SIZE_GUIDELINE;
|
|
1843
1094
|
if (workflowSizeGuidelineRaw !== undefined && !["small", "medium", "large", "unrestricted"].includes(workflowSizeGuidelineRaw)) {
|
|
1844
1095
|
throw new Error(`WORKFLOW_SIZE_GUIDELINE must be small|medium|large|unrestricted, got "${workflowSizeGuidelineRaw}"`);
|
|
1845
1096
|
}
|
|
1846
1097
|
const workflowSizeGuideline = workflowSizeGuidelineRaw;
|
|
1847
|
-
// design/166-T1:治理窗(fail-loud 解析;unset=undefined=关)。
|
|
1848
1098
|
const usageWindows = parseUsageWindows(process.env.USAGE_WINDOWS);
|
|
1849
1099
|
const bakeRunnerPrincipal = env("BAKE_RUNNER_PRINCIPAL", "system:bake-runner");
|
|
1850
|
-
// ── design/158 B4 ② — the four knobs whose canonical name went from negative to positive. Only the first
|
|
1851
|
-
// lands on `ServiceConfig`; the other three are read lazily at their use sites (main.ts's LKG gate,
|
|
1852
|
-
// remote-env-host's capability probe and exec path) but are resolved HERE too so that their polarity row and
|
|
1853
|
-
// any legacy-alias deprecation notice are emitted once, at boot, with every other config diagnostic — an
|
|
1854
|
-
// operator must not have to trigger a background shell before being told the name they set is retired.
|
|
1855
|
-
// (家族的第一枚 PROJECT_MEMORY_ENABLED 是 memory 域的字段,已在那里解析 —— 通知顺序不变。)
|
|
1856
1100
|
configLkgEnabled();
|
|
1857
1101
|
hostBackgroundShellEnabled();
|
|
1858
1102
|
hostExecSpoolEnabled();
|
|
1859
|
-
// ── design/158 B4 ③ — LSP: one knob per lane. `LSP_ENABLED` used to drive BOTH the sandbox lane (opt-in,
|
|
1860
|
-
// default OFF — it needs a baked `ai-agent-code-lsp` template) and the host lane (opt-out, default ON — it needs
|
|
1861
|
-
// nothing and degrades to grep/read), i.e. one env name with two OPPOSITE defaults and no way to silence the
|
|
1862
|
-
// host lane without also naming the sandbox knob. `LSP_HOST_ENABLED` now owns the host lane; an explicit
|
|
1863
|
-
// `LSP_ENABLED=false` keeps working as a host opt-out (it was the only one operators ever had) with a notice
|
|
1864
|
-
// naming the replacement, and `LSP_HOST_ENABLED` always wins when set.
|
|
1865
1103
|
const lspHostEnabled = (() => {
|
|
1866
|
-
// server 3.0.0([1989],不做兼容):`LSP_ENABLED=false` 的 host 腿别名形 = fail-loud 墓碑。
|
|
1867
|
-
// `LSP_ENABLED=true`(沙箱腿 opt-in 正名)不受影响——墓碑只咬「用沙箱名关 host 腿」的旧姿势。
|
|
1868
1104
|
if (process.env.LSP_ENABLED === "false") {
|
|
1869
1105
|
throw new Error("LSP_ENABLED=false no longer drives the HOST lane (removed in server 3.0.0) — set LSP_HOST_ENABLED=false " +
|
|
1870
1106
|
"for the host lane; LSP_ENABLED remains the SANDBOX-lane opt-in only (migration table in USAGE.md)");
|
|
1871
1107
|
}
|
|
1872
1108
|
return boolEnv("LSP_HOST_ENABLED", true);
|
|
1873
1109
|
})();
|
|
1874
|
-
// ── #157([2755] tag `server.exec-lane.silent-downgrade-to-host`,P 类车道选择)——执行车道的
|
|
1875
|
-
// fail-closed 校验。旧形:REMOTE_EXEC 点名了一条 lane 但必需 env 不全(或值拼错不在闭集里)时,下面
|
|
1876
|
-
// 三元链的合取逐臂失败、静默滑穿到尾部 ⇒ CONFIG_PROVIDER=local 落 host(worker 本机、无隔离)、否则落
|
|
1877
|
-
// in-process stub,且两种跑法的成功回执同形(status=completed,test [2753]§2 实测「在哪儿跑的」对外
|
|
1878
|
-
// 零信息量)。操作员配了沙箱却在真机上跑 = P 类判据表的样板案,必须拒启点名,不静默降级。
|
|
1879
|
-
// 语义边界:REMOTE_EXEC **未设**(或空串,env 管理器的显式「不选」形)不是降级,是文档化的部署姿态
|
|
1880
|
-
// (local ⇒ host 默认,fleet ⇒ stub),照旧;拒启只咬「点了名却配不齐/认不出」。缺失点名要**准**——
|
|
1881
|
-
// 只列缺的件,不列 lane 的全量需求(WORKFLOW_SIZE_GUIDELINE / MEMORY_SYNC_* 半配拒启同族先例)。
|
|
1882
1110
|
{
|
|
1883
1111
|
const lane = process.env.REMOTE_EXEC;
|
|
1884
1112
|
if (lane !== undefined && lane !== "") {
|
|
@@ -1903,15 +1131,8 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1903
1131
|
}
|
|
1904
1132
|
}
|
|
1905
1133
|
return {
|
|
1906
|
-
// #246(core 5.30.0):readFace 部署座席。显式设置才铸(缺席让 core 自默认 roots——server 不复制
|
|
1907
|
-
// 上游默认,上游改默认时不产生第二份真源);坏词响亮拒(#210 旋钮坏值纪律)。铸造在本域=组登记
|
|
1908
|
-
// (ORCHESTRATION_GROUP_KEYS)同域——合并码扫 C7:此前误写在 observability 域函数里,条件展开会
|
|
1909
|
-
// 逃过该函数返回型的多余属性检查,分组闭集校验从此看不见这两键。
|
|
1910
1134
|
...(process.env.READ_FACE !== undefined ? { readFace: enumEnv("READ_FACE", "roots", ["open", "roots"]) } : {}),
|
|
1911
1135
|
...(process.env.READ_DENY_PATTERNS !== undefined ? { readDenyPatterns: parseReadDenyPatternsEnv(process.env.READ_DENY_PATTERNS) } : {}),
|
|
1912
|
-
// #253 件 G5(core 5.33.0 #245):内建 deny 表的部署管理面。铸造与校验分两步——先按名字表解析
|
|
1913
|
-
// (空串/半空拒启,见 `parseNameListEnv` 顶注),再交 core 的 `resolveReadDenyBuiltins` 判档名/行名
|
|
1914
|
-
// (引擎同一套规则,宿主不自造第二份)。两键各自可单独设:只设 exclude 时 core 用默认档位。
|
|
1915
1136
|
...(() => {
|
|
1916
1137
|
const tiers = process.env.READ_DENY_BUILTIN_TIERS !== undefined ? parseNameListEnv("READ_DENY_BUILTIN_TIERS", process.env.READ_DENY_BUILTIN_TIERS, { emptySentinel: "none" }) : undefined;
|
|
1917
1138
|
const exclude = process.env.READ_DENY_BUILTIN_EXCLUDE !== undefined ? parseNameListEnv("READ_DENY_BUILTIN_EXCLUDE", process.env.READ_DENY_BUILTIN_EXCLUDE) : undefined;
|
|
@@ -1923,50 +1144,28 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1923
1144
|
provider: "e2b",
|
|
1924
1145
|
apiKey: process.env.E2B_API_KEY,
|
|
1925
1146
|
...(process.env.E2B_TEMPLATE ? { template: process.env.E2B_TEMPLATE } : {}),
|
|
1926
|
-
...((n) => (n !== undefined ? { timeoutMs: n } : {}))(optNumEnvStrict("E2B_TIMEOUT_MS")),
|
|
1927
|
-
|
|
1928
|
-
// gradle's first dependency download produces no output for minutes (gate#2 P1 finding) — looks
|
|
1929
|
-
// like a hang and gets killed; raise this for workers that run such tools. Agents should still
|
|
1930
|
-
// prefer background+poll, but this is the operator escape hatch.
|
|
1931
|
-
...((n) => (n !== undefined ? { livenessMs: n } : {}))(optNumEnvStrict("E2B_LIVENESS_MS")), // #210 A 档
|
|
1147
|
+
...((n) => (n !== undefined ? { timeoutMs: n } : {}))(optNumEnvStrict("E2B_TIMEOUT_MS")),
|
|
1148
|
+
...((n) => (n !== undefined ? { livenessMs: n } : {}))(optNumEnvStrict("E2B_LIVENESS_MS")),
|
|
1932
1149
|
...(process.env.E2B_ALLOW_NET ? { allowInternetAccess: boolEnv("E2B_ALLOW_NET", true) } : {}),
|
|
1933
|
-
// Out-of-band secret/config env injected sandbox-wide (OA Phase-B, design/48 §5 gap 7):
|
|
1934
|
-
// E2B_SANDBOX_ENV is a CSV of THIS worker's OWN env-var NAMES to forward into the sandbox under the
|
|
1935
|
-
// same name (name-only config keeps the secret in the worker's env, never in the spec/center). e.g.
|
|
1936
|
-
// `E2B_SANDBOX_ENV=GIT_TOKEN` → the agent's git can auth via $GIT_TOKEN (GIT_ASKPASS / credential
|
|
1937
|
-
// helper) without the token ever entering the prompt, the command string, or the durable event log.
|
|
1938
1150
|
...(process.env.E2B_SANDBOX_ENV ? { sandboxEnv: resolveEnvForward(process.env.E2B_SANDBOX_ENV) } : {}),
|
|
1939
|
-
// [1452]/[1454] CWD-A:workspace 根显式可配(其余四车道 *_MOUNT_PATH 早有;e2b 此前配置层
|
|
1940
|
-
// 不可达,恒 /home/user)。exec/spawnBackground/fs 三腿同源消费(remote-env-e2b this.cwd)。
|
|
1941
|
-
// ⚠️ 变更纪律(codex 终审 high):resume 契约只回传 snapshotId,恢复的沙箱以**当前**配置值为
|
|
1942
|
-
// cwd——改此值前先 drain 挂起中的 e2b 任务,否则 resumed 任务的 exec 落新根而产物在旧根
|
|
1943
|
-
// (根修=core resumeVM 契约带回 WorkspaceHandle.mountPath,已转 core)。
|
|
1944
1151
|
...(process.env.E2B_MOUNT_PATH ? { mountPath: process.env.E2B_MOUNT_PATH } : {}),
|
|
1945
|
-
// #242(design/242 v1):崩溃遗留沙盒回收腿(boot 一次+reaper 周期)。默认 ON——泄漏=真金
|
|
1946
|
-
// 白银,判据已保守化(活行跳过/无行 10min GRACE/拿不准恒不杀);v1 前置=每部署独立 E2B key
|
|
1947
|
-
// (跨部署异库共用一把 key 的形不支持,发车说明成文)。
|
|
1948
1152
|
orphanReclaim: boolEnv("E2B_ORPHAN_RECLAIM", true),
|
|
1949
1153
|
}
|
|
1950
|
-
:
|
|
1154
|
+
:
|
|
1951
1155
|
process.env.REMOTE_EXEC === "k8s" && process.env.K8S_SANDBOX_IMAGE
|
|
1952
1156
|
? {
|
|
1953
1157
|
provider: "k8s",
|
|
1954
1158
|
image: process.env.K8S_SANDBOX_IMAGE,
|
|
1955
1159
|
...(process.env.K8S_API_URL ? { apiUrl: process.env.K8S_API_URL } : {}),
|
|
1956
1160
|
...(process.env.K8S_TOKEN ? { token: process.env.K8S_TOKEN } : {}),
|
|
1957
|
-
// base64 to survive sema-registry's no-newline secrets API (same constraint as SSH_PRIVATE_KEY).
|
|
1958
1161
|
...(process.env.K8S_CA_CERT_B64 ? { caCert: Buffer.from(process.env.K8S_CA_CERT_B64, "base64").toString("utf8") } : {}),
|
|
1959
1162
|
...(boolEnv("K8S_INSECURE_TLS", false) ? { insecureTls: true } : {}),
|
|
1960
1163
|
...(process.env.K8S_NAMESPACE ? { namespace: process.env.K8S_NAMESPACE } : {}),
|
|
1961
1164
|
...(process.env.K8S_RUNTIME_CLASS != null ? { runtimeClass: process.env.K8S_RUNTIME_CLASS } : {}),
|
|
1962
1165
|
...(process.env.K8S_MOUNT_PATH ? { mountPath: process.env.K8S_MOUNT_PATH } : {}),
|
|
1963
|
-
...((n) => (n !== undefined ? { timeoutMs: n } : {}))(optNumEnvStrict("K8S_TIMEOUT_MS")),
|
|
1964
|
-
// Resource profile knob (resources are profile data, not mechanism — center/service
|
|
1965
|
-
// config, never wire-hardcoded). Found live: 25-module gradle integration OOM'd the 2Gi default.
|
|
1166
|
+
...((n) => (n !== undefined ? { timeoutMs: n } : {}))(optNumEnvStrict("K8S_TIMEOUT_MS")),
|
|
1966
1167
|
...(process.env.K8S_MEMORY ? { memory: process.env.K8S_MEMORY } : {}),
|
|
1967
1168
|
...(process.env.K8S_CPU ? { cpu: process.env.K8S_CPU } : {}),
|
|
1968
|
-
// Workspace-durable suspend/resume: adapter-held MinIO creds (secret minio-agent-worker);
|
|
1969
|
-
// the sandbox only ever sees a presigned URL. All three keys required to enable — else suspendable:false.
|
|
1970
1169
|
...(process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
1971
1170
|
? {
|
|
1972
1171
|
s3Snapshot: {
|
|
@@ -1976,22 +1175,22 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1976
1175
|
secretKey: process.env.MINIO_SECRET_KEY,
|
|
1977
1176
|
...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
|
|
1978
1177
|
...(process.env.WORKSPACE_SNAPSHOT_PREFIX ? { keyPrefix: process.env.WORKSPACE_SNAPSHOT_PREFIX } : {}),
|
|
1979
|
-
...((n) => (n !== undefined ? { presignTtlSec: n } : {}))(optNumEnvStrict("WORKSPACE_SNAPSHOT_TTL_SEC")),
|
|
1178
|
+
...((n) => (n !== undefined ? { presignTtlSec: n } : {}))(optNumEnvStrict("WORKSPACE_SNAPSHOT_TTL_SEC")),
|
|
1980
1179
|
},
|
|
1981
1180
|
}
|
|
1982
1181
|
: {}),
|
|
1983
1182
|
}
|
|
1984
|
-
:
|
|
1183
|
+
:
|
|
1985
1184
|
process.env.REMOTE_EXEC === "ssh" && process.env.SSH_HOST && process.env.SSH_USER && process.env.SSH_PRIVATE_KEY
|
|
1986
1185
|
? {
|
|
1987
1186
|
provider: "ssh",
|
|
1988
1187
|
host: process.env.SSH_HOST,
|
|
1989
1188
|
username: process.env.SSH_USER,
|
|
1990
1189
|
privateKey: normalizeSshPrivateKey(process.env.SSH_PRIVATE_KEY),
|
|
1991
|
-
...((n) => (n !== undefined ? { port: n } : {}))(optNumEnvStrict("SSH_PORT")),
|
|
1190
|
+
...((n) => (n !== undefined ? { port: n } : {}))(optNumEnvStrict("SSH_PORT")),
|
|
1992
1191
|
...(process.env.SSH_MOUNT_PATH ? { mountPath: process.env.SSH_MOUNT_PATH } : {}),
|
|
1993
1192
|
}
|
|
1994
|
-
:
|
|
1193
|
+
:
|
|
1995
1194
|
process.env.REMOTE_EXEC === "adb" && process.env.ADB_SERIAL
|
|
1996
1195
|
? {
|
|
1997
1196
|
provider: "adb",
|
|
@@ -1999,19 +1198,14 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1999
1198
|
...(process.env.ADB_PATH ? { adbPath: process.env.ADB_PATH } : {}),
|
|
2000
1199
|
...(process.env.ADB_MOUNT_PATH ? { mountPath: process.env.ADB_MOUNT_PATH } : {}),
|
|
2001
1200
|
}
|
|
2002
|
-
:
|
|
1201
|
+
:
|
|
2003
1202
|
process.env.REMOTE_EXEC === "host"
|
|
2004
1203
|
? {
|
|
2005
1204
|
provider: "host",
|
|
2006
1205
|
...(process.env.HOST_WORKSPACE_BASE ? { workspaceBase: process.env.HOST_WORKSPACE_BASE } : {}),
|
|
2007
|
-
...((n) => (n !== undefined ? { commandTimeoutMs: n } : {}))(optNumEnvStrict("HOST_COMMAND_TIMEOUT_MS")),
|
|
1206
|
+
...((n) => (n !== undefined ? { commandTimeoutMs: n } : {}))(optNumEnvStrict("HOST_COMMAND_TIMEOUT_MS")),
|
|
2008
1207
|
}
|
|
2009
|
-
:
|
|
2010
|
-
// suspendable:false). DOCKER_IMAGE is REQUIRED — no docker.io default (domestic-images iron rule); a
|
|
2011
|
-
// deployment points it at the docker.io/claybobby public pool or its own private registry (SWR
|
|
2012
|
-
// deprecated; the in-network Gitea registry container was retired 2026-08, no CN fallback baked
|
|
2013
|
-
// in). Secrets are env-NAMEs the worker resolves from its
|
|
2014
|
-
// own process.env (DOCKER_SANDBOX_ENV, a CSV of NAMEs) — never the values in the spec/center.
|
|
1208
|
+
:
|
|
2015
1209
|
process.env.REMOTE_EXEC === "local-docker" && process.env.DOCKER_IMAGE
|
|
2016
1210
|
? {
|
|
2017
1211
|
provider: "local-docker",
|
|
@@ -2020,31 +1214,19 @@ function parseOrchestrationDomain(ctx) {
|
|
|
2020
1214
|
...(process.env.DOCKER_PATH ? { dockerPath: process.env.DOCKER_PATH } : {}),
|
|
2021
1215
|
...(process.env.DOCKER_HOST ? { dockerHost: process.env.DOCKER_HOST } : {}),
|
|
2022
1216
|
...(process.env.DOCKER_MEMORY ? { memory: process.env.DOCKER_MEMORY } : {}),
|
|
2023
|
-
...((n) => (n !== undefined ? { cpus: n } : {}))(optNumEnvStrict("DOCKER_CPUS")),
|
|
2024
|
-
// 鲁棒性批5 A1(2026-08-05):此前裸 `Number(process.env.DOCKER_PIDS_LIMIT)`——非数字值(如
|
|
2025
|
-
// 手滑的 "512;") → NaN,而消费端(local-docker 执行环境)对 `pidsLimit` 的守卫是
|
|
2026
|
-
// `?? 512`(只挡 null/undefined),NaN 穿透守卫;随后 `NaN > 0` 恒假,`--pids-limit` 整个
|
|
2027
|
-
// 从 docker run 参数里省略——运维以为设了 fork-bomb 背栓,实际背栓被静默卸掉。
|
|
2028
|
-
// optFinitePositiveEnv:非法值回 undefined(消费端默认 512 生效)+ S20 boot 警告。
|
|
1217
|
+
...((n) => (n !== undefined ? { cpus: n } : {}))(optNumEnvStrict("DOCKER_CPUS")),
|
|
2029
1218
|
...((n) => (n !== undefined ? { pidsLimit: n } : {}))(optFinitePositiveEnv("DOCKER_PIDS_LIMIT")),
|
|
2030
1219
|
...(boolEnv("DOCKER_DROP_CAPS", false) ? { dropAllCaps: true } : {}),
|
|
2031
1220
|
...(process.env.DOCKER_NETWORK
|
|
2032
1221
|
? { network: enumEnv("DOCKER_NETWORK", "bridge", ["none", "bridge", "host"]) }
|
|
2033
1222
|
: {}),
|
|
2034
|
-
...((n) => (n !== undefined ? { commandTimeoutMs: n } : {}))(optNumEnvStrict("DOCKER_COMMAND_TIMEOUT_MS")),
|
|
1223
|
+
...((n) => (n !== undefined ? { commandTimeoutMs: n } : {}))(optNumEnvStrict("DOCKER_COMMAND_TIMEOUT_MS")),
|
|
2035
1224
|
...(process.env.DOCKER_SANDBOX_ENV ? { env: resolveEnvForward(process.env.DOCKER_SANDBOX_ENV) } : {}),
|
|
2036
1225
|
}
|
|
2037
|
-
:
|
|
2038
|
-
// single-machine source) with no explicit REMOTE_EXEC, default to the `host` lane so "fill .env
|
|
2039
|
-
// and run" works without the user setting REMOTE_EXEC. Fleet/remote (CONFIG_PROVIDER≠local) keeps
|
|
2040
|
-
// the in-process stub default (REMOTE_EXEC unset = OA/review posture) — unchanged.
|
|
1226
|
+
:
|
|
2041
1227
|
process.env.CONFIG_PROVIDER === "local"
|
|
2042
1228
|
? { provider: "host" }
|
|
2043
1229
|
: undefined,
|
|
2044
|
-
// SVC-3 git-worktree isolation: opt-in, operator-supplied ONLY. WORKTREE_REPO_ROOT is the trusted base
|
|
2045
|
-
// repo; WORKTREE_ALLOWED_ROOTS (CSV) is the defence-in-depth allowlist (default = [repoRoot]);
|
|
2046
|
-
// WORKTREE_BASE_COMMIT pins the worktree ref (default core HEAD). Enabled only when both the flag and the
|
|
2047
|
-
// repo root are present — a half-set config stays OFF (no silent default repo).
|
|
2048
1230
|
worktreeIsolation: boolEnv("WORKTREE_ISOLATION_ENABLED", false) && process.env.WORKTREE_REPO_ROOT
|
|
2049
1231
|
? {
|
|
2050
1232
|
repoRoot: process.env.WORKTREE_REPO_ROOT,
|
|
@@ -2056,79 +1238,60 @@ function parseOrchestrationDomain(ctx) {
|
|
|
2056
1238
|
: undefined,
|
|
2057
1239
|
leaderEnabled: boolEnv("LEADER_ENABLED", false),
|
|
2058
1240
|
leaderFanoutEnabled: boolEnv("LEADER_FANOUT_ENABLED", true),
|
|
2059
|
-
routerEnabled: boolEnv("ROUTER_ENABLED", false),
|
|
2060
|
-
selfOrchestrationEnabled: postureOn("SELF_ORCHESTRATION_ENABLED"),
|
|
2061
|
-
forkEnabled: postureOn("FORK_ENABLED"),
|
|
2062
|
-
experimentalObserverAgents: boolEnv("EXPERIMENTAL_OBSERVER_AGENTS", false),
|
|
2063
|
-
schedulerEnabled: postureOn("SCHEDULER_ENABLED"),
|
|
2064
|
-
planModeEnabled: boolEnv("PLAN_MODE_ENABLED", true),
|
|
1241
|
+
routerEnabled: boolEnv("ROUTER_ENABLED", false),
|
|
1242
|
+
selfOrchestrationEnabled: postureOn("SELF_ORCHESTRATION_ENABLED"),
|
|
1243
|
+
forkEnabled: postureOn("FORK_ENABLED"),
|
|
1244
|
+
experimentalObserverAgents: boolEnv("EXPERIMENTAL_OBSERVER_AGENTS", false),
|
|
1245
|
+
schedulerEnabled: postureOn("SCHEDULER_ENABLED"),
|
|
1246
|
+
planModeEnabled: boolEnv("PLAN_MODE_ENABLED", true),
|
|
2065
1247
|
...(process.env.SCHEDULER_STORE_PATH ? { schedulerStorePath: process.env.SCHEDULER_STORE_PATH } : {}),
|
|
2066
|
-
schedulerSessionWakeup: boolEnv("SCHEDULER_SESSION_WAKEUP", true),
|
|
2067
|
-
schedulerSessionLifetime: boolEnv("SCHEDULER_SESSION_LIFETIME", true),
|
|
1248
|
+
schedulerSessionWakeup: boolEnv("SCHEDULER_SESSION_WAKEUP", true),
|
|
1249
|
+
schedulerSessionLifetime: boolEnv("SCHEDULER_SESSION_LIFETIME", true),
|
|
2068
1250
|
selfOrchestrationModels: csv("SELF_ORCHESTRATION_MODELS"),
|
|
2069
1251
|
selfOrchestrationWorkerIsolation: boolEnv("SELF_ORCHESTRATION_WORKER_ISOLATION", false),
|
|
2070
1252
|
...(workflowSizeGuideline ? { workflowSizeGuideline } : {}),
|
|
2071
|
-
workflowRunStoreBackend: enumEnv("WORKFLOW_RUN_STORE", "auto", ["auto", "file", "memory"]),
|
|
2072
|
-
workflowOrphanGraceMs: clampEnvWithWarn("WORKFLOW_ORPHAN_GRACE_MS", String(24 * 60 * 60 * 1000), 60_000),
|
|
2073
|
-
workflowJournalRetentionMs: clampEnvWithWarn("WORKFLOW_JOURNAL_RETENTION_MS", String(7 * 24 * 60 * 60 * 1000), 60_000),
|
|
2074
|
-
workflowRunRetentionMs: clampEnvWithWarn("WORKFLOW_RUN_RETENTION_MS", String(30 * 24 * 60 * 60 * 1000), 60_000),
|
|
2075
|
-
...(usageWindows ? { usageWindows } : {}),
|
|
2076
|
-
backgroundAgentRetentionMs: clampEnvWithWarn("BG_AGENT_RETENTION_MS", String(7 * 24 * 60 * 60 * 1000), 60_000),
|
|
2077
|
-
backgroundAgentStaleRunningMs: clampEnvWithWarn("BG_AGENT_STALE_RUNNING_MS", String(15 * 60_000), 10 * 60_000),
|
|
2078
|
-
backgroundAgentParkClaimStaleMs: clampEnvWithWarn("BG_AGENT_PARK_CLAIM_STALE_MS", String(60 * 60_000), 60_000),
|
|
2079
|
-
rosterRetentionMs: clampEnvWithWarn("ROSTER_RETENTION_MS", String(30 * 24 * 60 * 60 * 1000), 60_000),
|
|
2080
|
-
leaderRunStaleMs: clampEnvWithWarn("LEADER_RUN_STALE_MS", String(26 * 60 * 60 * 1000), 60_000),
|
|
2081
|
-
// 修8: scratchpad sweep TTL — 0(or negative)= disabled; enabled values floor at 1m (a sub-minute TTL would
|
|
2082
|
-
// race in-flight tasks' scratchpad writes for no operational gain).
|
|
2083
|
-
// #210 B 档,只对**下限夹取**点名:`≤0 ⇒ 0` 是上面那条设计注写明的「显式关本腿」,是 operator 的
|
|
2084
|
-
// 意图本身,不是夹取,warn 它等于给正常配置刷噪声。
|
|
1253
|
+
workflowRunStoreBackend: enumEnv("WORKFLOW_RUN_STORE", "auto", ["auto", "file", "memory"]),
|
|
1254
|
+
workflowOrphanGraceMs: clampEnvWithWarn("WORKFLOW_ORPHAN_GRACE_MS", String(24 * 60 * 60 * 1000), 60_000),
|
|
1255
|
+
workflowJournalRetentionMs: clampEnvWithWarn("WORKFLOW_JOURNAL_RETENTION_MS", String(7 * 24 * 60 * 60 * 1000), 60_000),
|
|
1256
|
+
workflowRunRetentionMs: clampEnvWithWarn("WORKFLOW_RUN_RETENTION_MS", String(30 * 24 * 60 * 60 * 1000), 60_000),
|
|
1257
|
+
...(usageWindows ? { usageWindows } : {}),
|
|
1258
|
+
backgroundAgentRetentionMs: clampEnvWithWarn("BG_AGENT_RETENTION_MS", String(7 * 24 * 60 * 60 * 1000), 60_000),
|
|
1259
|
+
backgroundAgentStaleRunningMs: clampEnvWithWarn("BG_AGENT_STALE_RUNNING_MS", String(15 * 60_000), 10 * 60_000),
|
|
1260
|
+
backgroundAgentParkClaimStaleMs: clampEnvWithWarn("BG_AGENT_PARK_CLAIM_STALE_MS", String(60 * 60_000), 60_000),
|
|
1261
|
+
rosterRetentionMs: clampEnvWithWarn("ROSTER_RETENTION_MS", String(30 * 24 * 60 * 60 * 1000), 60_000),
|
|
1262
|
+
leaderRunStaleMs: clampEnvWithWarn("LEADER_RUN_STALE_MS", String(26 * 60 * 60 * 1000), 60_000),
|
|
2085
1263
|
scratchpadSweepTtlMs: ((n, raw) => {
|
|
2086
1264
|
if (n <= 0)
|
|
2087
1265
|
return 0;
|
|
2088
1266
|
if (n < 60_000) {
|
|
2089
|
-
// ⚠️ `raw` 记**环境原文**而不是 `String(n)`(codex 自审 R1-F4 真修):`1e3` / `+1000` / 带空白的
|
|
2090
|
-
// ` 1000 ` 归一后都是 "1000",而 operator 要在 env 文件里找的正是他写下的那串字符。
|
|
2091
1267
|
CONFIG_WARNINGS.push({ env: "SCRATCHPAD_SWEEP_TTL_MS", raw, used: "60000", kind: "clamped" });
|
|
2092
1268
|
return 60_000;
|
|
2093
1269
|
}
|
|
2094
1270
|
return n;
|
|
2095
1271
|
})(numEnv("SCRATCHPAD_SWEEP_TTL_MS", String(7 * 24 * 60 * 60 * 1000)), env("SCRATCHPAD_SWEEP_TTL_MS", String(7 * 24 * 60 * 60 * 1000))),
|
|
2096
|
-
// RFC B5 region package-source selector — 🆕 default `global` (clay 拍 2026-07-13 海外为准:官方源 env-first
|
|
2097
|
-
// 对存量镜像即生效);国内部署显式 SANDBOX_PKG_SOURCE=cn;`none` = 完全不注入(旧 unset 行为,字节级不变)。
|
|
2098
|
-
// #210 A 档:补闭集。此前**任何**词都原样透传进沙箱注入层——`SANDBOX_PKG_SOURCE=china`(拼错的
|
|
2099
|
-
// `cn`)在国内部署上得到的是「一个认不出的源名」,注入层认不出就什么都不注入 = 静默回到官方源,
|
|
2100
|
-
// 于是每次 pip/npm 都走境外、任务超时,而 operator 明明"配了国内源"。词表与 README 的四词同源。
|
|
2101
|
-
// ⚠️ 不直接用 `enumEnv`:它把空串当一个**值**去比对词表,而这条旋钮改前是 `|| "global"`(空串=未设,
|
|
2102
|
-
// compose 的 `${VAR:-}` 模板天天产空串)。走 `env(name, "global")` 先把空串归一成缺省,再验词表。
|
|
2103
1272
|
...(process.env.SANDBOX_PKG_SOURCE !== "none"
|
|
2104
1273
|
? { sandboxPkgSource: pkgSourceEnv() }
|
|
2105
1274
|
: {}),
|
|
2106
|
-
sessionAutoTitle: boolEnv("SESSION_AUTO_TITLE", true),
|
|
2107
|
-
selectEnvironmentTool: boolEnv("SELECT_ENVIRONMENT_TOOL", true),
|
|
2108
|
-
envFactsEnabled: boolEnv("SANDBOX_ENV_FACTS", true),
|
|
2109
|
-
toolDeferLongtail: boolEnv("TOOL_DEFER_LONGTAIL", false),
|
|
2110
|
-
lspEnabled: boolEnv("LSP_ENABLED", false),
|
|
2111
|
-
lspHostEnabled,
|
|
2112
|
-
workflowAgentsReadOnly: boolEnv("WORKFLOW_AGENTS_READONLY", false),
|
|
1275
|
+
sessionAutoTitle: boolEnv("SESSION_AUTO_TITLE", true),
|
|
1276
|
+
selectEnvironmentTool: boolEnv("SELECT_ENVIRONMENT_TOOL", true),
|
|
1277
|
+
envFactsEnabled: boolEnv("SANDBOX_ENV_FACTS", true),
|
|
1278
|
+
toolDeferLongtail: boolEnv("TOOL_DEFER_LONGTAIL", false),
|
|
1279
|
+
lspEnabled: boolEnv("LSP_ENABLED", false),
|
|
1280
|
+
lspHostEnabled,
|
|
1281
|
+
workflowAgentsReadOnly: boolEnv("WORKFLOW_AGENTS_READONLY", false),
|
|
2113
1282
|
imageBakes: {
|
|
2114
1283
|
enabled: boolEnv("IMAGE_BAKES_ENABLED", false),
|
|
2115
1284
|
runnerPrincipal: bakeRunnerPrincipal,
|
|
2116
1285
|
registry: process.env.BAKE_IMAGE_REGISTRY || null,
|
|
2117
1286
|
cacheBase: process.env.BAKE_CACHE_BASE || null,
|
|
2118
1287
|
defaultBaseRef: process.env.SANDBOX_BASE_REF || null,
|
|
2119
|
-
leaseMs: clampEnvWithWarn("BAKE_LEASE_MS", "90000", 1000),
|
|
1288
|
+
leaseMs: clampEnvWithWarn("BAKE_LEASE_MS", "90000", 1000),
|
|
2120
1289
|
staleMs: clampEnvWithWarn("BAKE_STALE_MS", "180000", 1000),
|
|
2121
1290
|
submitRateMax: clampEnvWithWarn("BAKE_SUBMIT_RATE_MAX", "10", 0),
|
|
2122
1291
|
submitRateWindowSec: clampEnvWithWarn("BAKE_SUBMIT_RATE_WINDOW_SEC", "60", 1),
|
|
2123
1292
|
},
|
|
2124
1293
|
};
|
|
2125
1294
|
}
|
|
2126
|
-
/** 域:limits+http(限额与 HTTP 面)—— 端口、附件/工作区体积与 TTL、成本与速率天花板、排空窗、回收周期。 */
|
|
2127
|
-
/**
|
|
2128
|
-
* #322:limitsHttp 组里**有下限**的三个数值旋钮的下限值 —— env 腿(夹取+点名)与 center 热腿(整批拒)
|
|
2129
|
-
* 共用同一组阈值。判据同源、**处置刻意不同**:env 值是既有部署的存量,夹取不打断服务;center 发布是一次
|
|
2130
|
-
* 显式且可重发的动作,拒绝才是有意义的反馈(#123 响亮拒)。
|
|
2131
|
-
*/
|
|
2132
1295
|
export const LIMITS_HTTP_FLOORS = {
|
|
2133
1296
|
attachmentMaxBytes: 1024,
|
|
2134
1297
|
attachmentUnboundTtlMs: 60_000,
|
|
@@ -2136,19 +1299,8 @@ export const LIMITS_HTTP_FLOORS = {
|
|
|
2136
1299
|
};
|
|
2137
1300
|
function parseLimitsHttpDomain() {
|
|
2138
1301
|
return {
|
|
2139
|
-
// #210 A 档:裸 Number ⇒ NaN 直进 `server.listen(NaN)`(Node 当成 0 = 随机端口),于是服务起来了、
|
|
2140
|
-
// 健康检查在**另一个**端口上、注册进 fleet 的地址是错的,全程零告警。[0,65535] 里 0 是合法的
|
|
2141
|
-
// 「随机端口」(测试在用),不收窄。
|
|
2142
1302
|
port: numEnvBounded("PORT", "8090", 0, 65535),
|
|
2143
|
-
// D-1 孤儿对象 GC grace(复审 F10;0=关掉本腿,负/坏值走默认)。
|
|
2144
|
-
// #210 C 档(不升拒的理由写在这里,不是复述行为):这条旋钮的坏值后果**轻**——回落到 1h 宽限,方向
|
|
2145
|
-
// 是「更晚回收」,不删多余的东西,不打开任何门;而它落在 D-1 附件面这种存量部署普遍设过的位置上,
|
|
2146
|
-
// 拒启的破坏面明显大于收益。所以保留回默认,补 CONFIG_WARNINGS 点名(原值 + 采用值),让 operator
|
|
2147
|
-
// 排空日志时看得见。`0` 是文档化的「关本腿」,合法且零 warn。
|
|
2148
1303
|
attachmentOrphanGraceMs: ((raw) => {
|
|
2149
|
-
// ⚠️ 只把 `undefined` 当未设(**不是** `undefined || ""`):改前的判据是 `!== undefined` + `Number("")`
|
|
2150
|
-
// = 0 = 「关本腿」,空串在这条上是一个**已生效的合法值**。加上一条空串归一化会静默把一台已经关了
|
|
2151
|
-
// 孤儿清扫的部署改成 1h 宽限 —— 律条治的是坏值静默,不是顺手改既有语义。
|
|
2152
1304
|
if (raw === undefined)
|
|
2153
1305
|
return 3_600_000;
|
|
2154
1306
|
const v = Number(raw);
|
|
@@ -2157,21 +1309,15 @@ function parseLimitsHttpDomain() {
|
|
|
2157
1309
|
CONFIG_WARNINGS.push({ env: "ATTACHMENT_ORPHAN_GRACE_MS", raw, used: "3600000", kind: "invalid-default" });
|
|
2158
1310
|
return 3_600_000;
|
|
2159
1311
|
})(process.env.ATTACHMENT_ORPHAN_GRACE_MS),
|
|
2160
|
-
// workspace 浏览面(#3,[1894]①)单文件读上限。
|
|
2161
1312
|
workspaceFileMaxBytes: optFinitePositiveEnv("WORKSPACE_FILE_MAX_BYTES") ?? 8 * 1024 * 1024,
|
|
2162
|
-
attachmentMaxBytes: clampEnvWithWarn("ATTACHMENT_MAX_BYTES", String(32 * 1024 * 1024), LIMITS_HTTP_FLOORS.attachmentMaxBytes),
|
|
1313
|
+
attachmentMaxBytes: clampEnvWithWarn("ATTACHMENT_MAX_BYTES", String(32 * 1024 * 1024), LIMITS_HTTP_FLOORS.attachmentMaxBytes),
|
|
2163
1314
|
...(process.env.ATTACHMENT_MIME_ALLOWLIST
|
|
2164
1315
|
? { attachmentMimeAllowlist: process.env.ATTACHMENT_MIME_ALLOWLIST.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean) }
|
|
2165
1316
|
: {}),
|
|
2166
|
-
attachmentUnboundTtlMs: clampEnvWithWarn("ATTACHMENT_UNBOUND_TTL_MS", String(24 * 3600 * 1000), LIMITS_HTTP_FLOORS.attachmentUnboundTtlMs),
|
|
1317
|
+
attachmentUnboundTtlMs: clampEnvWithWarn("ATTACHMENT_UNBOUND_TTL_MS", String(24 * 3600 * 1000), LIMITS_HTTP_FLOORS.attachmentUnboundTtlMs),
|
|
2167
1318
|
infraCostRates: {
|
|
2168
|
-
// #210 B 档:负单价夹到 0 是对的(负成本会让账面倒扣),但夹取必须点名——不然 operator 只看到
|
|
2169
|
-
// 基建成本恒 0,查不到是自己那个负号被吃掉了。
|
|
2170
1319
|
toolCallMicroUsd: clampEnvWithWarn("INFRA_COST_TOOL_CALL", "0", 0),
|
|
2171
1320
|
sandboxSecMicroUsd: clampEnvWithWarn("INFRA_COST_SANDBOX_SEC", "0", 0),
|
|
2172
|
-
// #193 车5(P1-4):per-run 出网计量未建(cost-taxonomy `infraUsageFromEvents` 恒 `egressBytes: 0`),
|
|
2173
|
-
// 该费率设成任何值都恒计 $0。旋钮保留(接真计量是产品件),但设非零必点名——静默会让 operator
|
|
2174
|
-
// 以为出网已计费。接上计量的那批必须同时删掉这条 push(test/config-knob-loudness #193 格看守)。
|
|
2175
1321
|
egressGbMicroUsd: (() => {
|
|
2176
1322
|
const v = clampEnvWithWarn("INFRA_COST_EGRESS_GB", "0", 0);
|
|
2177
1323
|
if (v > 0)
|
|
@@ -2179,11 +1325,9 @@ function parseLimitsHttpDomain() {
|
|
|
2179
1325
|
return v;
|
|
2180
1326
|
})(),
|
|
2181
1327
|
},
|
|
2182
|
-
drainGraceMs: clampEnvWithWarn("DRAIN_GRACE_MS", String(10 * 60 * 1000), LIMITS_HTTP_FLOORS.drainGraceMs),
|
|
2183
|
-
sighupIdleGraceMs: clampEnvWithWarn("SIGHUP_IDLE_GRACE_MS", String(120_000), 5_000),
|
|
2184
|
-
...parentPidEnv(),
|
|
2185
|
-
// BL-9: numEnv (not bare Number) — a non-numeric typo FAILS at startup instead of becoming NaN, which would
|
|
2186
|
-
// silently DISABLE the operator cost/rate ceiling (every `> ceiling` comparison against NaN is false).
|
|
1328
|
+
drainGraceMs: clampEnvWithWarn("DRAIN_GRACE_MS", String(10 * 60 * 1000), LIMITS_HTTP_FLOORS.drainGraceMs),
|
|
1329
|
+
sighupIdleGraceMs: clampEnvWithWarn("SIGHUP_IDLE_GRACE_MS", String(120_000), 5_000),
|
|
1330
|
+
...parentPidEnv(),
|
|
2187
1331
|
rateLimitPerMin: numEnv("RATE_LIMIT_RPM", "0"),
|
|
2188
1332
|
maxTaskCostUsd: numEnv("MAX_TASK_COST_USD", "0"),
|
|
2189
1333
|
maxTaskTokens: numEnv("MAX_TASK_TOKENS", "0"),
|
|
@@ -2191,25 +1335,21 @@ function parseLimitsHttpDomain() {
|
|
|
2191
1335
|
costQuotaWindowSec: numEnv("COST_QUOTA_WINDOW_SEC", "86400"),
|
|
2192
1336
|
reapIntervalSec: numEnv("REAP_INTERVAL_SEC", "60"),
|
|
2193
1337
|
runStaleSec: numEnv("REAP_RUN_STALE_SEC", "120"),
|
|
2194
|
-
toolResultTtlSec: numEnv("TOOL_RESULT_TTL_SEC", "86400"),
|
|
1338
|
+
toolResultTtlSec: numEnv("TOOL_RESULT_TTL_SEC", "86400"),
|
|
2195
1339
|
};
|
|
2196
1340
|
}
|
|
2197
|
-
/** 域:observability(可观测)—— 指标/追踪令牌、工具与思考轨迹开关、日志档位、OTLP 出口。 */
|
|
2198
1341
|
function parseObservabilityDomain() {
|
|
2199
1342
|
return {
|
|
2200
1343
|
metricsToken: process.env.METRICS_TOKEN || undefined,
|
|
2201
1344
|
traceToken: process.env.TRACE_TOKEN || undefined,
|
|
2202
1345
|
toolTrace: boolEnv("TOOL_TRACE", false),
|
|
2203
|
-
traceThinking: boolEnv("TRACE_THINKING", true),
|
|
1346
|
+
traceThinking: boolEnv("TRACE_THINKING", true),
|
|
2204
1347
|
logLevel: env("LOG_LEVEL", "info"),
|
|
2205
1348
|
otel: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
2206
1349
|
? {
|
|
2207
1350
|
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, ""),
|
|
2208
|
-
// #210 A 档:NaN 周期进 setInterval ⇒ Node 归一成 1ms,出口从「每 15s 一批」变成**每毫秒**一批
|
|
2209
|
-
// (打满 collector 且把本进程 CPU 吃光),而这是一条运维为了"看见"才配的旋钮。
|
|
2210
1351
|
intervalMs: numEnv("OTEL_EXPORT_INTERVAL_MS", "15000"),
|
|
2211
1352
|
serviceName: env("OTEL_SERVICE_NAME", "sema-server"),
|
|
2212
|
-
// `OTEL_EXPORTER_OTLP_HEADERS` = comma-separated `k=v` pairs (e.g. an auth header).
|
|
2213
1353
|
headers: Object.fromEntries(csv("OTEL_EXPORTER_OTLP_HEADERS").map((kv) => {
|
|
2214
1354
|
const i = kv.indexOf("=");
|
|
2215
1355
|
return i === -1 ? [kv, ""] : [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
|
|
@@ -2218,10 +1358,6 @@ function parseObservabilityDomain() {
|
|
|
2218
1358
|
: undefined,
|
|
2219
1359
|
};
|
|
2220
1360
|
}
|
|
2221
|
-
/** GIT_API_KIND:repo 工具的 Git-host 方言(闭集 gitea|github,缺省 gitea)。
|
|
2222
|
-
* 未知词=拒启(安全轴词表纪律);另一坏形也拒启:kind 缺省/gitea 而 BASEURL 指向 github.com——
|
|
2223
|
-
* Gitea 形状(`/api/v1` 前缀、`.diff` 后缀)对 GitHub 恒 404,与其在任务运行时逐调用 loud 404,
|
|
2224
|
-
* 不如启动即指路(#123 FLEET_ADVERTISE_ADDRESS 坏形响亮拒同族)。 */
|
|
2225
1361
|
function parseGitApiKind(raw, baseUrl) {
|
|
2226
1362
|
const kind = raw ?? "gitea";
|
|
2227
1363
|
if (!isGitApiKind(kind)) {
|
|
@@ -2229,8 +1365,6 @@ function parseGitApiKind(raw, baseUrl) {
|
|
|
2229
1365
|
}
|
|
2230
1366
|
if (!baseUrl)
|
|
2231
1367
|
return kind;
|
|
2232
|
-
// 配了就必须是合法 URL(USAGE 本就要求带 scheme 的完整 baseURL;坏 URL 此前静默过门、逐调用 fetch
|
|
2233
|
-
// 才炸 —— 启动即拒;codex F3)。尾点主机(api.github.com.)DNS 等价,归一后再比对,防绕过。
|
|
2234
1368
|
if (!URL.canParse(baseUrl)) {
|
|
2235
1369
|
throw new Error(`GIT_API_BASEURL="${baseUrl}" is not a valid URL (expected e.g. https://git.example.com)`);
|
|
2236
1370
|
}
|
|
@@ -2241,7 +1375,6 @@ function parseGitApiKind(raw, baseUrl) {
|
|
|
2241
1375
|
throw new Error(`GIT_API_BASEURL points at ${host} but GIT_API_KIND is "gitea" — the Gitea API shapes 404 on GitHub. Set GIT_API_KIND=github (base url should be https://api.github.com, or your GHE /api/v3 root).`);
|
|
2242
1376
|
}
|
|
2243
1377
|
if (kind === "github") {
|
|
2244
|
-
// 对称校验(codex F3):web 域名/带 path 的 api 根都会让每个仓操作 404,而客户端照建、场景照宣可用。
|
|
2245
1378
|
if (githubFamily && host !== "api.github.com") {
|
|
2246
1379
|
throw new Error(`GIT_API_BASEURL host ${host} is the GitHub WEB host — the API root is https://api.github.com (a repo page URL is not an API base).`);
|
|
2247
1380
|
}
|
|
@@ -2254,28 +1387,20 @@ function parseGitApiKind(raw, baseUrl) {
|
|
|
2254
1387
|
}
|
|
2255
1388
|
return kind;
|
|
2256
1389
|
}
|
|
2257
|
-
/** 域:misc-integration(外部集成)—— plugins clone 白名单、git/OA 面、场景与 skill 目录、sema-registry 接入。 */
|
|
2258
1390
|
function parseIntegrationsDomain() {
|
|
2259
1391
|
return {
|
|
2260
|
-
// plugins 域 clone host 白名单(契约 [1361]①;缺省 github.com,逗号扩 https 镜像域)。
|
|
2261
1392
|
pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
|
|
2262
|
-
configBootFetchBudgetMs: clampEnvWithWarn("CONFIG_BOOT_FETCH_BUDGET_MS", "1500", 0),
|
|
1393
|
+
configBootFetchBudgetMs: clampEnvWithWarn("CONFIG_BOOT_FETCH_BUDGET_MS", "1500", 0),
|
|
2263
1394
|
gitApiBaseUrl: process.env.GIT_API_BASEURL,
|
|
2264
1395
|
gitApiToken: process.env.GIT_API_TOKEN,
|
|
2265
1396
|
gitApiKind: parseGitApiKind(process.env.GIT_API_KIND, process.env.GIT_API_BASEURL),
|
|
2266
|
-
// BEHAVIOR CHANGE([891] clay 硬裁定):出厂缺省场景 default→code(CC 编码 persona 蒸馏版)——
|
|
2267
|
-
// 编码产品出厂态对标 CC 出厂态,通用域中性 persona 用 DEFAULT_SCENARIO=default 显式选回。
|
|
2268
1397
|
defaultScenario: env("DEFAULT_SCENARIO", "code"),
|
|
2269
1398
|
skillsDir: env("SKILLS_DIR", defaultSkillsDir()),
|
|
2270
|
-
// [2354] clay 令:OA 场景整体退役(oa-tools/oa 场景/OA 轮询审批车道随 5.0.0 全清)——
|
|
2271
|
-
// OA_* env 全族进 RETIRED_ENVS 墓碑(在场即拒启),此处零读取。
|
|
2272
|
-
// sema-registry (control plane) URL/token/worker. The orchestrator renamed the worker-injected env from
|
|
2273
|
-
// [2354]:CONFIG_CENTER_* 双读已删(墓碑在 rejectRetiredEnvs);SEMA_REGISTRY_* 唯一。
|
|
2274
1399
|
configCenter: process.env.SEMA_REGISTRY_URL
|
|
2275
1400
|
? {
|
|
2276
1401
|
baseUrl: env("SEMA_REGISTRY_URL"),
|
|
2277
1402
|
token: process.env.SEMA_REGISTRY_TOKEN || "",
|
|
2278
|
-
dryRun: boolEnv("SEMA_REGISTRY_DRY_RUN", false),
|
|
1403
|
+
dryRun: boolEnv("SEMA_REGISTRY_DRY_RUN", false),
|
|
2279
1404
|
worker: process.env.SEMA_REGISTRY_WORKER || undefined,
|
|
2280
1405
|
}
|
|
2281
1406
|
: undefined,
|
|
@@ -2284,26 +1409,9 @@ function parseIntegrationsDomain() {
|
|
|
2284
1409
|
a2aServe: parseA2aServe(),
|
|
2285
1410
|
};
|
|
2286
1411
|
}
|
|
2287
|
-
/** 一条已声明 skill 的字段上限(卡是**公网文档**:无界的运维自由文本会让一张卡长到任何大小,而卡由
|
|
2288
|
-
* 匿名调用方拉取)。id 短、文案宽,数量有帽。 */
|
|
2289
1412
|
const A2A_SERVE_MAX_SKILLS = 64;
|
|
2290
1413
|
const A2A_SERVE_MAX_ID_CHARS = 128;
|
|
2291
1414
|
const A2A_SERVE_MAX_TEXT_CHARS = 2048;
|
|
2292
|
-
/**
|
|
2293
|
-
* DESIGN-269 §3.3 车2 —— `A2A_SERVE_*` 域(server-as-peer 总旋钮 + 卡内容声明面)。
|
|
2294
|
-
*
|
|
2295
|
-
* 姿势(全是既有先例的照抄,不是新发明):
|
|
2296
|
-
* · **默认 OFF**(operator-knob 律:无客户端表态派生腿)。`A2A_SERVE_ENABLED` 走 {@link boolEnv} ⇒
|
|
2297
|
-
* 坏词当场拒启(#123/#241-rc2 响亮拒族),不静默回默认。
|
|
2298
|
-
* · OFF ⇒ 返回 `undefined`,而不是一个 `enabled:false` 的块。缺席即 OFF 让「发了卡但拒调」这个半态
|
|
2299
|
-
* 在类型上不可表达(设计稿 §3.3 的判词是「卡都不发=对外不存在」)。
|
|
2300
|
-
* · ON 而 `A2A_SERVE_URL` 缺席/坏形 ⇒ **拒启**。理由与 #123 `FLEET_ADVERTISE_ADDRESS` 逐字同族:
|
|
2301
|
-
* ①半配置的部署带病运行比起不来危险(卡上没有 `url`,对端拿到一张打不通的卡,失败点远离原因);
|
|
2302
|
-
* ②可达地址是**运维声明**的,不许自猜——从请求 `Host` 头推导等于让匿名调用方决定我们公网卡上
|
|
2303
|
-
* 公告的地址(反射/缓存投毒面),本仓在 fleet 那条腿上已经拒绝过一次同样的诱惑。
|
|
2304
|
-
* · `A2A_SERVE_SKILLS` = JSON 数组,**坏形拒启**(声明面静默丢=部署方以为发出去了、对端看不到,
|
|
2305
|
-
* 正是「无声 fail-open」;这里没有可用的降级语义)。
|
|
2306
|
-
*/
|
|
2307
1415
|
function parseA2aServe() {
|
|
2308
1416
|
if (!boolEnv("A2A_SERVE_ENABLED", false))
|
|
2309
1417
|
return undefined;
|
|
@@ -2323,11 +1431,6 @@ function parseA2aServe() {
|
|
|
2323
1431
|
if (parsed === undefined || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
|
|
2324
1432
|
throw new Error(`A2A_SERVE_URL must be a parseable http(s) URL (e.g. "https://agent.example.com/v1/a2a"); got ${JSON.stringify(rawUrl)}`);
|
|
2325
1433
|
}
|
|
2326
|
-
// 🔴 codex 轮2 R2-1(HIGH,验真后修):URL **userinfo** 段拒启。这个值被**逐字**放进 agent card,而卡是
|
|
2327
|
-
// 匿名可拉的公开文档 —— 一个看起来很合理的 Basic-auth 配置手滑(`https://user:pass@host/v1/a2a`)会把
|
|
2328
|
-
// 凭据发给每一个发现请求。方向取**拒启**而不是脱敏:脱敏会让部署方以为自己配的鉴权生效了(实际
|
|
2329
|
-
// 被抹掉),而这条路的鉴权本来就该走**头形**(R-11:RPC 持服务凭据)。同族先例=车1 自审第三镜给
|
|
2330
|
-
// client 侧读面做的 endpoint 脱敏,只是那边是**读别人的**卡、这边是**发我们自己的**卡,故更严。
|
|
2331
1434
|
if (parsed.username !== "" || parsed.password !== "") {
|
|
2332
1435
|
throw new Error(`A2A_SERVE_URL must not carry userinfo (credentials before "@") — it is published verbatim on the ANONYMOUSLY readable agent card, so those credentials would be handed to every discovery caller. ` +
|
|
2333
1436
|
`Inbound authentication on this endpoint is header-shaped (a service credential), not URL-embedded; strip the userinfo and keep the secret in the caller's headers.`);
|
|
@@ -2337,14 +1440,9 @@ function parseA2aServe() {
|
|
|
2337
1440
|
...(process.env.A2A_SERVE_DESCRIPTION ? { description: process.env.A2A_SERVE_DESCRIPTION } : {}),
|
|
2338
1441
|
url: rawUrl,
|
|
2339
1442
|
skills: parseA2aServeSkills(process.env.A2A_SERVE_SKILLS),
|
|
2340
|
-
// 服务端等待窗。**远小于** core 客户端腿的 10min 总预算(那是调用方的钱包,不是我们的连接池):
|
|
2341
|
-
// 一条 inbound HTTP 请求被按住的每一秒都占着一条连接与一个 socket,而协议自带 `tasks/get` 轮询
|
|
2342
|
-
// 出路,所以窗到期回 `working` 是**正常路径**、不是降级。上限 5min 是硬帽(比它更长请改用轮询)。
|
|
2343
1443
|
blockingWaitMs: numEnvBounded("A2A_SERVE_BLOCKING_WAIT_MS", "45000", 0, 300_000),
|
|
2344
1444
|
};
|
|
2345
1445
|
}
|
|
2346
|
-
/** `A2A_SERVE_SKILLS` 的逐条校验。**在场即须合法,否则拒启**——声明面没有「丢掉坏的那条继续跑」的
|
|
2347
|
-
* 合法语义:那会让部署方以为自己公告了一个技能而对端根本看不到(静默 fail-open 的教科书形)。 */
|
|
2348
1446
|
function parseA2aServeSkills(raw) {
|
|
2349
1447
|
if (raw === undefined || raw.trim() === "")
|
|
2350
1448
|
return [];
|
|
@@ -2433,7 +1531,6 @@ const INTEGRATIONS_GROUP_KEYS = [
|
|
|
2433
1531
|
"pluginsAllowHosts", "mcpServers", "a2aPeers", "a2aServe", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "gitApiKind", "defaultScenario",
|
|
2434
1532
|
"skillsDir", "configCenter", "configProvider", "configLocalDir",
|
|
2435
1533
|
];
|
|
2436
|
-
/** 组名 → 该组取景的平铺键(introspection 面:测试用它钉「每个平铺键恰好被一组取景」)。 */
|
|
2437
1534
|
export const CONFIG_GROUP_KEYS = {
|
|
2438
1535
|
store: STORE_GROUP_KEYS,
|
|
2439
1536
|
modelPlane: MODEL_PLANE_GROUP_KEYS,
|
|
@@ -2445,13 +1542,6 @@ export const CONFIG_GROUP_KEYS = {
|
|
|
2445
1542
|
observability: OBSERVABILITY_GROUP_KEYS,
|
|
2446
1543
|
integrations: INTEGRATIONS_GROUP_KEYS,
|
|
2447
1544
|
};
|
|
2448
|
-
/** 一组的**取景器**:每个键一个 accessor,读的时候才去平铺面取值。
|
|
2449
|
-
* ⚠️ 刻意不是 `{...pick(flat, keys)}` 的快照——快照会在 sema-registry 热应用(`config.model = …`、
|
|
2450
|
-
* `mutateInPlace(config.models, …)`)之后变成一份**静默过期**的旧值;取景器没有第二份存储,故不存在
|
|
2451
|
-
* 过期这个状态。`create*` 而非 `build*`:返回的是带行为(accessor)的活对象,不是纯数据。 */
|
|
2452
|
-
/** ⚠️ 语义边界(复审 2026-07-30 F4):组视图给**每个注册键**都装访问器,不看平铺面是否真有值——
|
|
2453
|
-
* `"tidb" in config.store` 恒 true、`Object.keys(config.store).length` 恒为注册表长度。本仓的
|
|
2454
|
-
* presence-as-semantics 判定(`"k" in x` / Object.keys 计数)**只能打平铺面**,组视图仅供点号读值。 */
|
|
2455
1545
|
function createGroupView(flat, keys) {
|
|
2456
1546
|
const view = {};
|
|
2457
1547
|
for (const key of keys) {
|
|
@@ -2459,13 +1549,6 @@ function createGroupView(flat, keys) {
|
|
|
2459
1549
|
}
|
|
2460
1550
|
return view;
|
|
2461
1551
|
}
|
|
2462
|
-
/** 把九个组视图挂到平铺配置上(**就地**,返回的就是同一个对象——视图必须看着这一个存储处)。
|
|
2463
|
-
*
|
|
2464
|
-
* 组键是**不可枚举**的:`Object.keys(config)` / `JSON.stringify(config)` / `{...config}` 的平铺面因此
|
|
2465
|
-
* 逐字节不变(既有消费点、既有 dump/审计面零影响),代价是 spread 克隆不带组视图——克隆后要用组形,
|
|
2466
|
-
* 再过一次本函数(读一个缺席的组是当场 TypeError,不会静默给旧值)。
|
|
2467
|
-
*
|
|
2468
|
-
* `loadConfig` 是主调用点;测试夹具(test/helpers/make-config.ts)构造 `ServiceConfigFlat` 后同样过它。 */
|
|
2469
1552
|
export function attachConfigGroups(flat) {
|
|
2470
1553
|
const groups = {
|
|
2471
1554
|
store: createGroupView(flat, STORE_GROUP_KEYS),
|
|
@@ -2484,39 +1567,19 @@ export function attachConfigGroups(flat) {
|
|
|
2484
1567
|
return flat;
|
|
2485
1568
|
}
|
|
2486
1569
|
export function loadConfig() {
|
|
2487
|
-
// #157:落点清槽必须排在**第一条**语句 —— 比下面那组同类复位还早一格,因为 rejectRetiredEnvs 本身会抛,
|
|
2488
|
-
// 而一轮抛出的 loadConfig 绝不能把**上一轮**成功解析的落点留在槽里被下一次排空读成本轮事实
|
|
2489
|
-
// (交叉复审第二轮,2026-08-06)。下面三格同族收集器在这条最早拒启路径上仍有同形残留,那是既有面、
|
|
2490
|
-
// 且都按 warn 排空,不在本件射程内。
|
|
2491
1570
|
EXEC_LANE_NOTICE = undefined;
|
|
2492
|
-
rejectRetiredEnvs();
|
|
2493
|
-
CONFIG_WARNINGS.length = 0;
|
|
1571
|
+
rejectRetiredEnvs();
|
|
1572
|
+
CONFIG_WARNINGS.length = 0;
|
|
2494
1573
|
CONFIG_NOTICES.length = 0;
|
|
2495
|
-
CONFIG_KNOBS.clear();
|
|
2496
|
-
// Posture gate: single-user turnkey (`REQUIRE_PRINCIPAL !== "true"` = one trusted super-admin on their own
|
|
2497
|
-
// box/tenant, CC's trust model) ⇒ the CC capability set defaults ON, each composed with its infra prereq; multi-tenant
|
|
2498
|
-
// (requirePrincipal) stays opt-in (gated + secured). This is the SAME discriminator LSP's host-default-ON uses,
|
|
2499
|
-
// generalized into one posture switch (the PM's "not per-flag" ask). In `postureOn`, an explicit `X_ENABLED=true/false`
|
|
2500
|
-
// ALWAYS wins; else the default = single-user ∧ infra-ready. 🔒 Multi-tenant (prod) is UNAFFECTED (postureOn → false).
|
|
2501
|
-
// Parsed ONCE here (design/158 B4) instead of re-comparing `process.env.REQUIRE_PRINCIPAL` at the four sites
|
|
2502
|
-
// that needed it — two of which spelled it `!== "true"` and two `=== "true"`, i.e. the same knob read in both
|
|
2503
|
-
// polarities within one function. `boolEnv(x, false)` is value-identical to the old `=== "true"`.
|
|
1574
|
+
CONFIG_KNOBS.clear();
|
|
2504
1575
|
const requirePrincipal = boolEnv("REQUIRE_PRINCIPAL", false);
|
|
2505
1576
|
const singleUserTurnkey = !requirePrincipal;
|
|
2506
|
-
// `postureOn` takes the NAME (not the pre-read value) so the knob can register itself in the polarity table —
|
|
2507
|
-
// the posture family is precisely the one whose default is NOT readable from its name, so leaving it out of the
|
|
2508
|
-
// self-report would omit the worst offenders. Its tri-state semantics are unchanged: an explicit literal wins,
|
|
2509
|
-
// anything else (including a typo — deliberately NOT warned here, unlike boolEnv) falls to the posture default.
|
|
2510
1577
|
const postureOn = (name, infraReady = true) => {
|
|
2511
|
-
// #241-rc2:显式臂与 boolEnv 同词表同拒启(parseBoolWord 单一属主)——三态语义不变
|
|
2512
|
-
// (显式 true/显式 false/未设 ⇒ posture 默认),变的只是「显式」的判读:`=1` 是显式 true,
|
|
2513
|
-
// 未知词拒启而不是静默滑进 posture 默认。
|
|
2514
1578
|
const explicit = parseBoolWord(name, process.env[name]);
|
|
2515
1579
|
const value = explicit ?? (singleUserTurnkey && infraReady);
|
|
2516
1580
|
registerKnob({ name, polarity: "posture", value, source: explicit === undefined ? "posture" : "env" });
|
|
2517
1581
|
return value;
|
|
2518
1582
|
};
|
|
2519
|
-
// ── 九域解析(纯解析)。调用次序贴合改前的 env 读序,故 CONFIG_NOTICES 的产出顺序逐条不变。
|
|
2520
1583
|
const store = parseStoreDomain({ requirePrincipal });
|
|
2521
1584
|
const modelPlane = parseModelDomain();
|
|
2522
1585
|
const approval = parseApprovalDomain({ postureOn });
|
|
@@ -2526,21 +1589,10 @@ export function loadConfig() {
|
|
|
2526
1589
|
const limitsHttp = parseLimitsHttpDomain();
|
|
2527
1590
|
const observability = parseObservabilityDomain();
|
|
2528
1591
|
const integrations = parseIntegrationsDomain();
|
|
2529
|
-
// ── CROSS-INVARIANT-REGION:begin ────────────────────────────────────────────────────────────────
|
|
2530
|
-
// 跨域耦合与跨域不变量:两条门(D-G 直连门、bake 门)的判据各自横跨 approval/auth/orchestration 三域,
|
|
2531
|
-
// 任何单域都不足以裁定,故留在装配层(旧名保留,断言与消息逐字不变)。
|
|
2532
|
-
// 🔴 #322:本区内的每一条拒启 throw 必须挂 `CROSS-INVARIANT:<id>` 锚,且 id 在
|
|
2533
|
-
// `src/config-invariants.ts` 的 CONFIG_CROSS_INVARIANTS 表内 —— 配置改热之后,一条**只活在 boot**
|
|
2534
|
-
// 的不变量就是热路径上的静默漏检。机械门:test/config-hot/batch1-cross-invariants.test.ts(双向)。
|
|
2535
1592
|
const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
|
|
2536
1593
|
const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, authTokens } = auth;
|
|
2537
1594
|
const { enabled: imageBakesEnabled, runnerPrincipal: bakeRunnerPrincipal } = orchestration.imageBakes;
|
|
2538
|
-
// 🔴 the direct-door crypto gate lives ONLY in the durable (checkpointStore) /decide branch — so DURABLE_APPROVAL
|
|
2539
|
-
// is a HARD precondition. Without it, checkpointStore is undefined and there is NO decide path at all
|
|
2540
|
-
// ([2354] A-5:legacy trusted-header /decide 车道已在 5.0.0 退役——本不变量从「防旁路」变成「防死门」,保留)。
|
|
2541
1595
|
const directDoorActive = directApprovalDoor && dgDurable && dgHmacKeys.length > 0 && principalJwtPubkeys.length > 0 && !!principalJwtIss && !!principalJwtAud;
|
|
2542
|
-
// 🔴 boot invariant (D-G §3): a half-configured direct door is the WORST state (looks on, verifies nothing) — and
|
|
2543
|
-
// an empty OPERATOR_PRINCIPALS makes isOperator() true-for-all (= cross-tenant escalation). Refuse to start.
|
|
2544
1596
|
if (directApprovalDoor) {
|
|
2545
1597
|
const missing = [];
|
|
2546
1598
|
if (!dgDurable)
|
|
@@ -2555,56 +1607,22 @@ export function loadConfig() {
|
|
|
2555
1607
|
missing.push("PRINCIPAL_JWT_AUD");
|
|
2556
1608
|
if (dgOperators.length === 0)
|
|
2557
1609
|
missing.push("OPERATOR_PRINCIPALS (else isOperator is true-for-all)");
|
|
2558
|
-
// 🔴 F-fix (dim-6): a direct door is inherently MULTI-TENANT (per-task verified JWT principal). Without
|
|
2559
|
-
// REQUIRE_PRINCIPAL=true an absent/invalid principal falls through as ANONYMOUS and the owner/quota checks
|
|
2560
|
-
// degrade to the spoofable header for null-owner rows — refuse a direct door that doesn't require a principal.
|
|
2561
1610
|
if (!requirePrincipal)
|
|
2562
1611
|
missing.push("REQUIRE_PRINCIPAL=true (a direct door is multi-tenant; without it an absent principal is anonymous and the spoofable header governs owner/quota checks)");
|
|
2563
1612
|
if (missing.length) {
|
|
2564
|
-
// CROSS-INVARIANT:direct-door-anchors
|
|
2565
1613
|
throw new Error(`DIRECT_APPROVAL_DOOR=true but D-G anchors are missing: ${missing.join(", ")} — refusing to start a half-open direct door (design/80 D-G boot invariant)`);
|
|
2566
1614
|
}
|
|
2567
1615
|
}
|
|
2568
|
-
// 🔴 boot invariant (#157-①, silent-downgrade-batch-20260805 普查候选 1/2): the BFF multi-tenant durable form
|
|
2569
|
-
// (DURABLE_APPROVAL=true + REQUIRE_PRINCIPAL=true, direct door OFF) had NO invariant — the D-G one above only
|
|
2570
|
-
// binds when DIRECT_APPROVAL_DOOR=true. On that form the four READ surfaces in approvals-assistant.ts
|
|
2571
|
-
// (GET /v1/approvals, /stream, /v1/assistant/inbox, /tasks) scope by the bare isOperator, whose empty-list
|
|
2572
|
-
// "true-for-all" lets ANY authenticated tenant principal list/subscribe every other tenant's pending queue
|
|
2573
|
-
// (redacted tool-input previews + decide-binding fields) and inspect arbitrary owners via ?owner. The WRITE
|
|
2574
|
-
// surfaces (decide/preempt/resume/plan_review) already use explicitOperatorOk and were never exposed. The
|
|
2575
|
-
// single-tenant legacy semantic (REQUIRE_PRINCIPAL unset — the shared service token IS the operator boundary)
|
|
2576
|
-
// is deliberately preserved: only the multi-tenant form is a hole. Refuse to start it.
|
|
2577
1616
|
if (dgDurable && requirePrincipal && dgOperators.length === 0) {
|
|
2578
|
-
// CROSS-INVARIANT:operator-principals-required
|
|
2579
1617
|
throw new Error("DURABLE_APPROVAL=true + REQUIRE_PRINCIPAL=true but OPERATOR_PRINCIPALS is empty — on a multi-tenant worker the empty operator set makes isOperator() true-for-all, so every authenticated tenant could read every other tenant's pending-approval queue (GET /v1/approvals & siblings). Set OPERATOR_PRINCIPALS, or unset REQUIRE_PRINCIPAL if this is a single-tenant deployment (#157-① boot invariant)");
|
|
2580
1618
|
}
|
|
2581
|
-
// 🔴 boot invariant (IMAGE-API-DESIGN.md §P2.4a): the bake door is build-host-RCE-capable, and isOperator([],p)
|
|
2582
|
-
// is true-for-all — so enabling bakes with an empty OPERATOR_PRINCIPALS = a WORLD-WRITABLE RCE door. Refuse to
|
|
2583
|
-
// start (never a half-open bake door). The explicit-operator gate in server.ts is the runtime twin of this.
|
|
2584
1619
|
if (imageBakesEnabled && dgOperators.length === 0) {
|
|
2585
|
-
// CROSS-INVARIANT:bake-operator-principals
|
|
2586
1620
|
throw new Error("IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host; an empty operator set makes isOperator() true-for-all (world-writable RCE). Set OPERATOR_PRINCIPALS or disable bakes (IMAGE-API-DESIGN.md §P2.4a boot invariant)");
|
|
2587
1621
|
}
|
|
2588
|
-
// 🔴 boot invariant (IMAGE-API-DESIGN.md §P2.12 / H1): the bake-runner identity is DERIVED from its bearer
|
|
2589
|
-
// credential (token-derived `source`), never a self-asserted header — so a SERVICE_AUTH_TOKENS entry whose
|
|
2590
|
-
// system == BAKE_RUNNER_PRINCIPAL MUST exist, else the runner can never satisfy isRunner and the entire
|
|
2591
|
-
// claim/ingest/heartbeat pipeline is silently dead. Refuse to start a bake door with no runner credential.
|
|
2592
1622
|
if (imageBakesEnabled && !Object.values(authTokens).includes(bakeRunnerPrincipal)) {
|
|
2593
|
-
// CROSS-INVARIANT:bake-runner-credential
|
|
2594
1623
|
throw new Error(`IMAGE_BAKES_ENABLED=true but no SERVICE_AUTH_TOKENS entry maps to the runner principal '${bakeRunnerPrincipal}' — the bake-runner authenticates by its credential (token-derived source), so a '<BAKE_RUNNER_TOKEN>=${bakeRunnerPrincipal}' SERVICE_AUTH_TOKENS entry is required or claim/ingest/heartbeat all 403 (IMAGE-API-DESIGN.md §P2.12 boot invariant)`);
|
|
2595
1624
|
}
|
|
2596
|
-
// 🔴 boot invariant #193 车4 件2(台账 P1-10 / docs/DESIGN-193-CAR4-leader-failclosed.md 件2):leader
|
|
2597
|
-
// 车道的两条「启用即损坏」组合,在 config 层 fail-closed。两门都只在 `LEADER_ENABLED=true` 时可能触发
|
|
2598
|
-
// —— 默认 false ⇒ 现行为逐位不变(负控在 config-validation.test.ts 里钉着)。
|
|
2599
|
-
//
|
|
2600
|
-
// 门①(host 腿 per-worker workspace 未接线):`leader/wire.ts` 里 worker 环境的 workspace 基准是
|
|
2601
|
-
// `cfg.workspace ?? "/home/user"` 这一条**绝对路径**,并贯穿 stageWorkerEnv / provisionIntegrationSandbox /
|
|
2602
|
-
// mkRepair / mkConflictResolver;而 `plugins/remote-env-host.ts` 给每个 host env 分的是**各不相同**的随机
|
|
2603
|
-
// 目录(`sema-host-<id>-<rand>`)——绝对路径不随之走。于是 N 个并发 host worker 全部落在同一个物理目录里
|
|
2604
|
-
// 竞态写 git 状态:不是报错,是**静默数据损坏**。真修(provisionWorker 给每个 worker 传唯一 workspace)
|
|
2605
|
-
// 不在本车;门先行,消灭「启用即损坏」的窗口。
|
|
2606
1625
|
if (orchestration.leaderEnabled && orchestration.remoteExec?.provider === "host") {
|
|
2607
|
-
// CROSS-INVARIANT:leader-host-lane
|
|
2608
1626
|
throw new Error("LEADER_ENABLED=true + REMOTE_EXEC=host is refused: the leader wire pins ONE absolute workspace path " +
|
|
2609
1627
|
"(LEADER_WORKSPACE / the /home/user default) for every sub-worker, while the host adapter gives each env a " +
|
|
2610
1628
|
"DIFFERENT random directory — so N concurrent host workers would share one physical git worktree and " +
|
|
@@ -2612,25 +1630,14 @@ export function loadConfig() {
|
|
|
2612
1630
|
"on an isolated lane (REMOTE_EXEC=e2b or k8s), or turn the leader off (unset LEADER_ENABLED) " +
|
|
2613
1631
|
"(#193 车4 件2 / staleness ledger P1-10 boot invariant)");
|
|
2614
1632
|
}
|
|
2615
|
-
// 门②(无外部 SQL 持久层):仓级前提是「durable state lives in an external SQL store」。没有 SQL 后端时
|
|
2616
|
-
// leader run 的登记表退化成进程内 Map ⇒ 跨副本 GET 404、重启即失忆、POST 重试直接起**第二次真 git push**。
|
|
2617
|
-
// 「多副本」这件事在 config 层判不了(副本数不是本进程的知识),可判的**代理**就是 durable 前提本身:
|
|
2618
|
-
// 一个没有外部 SQL 店的部署,与 leader run(N worker、最长 24h、真 push)的语义结构性不相容。
|
|
2619
1633
|
if (orchestration.leaderEnabled && store.dbBackend !== "mysql" && store.dbBackend !== "pg") {
|
|
2620
|
-
// CROSS-INVARIANT:leader-durable-store
|
|
2621
1634
|
throw new Error(`LEADER_ENABLED=true but DB_BACKEND=${store.dbBackend} (no external SQL store) is refused: a leader run is an ` +
|
|
2622
1635
|
"N-worker fan-out lasting up to 24h that ends in a REAL git push, and without a durable store its registry is " +
|
|
2623
1636
|
"a per-process Map — a cross-replica GET 404s, a restart forgets the run, and a retried POST starts a SECOND " +
|
|
2624
1637
|
"push. Point the service at a durable store (DB_BACKEND=mysql|pg), or turn the leader off (unset LEADER_ENABLED) " +
|
|
2625
1638
|
"(#193 车4 件2 / staleness ledger P1-9+P1-10 boot invariant)");
|
|
2626
1639
|
}
|
|
2627
|
-
// ── CROSS-INVARIANT-REGION:end ──────────────────────────────────────────────────────────────────
|
|
2628
|
-
// #157 F 类观测:执行车道落点记账放在这里 —— 九域解析与上面全部跨域不变量都过了之后,即「这一轮
|
|
2629
|
-
// 确实启得起来」才成立的位置(写在 REMOTE_EXEC 解析途中的话,后置拒启会留下一条没启起来的落点)。
|
|
2630
1640
|
EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec);
|
|
2631
|
-
// 平铺面**单点展开**九域产物(唯一存储处),再挂上九个组视图。组视图是对这同一个对象的取景器,
|
|
2632
|
-
// 不是第二份拷贝 —— 故 `config.limitsHttp.port === config.port` 恒成立,含热应用之后(见
|
|
2633
|
-
// attachConfigGroups / config-types.ts 分组段)。
|
|
2634
1641
|
return attachConfigGroups({
|
|
2635
1642
|
...store,
|
|
2636
1643
|
...modelPlane,
|
|
@@ -2641,34 +1648,11 @@ export function loadConfig() {
|
|
|
2641
1648
|
...limitsHttp,
|
|
2642
1649
|
...observability,
|
|
2643
1650
|
...integrations,
|
|
2644
|
-
directDoorActive,
|
|
1651
|
+
directDoorActive,
|
|
2645
1652
|
});
|
|
2646
1653
|
}
|
|
2647
|
-
/** 有效监听绑址([1934])。**显式 `BIND_HOST`/`HOST` 恒生效**(operator 保留在任何形下明示暴露的
|
|
2648
|
-
* 权利);未显式时:**无鉴权写面 ⇒ 127.0.0.1**,其余 ⇒ undefined(Node 默认全接口,既有部署零影响)。
|
|
2649
|
-
*
|
|
2650
|
-
* 为什么缺省要自己收窄、而不是只加旋钮:桌面形的安全结论原本写着「safe ONLY because HOST is
|
|
2651
|
-
* pinned to 127.0.0.1」,而那个前提在本仓从未成立(HOST 零消费)——把一个没人实现的假设当防线,
|
|
2652
|
-
* 是「沉默陷阱」。零凭证 + 全接口 + host lane(用户真机非沙箱)= 同网段任意执行,缺省必须安全。
|
|
2653
|
-
* "无鉴权写面" 的判据与 http 层 503 fail-closed 门同源:allowUnauthedWrites 且**完全没有**凭证
|
|
2654
|
-
* (半配=凭证必须使用,不算无鉴权)。 */
|
|
2655
|
-
/**
|
|
2656
|
-
* 缝合审 M4:per-request 配速上限旋钮(TASK_TIMEOUT_MAX_SEC / TASK_MAX_OUTPUT_TOKENS_MAX /
|
|
2657
|
-
* TASK_MAX_TURNS_MAX)的解析 —— 判据提成函数放到 env 解析的家里。
|
|
2658
|
-
*
|
|
2659
|
-
* 病:旧码是 `Math.floor(Number(process.env[name] ?? "0"))`,非有限或 ≤0 ⇒ 回 undefined ⇒ **该上限
|
|
2660
|
-
* 不设**。而这三个旋钮的全部用途就是给多租部署封顶 caller 自报的配速,于是打错一个字符(`50s`、
|
|
2661
|
-
* `5_000`、全角数字)⇒ 封顶静默消失,caller 从此可自报任意 timeout/tokens/turns。fail-open 且零痕迹。
|
|
2662
|
-
*
|
|
2663
|
-
* 本仓**已经有**为这种情况建的渠道(`CONFIG_WARNINGS` / `drainConfigWarnings`,注释逐字写着
|
|
2664
|
-
* 「soft-knob parse rejects … main.ts drains this once after createLogger and warns per entry」),
|
|
2665
|
-
* 这三个旋钮没走它。这里只加**可观测性**,不改解析语义:非法值仍不设限(改成拒启会把一个打错字的
|
|
2666
|
-
* 运营方整个挡在门外,代价与收益不成比例),但它必须出声。
|
|
2667
|
-
*
|
|
2668
|
-
* @returns `{}` = 缺席(正常的「不设限」);`{ value }` = 生效;`{ invalid }` = 存在但非法(不设限 + 该告警)。
|
|
2669
|
-
*/
|
|
2670
1654
|
export function parseCapEnv(name, raw) {
|
|
2671
|
-
void name;
|
|
1655
|
+
void name;
|
|
2672
1656
|
if (raw === undefined)
|
|
2673
1657
|
return {};
|
|
2674
1658
|
const n = Math.floor(Number(raw));
|
|
@@ -2676,21 +1660,6 @@ export function parseCapEnv(name, raw) {
|
|
|
2676
1660
|
return { invalid: raw };
|
|
2677
1661
|
return { value: n };
|
|
2678
1662
|
}
|
|
2679
|
-
/**
|
|
2680
|
-
* 缝合审 M6:数据根与配置根是否分家 —— 判据提成函数,放在 `localDataRoot` 自己的文件里。
|
|
2681
|
-
*
|
|
2682
|
-
* 两条解析各写各的:数据根吃 `LOCAL_DATA_ROOT`,配置根(`main.ts` 的 `localRoot` / `run-local.ts` 的
|
|
2683
|
-
* `root`)不吃。只设 `LOCAL_DATA_ROOT=/data/sema` 的部署因此把 store 数据挪到了 /data/sema,而
|
|
2684
|
-
* config.d / remote-exec.json 仍从 `~/.ai-agent` 读 —— 静默分家,而设它的人多半以为改的是「本地数据放哪」。
|
|
2685
|
-
*
|
|
2686
|
-
* 讽刺处:`main.ts` 那段的标题逐字写着「ONE local root for BOTH … avoids a split-brain」——那条论证
|
|
2687
|
-
* 合并了 config provider 与 remoteExec 两个根,**漏了第三个**。
|
|
2688
|
-
*
|
|
2689
|
-
* 这里**不改解析**(让配置根跟着 `LOCAL_DATA_ROOT` 走,会让现有部署的 config.d 当场失踪 —— 代价比
|
|
2690
|
-
* 分家本身大得多)。只回一个「分家了,两个根分别是什么」的事实,由 boot 期打告警,合一旋钮 = `CONFIG_LOCAL_DIR`。
|
|
2691
|
-
*
|
|
2692
|
-
* @returns 分家时回两个根;一致(或已被 CONFIG_LOCAL_DIR/AGENT_DATA_DIR 统一)时回 undefined。
|
|
2693
|
-
*/
|
|
2694
1663
|
export function splitLocalRoots(a) {
|
|
2695
1664
|
const configRoot = a.configLocalDir ?? a.agentDataDir ?? join(a.home, ".ai-agent");
|
|
2696
1665
|
if (!a.localDataRoot || a.localDataRoot === configRoot)
|
|
@@ -2700,13 +1669,6 @@ export function splitLocalRoots(a) {
|
|
|
2700
1669
|
export function resolveBindHost(config) {
|
|
2701
1670
|
const anyServiceAuth = Boolean(config.authToken) || Object.keys(config.authTokens ?? {}).length > 0;
|
|
2702
1671
|
const mustNarrow = Boolean(config.allowUnauthedWrites) && !anyServiceAuth;
|
|
2703
|
-
// 缝合审 M5:`bindHost` = `BIND_HOST || HOST`,而 zsh 常设 `HOST=<机器名>` —— 用户从未设过它。
|
|
2704
|
-
// 旧码第一行就是 `if (config.bindHost) return config.bindHost`,于是一个**继承来的** shell 变量
|
|
2705
|
-
// 静默击穿了下面那条无鉴权自收窄:写面无鉴权 + 绑到机器名解析出的 LAN 地址 = 同网段任何人可在
|
|
2706
|
-
// 用户真机上提交任务并执行。触发条件是「什么都没做」,这是它比一般配置错误危险的地方。
|
|
2707
|
-
// [2062]③ 已经识别出「HOST 是暗通道」并加了启动 warn,但那个识别没接到这条判据上 —— 现在接上:
|
|
2708
|
-
// **只在收窄条件成立时**让收窄压过继承的 HOST(`HOST` 在 Node 生态确有绑址惯例,不能当它不存在);
|
|
2709
|
-
// 显式 `BIND_HOST` 恒生效,operator 明示暴露的权利一点不动。main.ts 会为被压过的那次打专门告警。
|
|
2710
1672
|
if (config.bindHost && !(mustNarrow && config.bindHostSource === "HOST"))
|
|
2711
1673
|
return config.bindHost;
|
|
2712
1674
|
return mustNarrow ? "127.0.0.1" : undefined;
|