@sema-agent/server 7.2.0 → 7.4.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/README.md +2 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +26 -1
- package/dist/approval-ask-machine.d.ts +39 -0
- package/dist/approval-ask-machine.js +101 -0
- package/dist/approval-card.d.ts +244 -0
- package/dist/approval-card.js +237 -0
- package/dist/approval-deny-reasons.d.ts +56 -0
- package/dist/approval-deny-reasons.js +54 -0
- package/dist/approval-reconciler.d.ts +174 -0
- package/dist/approval-reconciler.js +307 -0
- package/dist/boot/coordinators.d.ts +1 -0
- package/dist/boot/coordinators.js +39 -4
- package/dist/boot/deferred-sandbox-path-env.d.ts +99 -0
- package/dist/boot/deferred-sandbox-path-env.js +279 -0
- package/dist/boot/execution-env.js +11 -1
- package/dist/boot/lexical-path-env.d.ts +10 -0
- package/dist/boot/lexical-path-env.js +88 -0
- package/dist/boot/reapers.d.ts +34 -0
- package/dist/boot/reapers.js +198 -23
- package/dist/boot/resolve-spec.js +97 -33
- package/dist/capabilities/center-prompts.js +4 -1
- package/dist/capabilities/oa-tools.d.ts +15 -0
- package/dist/capabilities/oa-tools.js +54 -0
- package/dist/config-types.d.ts +68 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +138 -2
- package/dist/elicitation.d.ts +4 -0
- package/dist/elicitation.js +7 -3
- package/dist/finance/cost-taxonomy.d.ts +34 -0
- package/dist/finance/cost-taxonomy.js +26 -0
- package/dist/hooks/hook-runner.js +32 -0
- package/dist/http/routes/capabilities.js +14 -0
- package/dist/http/routes/diagnostics.d.ts +84 -0
- package/dist/http/routes/diagnostics.js +140 -0
- package/dist/http/routes/runs.d.ts +1 -0
- package/dist/http/routes/runs.js +548 -16
- package/dist/http/routes/tasks.js +175 -12
- package/dist/http/server.d.ts +6 -1
- package/dist/http/server.js +120 -4
- package/dist/http/sse-log.d.ts +51 -0
- package/dist/http/sse-log.js +64 -0
- package/dist/http/wire-types.d.ts +20 -5
- package/dist/main.js +35 -4
- package/dist/observability/fail-open.d.ts +98 -0
- package/dist/observability/fail-open.js +216 -0
- package/dist/observability/prompt-manifest.d.ts +13 -0
- package/dist/observability/prompt-manifest.js +8 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +38 -0
- package/dist/plugins/approval-ask-store-memory.js +299 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +341 -0
- package/dist/plugins/approval-ask-store-sql.js +705 -0
- package/dist/plugins/approval-store-sql.d.ts +116 -0
- package/dist/plugins/approval-store-sql.js +151 -0
- package/dist/plugins/background-agent-store-sql.js +20 -1
- package/dist/plugins/checkpoint-store-sql.d.ts +84 -9
- package/dist/plugins/checkpoint-store-sql.js +297 -16
- package/dist/plugins/file-workflow-journal-store.d.ts +12 -0
- package/dist/plugins/file-workflow-journal-store.js +12 -0
- package/dist/plugins/local-checkpoint-store.d.ts +6 -5
- package/dist/plugins/local-checkpoint-store.js +4 -0
- package/dist/plugins/pg-approval-store.d.ts +9 -0
- package/dist/plugins/pg-approval-store.js +9 -0
- package/dist/plugins/pg-breaker-state.d.ts +8 -0
- package/dist/plugins/pg-breaker-state.js +8 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +10 -0
- package/dist/plugins/pg-checkpoint-store.js +10 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +8 -0
- package/dist/plugins/pg-file-snapshot-store.js +8 -0
- package/dist/plugins/pg-image-bake.d.ts +12 -0
- package/dist/plugins/pg-image-bake.js +11 -0
- package/dist/plugins/pg-image-index.d.ts +12 -0
- package/dist/plugins/pg-image-index.js +11 -0
- package/dist/plugins/pg-outcome-ledger.d.ts +12 -0
- package/dist/plugins/pg-outcome-ledger.js +11 -0
- package/dist/plugins/pg-pool.js +11 -0
- package/dist/plugins/pg-resume-anchor-store.d.ts +7 -0
- package/dist/plugins/pg-resume-anchor-store.js +7 -0
- package/dist/plugins/pg-run-store.d.ts +9 -0
- package/dist/plugins/pg-run-store.js +9 -0
- package/dist/plugins/pg-session-policy-store.d.ts +7 -0
- package/dist/plugins/pg-session-policy-store.js +7 -0
- package/dist/plugins/pg-session-store.d.ts +12 -0
- package/dist/plugins/pg-session-store.js +12 -0
- package/dist/plugins/pg-tool-result-store.d.ts +9 -0
- package/dist/plugins/pg-tool-result-store.js +9 -0
- package/dist/plugins/pg-workflow-journal-store.d.ts +9 -0
- package/dist/plugins/pg-workflow-journal-store.js +9 -0
- package/dist/plugins/pg-workflow-run-store.d.ts +9 -0
- package/dist/plugins/pg-workflow-run-store.js +9 -0
- package/dist/plugins/store-backend.d.ts +18 -0
- package/dist/plugins/store-backend.js +10 -0
- package/dist/plugins/tidb-approval-store.d.ts +8 -0
- package/dist/plugins/tidb-approval-store.js +8 -0
- package/dist/plugins/tidb-breaker-state.d.ts +7 -0
- package/dist/plugins/tidb-breaker-state.js +7 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +9 -0
- package/dist/plugins/tidb-checkpoint-store.js +9 -0
- package/dist/plugins/tidb-file-snapshot-store.d.ts +8 -0
- package/dist/plugins/tidb-file-snapshot-store.js +8 -0
- package/dist/plugins/tidb-image-bake.d.ts +12 -0
- package/dist/plugins/tidb-image-bake.js +11 -0
- package/dist/plugins/tidb-image-index.d.ts +12 -0
- package/dist/plugins/tidb-image-index.js +11 -0
- package/dist/plugins/tidb-outcome-ledger.d.ts +12 -0
- package/dist/plugins/tidb-outcome-ledger.js +12 -0
- package/dist/plugins/tidb-pool.js +27 -4
- package/dist/plugins/tidb-resume-anchor-store.d.ts +7 -0
- package/dist/plugins/tidb-resume-anchor-store.js +7 -0
- package/dist/plugins/tidb-run-store.d.ts +10 -0
- package/dist/plugins/tidb-run-store.js +9 -0
- package/dist/plugins/tidb-session-policy-store.d.ts +7 -0
- package/dist/plugins/tidb-session-policy-store.js +7 -0
- package/dist/plugins/tidb-tool-result-store.d.ts +8 -0
- package/dist/plugins/tidb-tool-result-store.js +10 -0
- package/dist/plugins/tidb-workflow-journal-store.d.ts +9 -0
- package/dist/plugins/tidb-workflow-journal-store.js +9 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts +10 -0
- package/dist/plugins/tidb-workflow-run-store.js +10 -0
- package/dist/plugins/workflow-journal-limits.d.ts +12 -0
- package/dist/plugins/workflow-journal-limits.js +12 -0
- package/dist/question.d.ts +21 -14
- package/dist/question.js +83 -34
- package/dist/runs.d.ts +16 -1
- package/dist/runs.js +61 -3
- package/dist/runtime-caps-resolver.d.ts +7 -1
- package/dist/runtime-caps-resolver.js +65 -3
- package/dist/sema-registry.d.ts +41 -0
- package/dist/sema-registry.js +40 -0
- package/dist/spec-fields.d.ts +4 -0
- package/dist/spec-fields.js +6 -0
- package/dist/task-settings.d.ts +36 -15
- package/dist/task-settings.js +19 -5
- package/dist/tool-approval.d.ts +296 -3
- package/dist/tool-approval.js +1074 -50
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.js +14 -1
- package/dist/trace/project.d.ts +90 -0
- package/dist/trace/project.js +188 -0
- package/package.json +5 -4
package/dist/boot/reapers.js
CHANGED
|
@@ -15,9 +15,66 @@
|
|
|
15
15
|
import { Runner, defaultTaskRegistry } from "@sema-agent/core";
|
|
16
16
|
import { sweepStaleScratchpads } from "../env-facts.js";
|
|
17
17
|
import { CostQuota } from "../observability/cost-quota.js";
|
|
18
|
+
import { createApprovalReconciler } from "../approval-reconciler.js";
|
|
19
|
+
/** #157 / [2762]§五(sema-comms `audits/failopen-governance-176.md` §7 "Z 类 1 条"):default consecutive-failure
|
|
20
|
+
* count before a reaper leg's swallowed error gets a `warn` line. The ~15 `.catch(() => undefined)` sites
|
|
21
|
+
* below are correct in DIRECTION (a hiccuping periodic sweep must never crash the whole tick loop — one bad
|
|
22
|
+
* leg cannot be allowed to starve its ~15 siblings), but were zero-observability: an operator watching only
|
|
23
|
+
* `warn`/`error` would never learn a leg had been silently broken for days. `5` (not `1`) is deliberate —
|
|
24
|
+
* a transient blip (one DB reconnect, one timeout) is exactly what fail-open exists to absorb without noise;
|
|
25
|
+
* only a STREAK crosses into "diagnosable, not transient". Exported so a future per-leg override is a
|
|
26
|
+
* constructor-arg change, not a magic-number hunt. */
|
|
27
|
+
export const REAPER_FAILURE_WARN_THRESHOLD = 5;
|
|
28
|
+
/**
|
|
29
|
+
* Builds a NAMED, stateful throttle for one reaper leg: attach as `.then(() => guard.onSuccess(), guard.onError)`
|
|
30
|
+
* in place of a bare `.catch(() => undefined)` (the two-arg `.then` form still catches a rejection from any
|
|
31
|
+
* earlier link in the chain — same coverage as the `.catch()` it replaces — while also observing the FULFILLED
|
|
32
|
+
* path, which a plain `.catch()` cannot). Behavior this closure owns, unchanged reaper scheduling/cleanup:
|
|
33
|
+
* - a successful run resets the consecutive-failure streak to 0 (one hiccup mid-outage doesn't inflate the count);
|
|
34
|
+
* - a failure increments the streak; only once the streak reaches `threshold` (default
|
|
35
|
+
* {@link REAPER_FAILURE_WARN_THRESHOLD}) does ONE `warn` line fire (reaper name + streak length + the most
|
|
36
|
+
* recent error's message) — then the streak resets to 0 so a STILL-broken leg reports again after another
|
|
37
|
+
* full `threshold` run, rather than going silent forever after its first report.
|
|
38
|
+
* One instance must be created ONCE per named leg (outside the tick's `setInterval` callback) so the streak
|
|
39
|
+
* survives across ticks — a fresh instance per tick would never accumulate past 1.
|
|
40
|
+
*/
|
|
41
|
+
export function createThrottledReaperCatch(name, logger, threshold = REAPER_FAILURE_WARN_THRESHOLD) {
|
|
42
|
+
let streak = 0;
|
|
43
|
+
return {
|
|
44
|
+
onSuccess: () => {
|
|
45
|
+
streak = 0;
|
|
46
|
+
},
|
|
47
|
+
onError: (err) => {
|
|
48
|
+
streak += 1;
|
|
49
|
+
if (streak < threshold)
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
// #157 补痕交叉复审真发现(codex,2026-08-05):`String(err)` on a pathological rejection reason
|
|
53
|
+
// (a throwing `Symbol.toPrimitive`/`toString`) or a throwing `logger.warn` transport would otherwise
|
|
54
|
+
// make THIS handler throw — and since onError is wired as a `.then(fulfilled, onError)` REJECTED
|
|
55
|
+
// branch with no further `.catch` downstream (every reapers.ts call site), that throw becomes a
|
|
56
|
+
// floating rejected promise (an unhandled rejection under Node's default handling can crash the
|
|
57
|
+
// process). The ORIGINAL `.catch(() => undefined)` this replaces could never do that — a failure
|
|
58
|
+
// in the REPORTING of a swallowed leg failure must not be worse than the silence it replaces, so
|
|
59
|
+
// this whole block is wrapped total/non-throwing.
|
|
60
|
+
logger.warn("reaper_leg_repeated_failure", {
|
|
61
|
+
reaper: name,
|
|
62
|
+
consecutiveFailures: streak,
|
|
63
|
+
lastError: err instanceof Error ? err.message : String(err),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// observability must never itself become the outage — see comment above.
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
streak = 0; // re-open the window — a leg that stays broken reports again every `threshold` failures, not once ever.
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
18
75
|
/** 起后台维护 tick,返回定时器 handle(收尾段 clearInterval 用)。 */
|
|
19
76
|
export function startReapers(ctx) {
|
|
20
|
-
const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, getRunDenySweep, } = ctx;
|
|
77
|
+
const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, getRunDenySweep, } = ctx;
|
|
21
78
|
// S7 (SILENT-FALLBACK P0-d): the sweeps' return counts were discarded — an instance death that batch-fails
|
|
22
79
|
// N orphans was indistinguishable from organic failures. Count + log ONLY when a sweep flipped rows (>0),
|
|
23
80
|
// so healthy ticks stay silent. Tolerant of void-returning stores (typeof guard).
|
|
@@ -31,6 +88,54 @@ export function startReapers(ctx) {
|
|
|
31
88
|
let attachmentSweepInFlight = false; // 复审 F1:附件孤儿 sweep 的重入守卫(同上)
|
|
32
89
|
let bgAgentReapInFlight = false; // core 1.364: same serialization for the durable bg-agent joint reap (per-scope serial loop)
|
|
33
90
|
let worktreeReapInFlight = false; // A10 留档发现①(2026-07-29):worktreeReap 重入守卫(同上三个邻居——见下方调用点注释)
|
|
91
|
+
// #157 / [2762]§五 Z 类补痕:one NAMED throttled-catch guard per periodic leg below (created ONCE here, not
|
|
92
|
+
// per-tick, so each leg's consecutive-failure streak survives across ticks — see createThrottledReaperCatch).
|
|
93
|
+
const probeClockSkewGuard = createThrottledReaperCatch("probe_clock_skew", logger);
|
|
94
|
+
const runsReapStaleGuard = createThrottledReaperCatch("runs_reap_stale", logger);
|
|
95
|
+
const checkpointsReapExpiredGuard = createThrottledReaperCatch("checkpoints_reap_expired", logger);
|
|
96
|
+
const runsFailSuspendedExpiredCheckpointGuard = createThrottledReaperCatch("runs_fail_suspended_expired_checkpoint", logger);
|
|
97
|
+
const runDenySweepGuard = createThrottledReaperCatch("run_deny_sweep", logger);
|
|
98
|
+
const runsReapSuspendedGuard = createThrottledReaperCatch("runs_reap_suspended", logger);
|
|
99
|
+
const checkpointReapCtxGuard = createThrottledReaperCatch("checkpoint_reap_ctx", logger);
|
|
100
|
+
const toolResultReapOlderThanGuard = createThrottledReaperCatch("tool_result_reap_older_than", logger);
|
|
101
|
+
const fileSnapshotSweepOrphanBlobsGuard = createThrottledReaperCatch("file_snapshot_sweep_orphan_blobs", logger);
|
|
102
|
+
const attachmentReapUnboundGuard = createThrottledReaperCatch("attachment_reap_unbound", logger);
|
|
103
|
+
const sessionSweepStagingGuard = createThrottledReaperCatch("session_sweep_staging", logger);
|
|
104
|
+
const imageBakesReapStaleGuard = createThrottledReaperCatch("image_bakes_reap_stale", logger);
|
|
105
|
+
const worktreeReapGuard = createThrottledReaperCatch("worktree_reap", logger);
|
|
106
|
+
const workflowNotifyRecoverGuard = createThrottledReaperCatch("workflow_notify_recover", logger);
|
|
107
|
+
const workflowJournalReapExpiredGuard = createThrottledReaperCatch("workflow_journal_reap_expired", logger);
|
|
108
|
+
const workflowRunReapAllScopesGuard = createThrottledReaperCatch("workflow_run_reap_all_scopes", logger);
|
|
109
|
+
const workflowNotifyJournalReapAckedGuard = createThrottledReaperCatch("workflow_notify_journal_reap_acked", logger);
|
|
110
|
+
const bgAgentsReapGuard = createThrottledReaperCatch("bg_agents_reap", logger);
|
|
111
|
+
const rosterReapOlderThanGuard = createThrottledReaperCatch("roster_reap_older_than", logger);
|
|
112
|
+
const scratchpadSweepStaleGuard = createThrottledReaperCatch("scratchpad_sweep_stale", logger);
|
|
113
|
+
const approvalReconcileGuard = createThrottledReaperCatch("approval_reconcile", logger);
|
|
114
|
+
let approvalReconcileInFlight = false; // 车5 §8 C-8:重入守卫(邻居四先例同形——本腿时长随 PARKING 行数增长)
|
|
115
|
+
// #151 车5(design/172 §3.0 对账收敛器 + 崩溃恢复扫描):**总开关默认 OFF ⇒ 本腿根本不注册**
|
|
116
|
+
// (`STREAM_APPROVAL_ENABLED=false` 的部署一行库都不扫,零新增查询)。开且有 durable backend 时才建。
|
|
117
|
+
// 判据 1/2 的两个读口按能力面接:checkpoint 店的窄读口只有 SQL twins 有(local 车道的店没有这个面),
|
|
118
|
+
// 缺席 ⇒ 判据 1 恒不命中(诚实降级,行落 ②③④⑤),不是「假装没 checkpoint 就该 DENY」。
|
|
119
|
+
const approvalReconciler = (() => {
|
|
120
|
+
if (!config.streamApproval.enabled || !backend)
|
|
121
|
+
return undefined;
|
|
122
|
+
// duck probe(邻居同形:本文件 `sweepStagingSessions` / `reapAllScopes` 都是这个姿势)——
|
|
123
|
+
// `CheckpointStoreFull` 的 local 成员没有这个窄读口,它是 SQL twins 的扩展面。
|
|
124
|
+
const cpPort = checkpointStore;
|
|
125
|
+
return createApprovalReconciler({
|
|
126
|
+
askStore: backend.approvalAsk(),
|
|
127
|
+
checkpoints: typeof cpPort?.findCheckpointCandidatesForAsk === "function" ? cpPort : undefined,
|
|
128
|
+
// RunStore 三形(TiDB/Pg/File)都带 `getRun`,故直接接、无 probe。
|
|
129
|
+
runs: runStore,
|
|
130
|
+
logger,
|
|
131
|
+
metrics,
|
|
132
|
+
batchLimit: config.streamApproval.reconcileBatch,
|
|
133
|
+
pendingGraceMs: config.streamApproval.pendingGraceMs,
|
|
134
|
+
adhocGraceMs: config.streamApproval.adhocGraceMs,
|
|
135
|
+
orphanTtlMs: config.streamApproval.orphanTtlMs,
|
|
136
|
+
...(toolApproval ? { emitRevoke: (frame, target) => void toolApproval.emitRevoke(frame, target) } : {}),
|
|
137
|
+
});
|
|
138
|
+
})();
|
|
34
139
|
// core 1.364([1503] 提货单③):retention **只走** reapDurableAgents(联合 reap:条件删赢了才 release
|
|
35
140
|
// 转录 session;裸 store.reap 会 strand 转录)。staleRunning 翻转在 core 编排内先行(store.reap 一步)。
|
|
36
141
|
// scope 枚举:core 1.368 `BackgroundAgentStore.listScopes?()`([1516]② 交付,SQL twins/File 双实现
|
|
@@ -87,7 +192,17 @@ export function startReapers(ctx) {
|
|
|
87
192
|
}
|
|
88
193
|
catch (err) {
|
|
89
194
|
failedScopes++;
|
|
90
|
-
|
|
195
|
+
// codex 交叉复审第三轮真发现(2026-08-05):a poison `err` (throwing `Symbol.toPrimitive`/`toString`)
|
|
196
|
+
// or a throwing `logger.warn` must not escape THIS catch — the outer `bgAgentsReapGuard` (this
|
|
197
|
+
// car's own wiring) already stops it from becoming an unhandled rejection, but an uncaught throw
|
|
198
|
+
// HERE would abort the `for` loop early and skip every remaining scope, defeating the per-scope
|
|
199
|
+
// isolation this block exists for ("[1522] LOW3" above — one bad scope must not starve the rest).
|
|
200
|
+
try {
|
|
201
|
+
logger.warn("bg_agent_reap_scope_failed", { scope, err: err instanceof Error ? err.message : String(err) });
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// observability must never break the per-scope loop — see comment above.
|
|
205
|
+
}
|
|
91
206
|
}
|
|
92
207
|
}
|
|
93
208
|
if (failedScopes > 0)
|
|
@@ -116,8 +231,11 @@ export function startReapers(ctx) {
|
|
|
116
231
|
}
|
|
117
232
|
};
|
|
118
233
|
const reaper = setInterval(() => {
|
|
119
|
-
void probeClockSkew().
|
|
120
|
-
void runStore
|
|
234
|
+
void probeClockSkew().then(() => probeClockSkewGuard.onSuccess(), probeClockSkewGuard.onError); // S10
|
|
235
|
+
void runStore
|
|
236
|
+
?.reapStale(config.runStaleSec * 1000)
|
|
237
|
+
.then(reapCount("runs_reaped_total", { kind: "stale" }))
|
|
238
|
+
.then(() => runsReapStaleGuard.onSuccess(), runsReapStaleGuard.onError);
|
|
121
239
|
// Durable F4 (design/45) + design/80 §3 inv#3 crash-safe backstop: CAS-expire checkpoints past their
|
|
122
240
|
// deadline OR their absolute terminal_at backstop (≈ deny), then fail the suspended run rows whose
|
|
123
241
|
// checkpoint was thereby expired (release task_active = unlock the session). These run EVERY tick,
|
|
@@ -131,32 +249,51 @@ export function startReapers(ctx) {
|
|
|
131
249
|
// 真 pin 只存在于 in-memory TtlSessionStore;本部署 durable checkpoint 恒配 durable session 后端
|
|
132
250
|
// (pin=no-op),deny-sweep 走 resumeCheckpoint=core 内部 unpin ✓。SESSION_BACKEND=memory+durable
|
|
133
251
|
// checkpoint 的 niche dev 组合下 abort-expire 臂会把被钉会话泄到进程终——显式接受,不为 dev 形加腿。
|
|
134
|
-
void checkpointStore
|
|
135
|
-
|
|
252
|
+
void checkpointStore
|
|
253
|
+
.reapExpired(Date.now())
|
|
254
|
+
.then(reapCount("checkpoints_reaped_total", {}))
|
|
255
|
+
.then(() => checkpointsReapExpiredGuard.onSuccess(), checkpointsReapExpiredGuard.onError);
|
|
256
|
+
void runStore
|
|
257
|
+
?.failSuspendedWithExpiredCheckpoint()
|
|
258
|
+
.then(reapCount("runs_reaped_total", { kind: "expired_checkpoint" }))
|
|
259
|
+
.then(() => runsFailSuspendedExpiredCheckpointGuard.onSuccess(), runsFailSuspendedExpiredCheckpointGuard.onError);
|
|
136
260
|
// D-D SLA-timer: resolve-DENY human/irreversible_ask gates past their deadline (graceful — the model
|
|
137
261
|
// continues with the denial), vs the abort reapExpired gives the other kinds. Bounded per tick.
|
|
138
|
-
void getRunDenySweep()?.(Date.now())
|
|
262
|
+
void getRunDenySweep()?.(Date.now())
|
|
263
|
+
.then(() => runDenySweepGuard.onSuccess(), runDenySweepGuard.onError); // A10 搬运改写:晚绑取值(原 `runDenySweep?.`)
|
|
139
264
|
}
|
|
140
265
|
// The finer, OPT-IN time-based suspended-run reaper (APPROVAL_TIMEOUT_SEC). Stays gated — reapSuspended
|
|
141
266
|
// with a 0 TTL would nuke ALL suspended rows (cutoff = now). The absolute backstop above is the safety
|
|
142
267
|
// floor; this is the operator-chosen deadline. ([2354] A-5:F4 轮询店的 expireStale 腿随车道退役。)
|
|
143
268
|
if (config.approvalTimeoutSec > 0) {
|
|
144
|
-
void runStore
|
|
269
|
+
void runStore
|
|
270
|
+
?.reapSuspended(config.approvalTimeoutSec * 1000)
|
|
271
|
+
.then(reapCount("runs_reaped_total", { kind: "suspended" }))
|
|
272
|
+
.then(() => runsReapSuspendedGuard.onSuccess(), runsReapSuspendedGuard.onError);
|
|
145
273
|
}
|
|
146
274
|
// GC checkpoint_ctx rows whose checkpoint is gone (bound to the checkpoint lifecycle).
|
|
147
|
-
void checkpointStore
|
|
275
|
+
void checkpointStore
|
|
276
|
+
?.reapCtx(Date.now() - config.runStaleSec * 1000)
|
|
277
|
+
.then(() => checkpointReapCtxGuard.onSuccess(), checkpointReapCtxGuard.onError);
|
|
148
278
|
rateLimiter?.sweep();
|
|
149
279
|
if (costQuota instanceof CostQuota)
|
|
150
280
|
costQuota.reap(); // TiDB variant self-reaps in its flush loop
|
|
151
|
-
void toolResultStore
|
|
281
|
+
void toolResultStore
|
|
282
|
+
?.reapOlderThan?.(Date.now() - config.toolResultTtlSec * 1000)
|
|
283
|
+
?.then(() => toolResultReapOlderThanGuard.onSuccess(), toolResultReapOlderThanGuard.onError); // optional extra: SQL twins only (the local FileToolResultStore persists like transcripts)
|
|
152
284
|
// 2c session-sync: GLOBALLY GC grace-window orphan blobs (standalone /sync/blobs PUTs that were never
|
|
153
285
|
// imported are otherwise collected only on reap()/deleteBySession(), which never fire for a never-imported scope).
|
|
154
286
|
// Bounds the standalone-PUT orphan-blob exhaustion (review finding) to the grace window. Durable twins only (local
|
|
155
287
|
// omits the seam → optional-chained no-op).
|
|
156
|
-
void fileSnapshotStore
|
|
288
|
+
void fileSnapshotStore
|
|
289
|
+
?.sweepOrphanBlobs?.()
|
|
290
|
+
.then(() => fileSnapshotSweepOrphanBlobsGuard.onSuccess(), fileSnapshotSweepOrphanBlobsGuard.onError);
|
|
157
291
|
// D-1 附件 TTL:上传后从未被任何 task 引用(session_id NULL)且超过 attachmentUnboundTtlMs 的行收割
|
|
158
292
|
// (绑定行不在此收——随会话 E21 级联删)。best-effort,与其余 reaper 腿同姿。
|
|
159
|
-
void taskAttachmentStore
|
|
293
|
+
void taskAttachmentStore
|
|
294
|
+
?.reapUnbound(Date.now() - config.attachmentUnboundTtlMs)
|
|
295
|
+
.then(reapCount("attachments_reaped_total", {}))
|
|
296
|
+
.then(() => attachmentReapUnboundGuard.onSuccess(), attachmentReapUnboundGuard.onError);
|
|
160
297
|
// D-1 孤儿**对象**彻底 GC(clay 拍 2026-07-28):对象存储 × meta 行对账,grace 默认 1h(上传先行窗
|
|
161
298
|
// 保护;`ATTACHMENT_ORPHAN_GRACE_MS=0` 关掉本腿——同 tick 邻居都有旋钮,复审 F10)。
|
|
162
299
|
// 列举失败=本轮 warn 跳过(「列不出来」绝不当「没有孤儿」),下轮再试。
|
|
@@ -171,18 +308,35 @@ export function startReapers(ctx) {
|
|
|
171
308
|
metrics.inc("attachment_orphan_objects_swept_total", {}, n);
|
|
172
309
|
logger.info("attachment_orphan_objects_swept", { removed: n });
|
|
173
310
|
} })
|
|
174
|
-
.catch((err) =>
|
|
311
|
+
.catch((err) => {
|
|
312
|
+
// codex 交叉复审第二轮真发现(2026-08-05):`String(err)` on a pathological rejection reason or a
|
|
313
|
+
// throwing `logger.warn` transport must not escape THIS handler — this `.catch` sits ahead of a
|
|
314
|
+
// `.finally()` with no further `.catch` downstream, so an uncaught throw here becomes a floating
|
|
315
|
+
// rejected promise (unhandled rejection under Node's default handling). Same invariant as
|
|
316
|
+
// createThrottledReaperCatch.onError above: a reaper leg's failure REPORTING must never itself
|
|
317
|
+
// crash the tick loop.
|
|
318
|
+
try {
|
|
319
|
+
logger.warn("attachment_orphan_sweep_failed", { err: err instanceof Error ? err.message : String(err) });
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// observability must never crash the tick — see comment above.
|
|
323
|
+
}
|
|
324
|
+
})
|
|
175
325
|
.finally(() => { attachmentSweepInFlight = false; });
|
|
176
326
|
}
|
|
177
327
|
// P1d-β 2c session-sync: GC ABANDONED staged imports — staging-id session_event rows (`%#stg-%`) with NO
|
|
178
328
|
// session_meta whose oldest row is older than the grace window (an in-flight stream stays fresh → never reaped).
|
|
179
329
|
// Bounds the orphan-staging-row growth (a Phase B that opened a staging then never committed). Durable session
|
|
180
330
|
// stores only (the local backend stages in memory → no durable rows; the seam is absent → optional-chained no-op).
|
|
181
|
-
void backend?.session()?.sweepStagingSessions?.()
|
|
331
|
+
void backend?.session()?.sweepStagingSessions?.()
|
|
332
|
+
.then(() => sessionSweepStagingGuard.onSuccess(), sessionSweepStagingGuard.onError);
|
|
182
333
|
// Bake-runner backstop (§P2.7): fail any `running` bake whose lease went stale (the runner crashed mid-build),
|
|
183
334
|
// append a synthetic terminal `done{failed}` so SSE readers settle, and force-release the single-flight lease.
|
|
184
335
|
// staleMs (≈3× the 30s heartbeat) bounds a healthy slow build so it is never wrongly reaped.
|
|
185
|
-
void imageBakes
|
|
336
|
+
void imageBakes
|
|
337
|
+
?.reapStaleBakes(config.imageBakes.staleMs)
|
|
338
|
+
.then(reapCount("bakes_reaped_total", {}))
|
|
339
|
+
.then(() => imageBakesReapStaleGuard.onSuccess(), imageBakesReapStaleGuard.onError);
|
|
186
340
|
// SVC-3 worktree isolation: deregister worktrees orphaned by a process crash (the Runner never reached
|
|
187
341
|
// destroy → `git worktree remove` never ran). `git worktree prune` cleans registrations whose dirs are
|
|
188
342
|
// already gone. Best-effort, userland (core ships no post-kill Runner hook); never throws. Unset = no-op.
|
|
@@ -192,16 +346,22 @@ export function startReapers(ctx) {
|
|
|
192
346
|
// 照抄 bgAgentReap 的 in-flight + finally 释放形。
|
|
193
347
|
if (worktreeReap && !worktreeReapInFlight) {
|
|
194
348
|
worktreeReapInFlight = true;
|
|
195
|
-
void worktreeReap()
|
|
349
|
+
void worktreeReap()
|
|
350
|
+
.then(() => worktreeReapGuard.onSuccess(), worktreeReapGuard.onError)
|
|
351
|
+
.finally(() => (worktreeReapInFlight = false));
|
|
196
352
|
}
|
|
197
353
|
// SVC-1 (adversarial-review HIGH): PERIODIC notify-recovery sweep (not just at boot) — re-delivers a terminal
|
|
198
354
|
// run whose in-process notify was lost, AND finalizes-as-abandoned a `running` run orphaned past the grace
|
|
199
355
|
// window (core never resumes/reaps a prior `running` row, so nothing else would). Idempotent; best-effort.
|
|
200
|
-
void workflowNotifyGate
|
|
356
|
+
void workflowNotifyGate
|
|
357
|
+
?.recover({ orphanGraceMs: config.workflowOrphanGraceMs })
|
|
358
|
+
.then(() => workflowNotifyRecoverGuard.onSuccess(), workflowNotifyRecoverGuard.onError);
|
|
201
359
|
// SVC-2 (adversarial-review HIGH): time-based GC of the workflow_journal table (the heaviest, TaskResult-bearing
|
|
202
360
|
// one) — the per-run deleteByRun has no run-store reap hook, so this bounded sweep is what stops unbounded
|
|
203
361
|
// growth. A resume of a journal older than the retention window re-runs live (resume is an optimization).
|
|
204
|
-
void workflowJournalStore
|
|
362
|
+
void workflowJournalStore
|
|
363
|
+
?.reapExpired?.(Date.now(), config.workflowJournalRetentionMs)
|
|
364
|
+
.then(() => workflowJournalReapExpiredGuard.onSuccess(), workflowJournalReapExpiredGuard.onError);
|
|
205
365
|
// Retention (WorkflowRunStore never auto-purges, reap is explicit):
|
|
206
366
|
// age out TERMINAL workflow_run rows across ALL scopes. SQL twins only (reapAllScopes is their DISTINCT-scope
|
|
207
367
|
// extension; the contract itself has no cross-scope enumeration, and the File store keeps the transcripts-like
|
|
@@ -212,37 +372,52 @@ export function startReapers(ctx) {
|
|
|
212
372
|
const sweep = sqlWorkflowRunStore?.reapAllScopes?.(Date.now(), { maxAgeMs: config.workflowRunRetentionMs });
|
|
213
373
|
if (sweep) {
|
|
214
374
|
wfRunReapInFlight = true;
|
|
215
|
-
void sweep
|
|
375
|
+
void sweep
|
|
376
|
+
.then(() => workflowRunReapAllScopesGuard.onSuccess(), workflowRunReapAllScopesGuard.onError)
|
|
377
|
+
.finally(() => (wfRunReapInFlight = false));
|
|
216
378
|
}
|
|
217
379
|
// 1.108: SQL notify-journal retention rides the same knob — ACKED rows are pure history (pending rows are
|
|
218
380
|
// the recovery backlog and are NEVER reaped; the orphan-grace sweep retires a stuck pending run).
|
|
219
381
|
void workflowNotifyJournal
|
|
220
382
|
?.reapAcked?.(Date.now() - config.workflowRunRetentionMs)
|
|
221
|
-
.
|
|
383
|
+
.then(() => workflowNotifyJournalReapAckedGuard.onSuccess(), workflowNotifyJournalReapAckedGuard.onError);
|
|
222
384
|
}
|
|
223
385
|
// core 1.364 durable bg-agent joint reap(定义在 interval 上方,契约注释在彼)。in-flight 守卫同
|
|
224
386
|
// wfRun sweep(per-scope 串行循环,短 tick + 多 scope 下不叠罗汉)。
|
|
225
387
|
if (reapBgAgents && !bgAgentReapInFlight) {
|
|
226
388
|
bgAgentReapInFlight = true;
|
|
227
|
-
void reapBgAgents()
|
|
389
|
+
void reapBgAgents()
|
|
390
|
+
.then(() => bgAgentsReapGuard.onSuccess(), bgAgentsReapGuard.onError)
|
|
391
|
+
.finally(() => (bgAgentReapInFlight = false));
|
|
228
392
|
}
|
|
229
393
|
// [1522] MED2:agent_roster TTL 清理(core RB-23②③ 派给部署的半场)——SQL twins 扩展面
|
|
230
394
|
// (reapOlderThan,duck probe;File/Memory 店 core 自带 maxAgeMs,无此面=no-op)。
|
|
231
395
|
void rosterStore
|
|
232
396
|
?.reapOlderThan?.(Date.now() - config.rosterRetentionMs)
|
|
233
397
|
.then(reapCount("roster_rows_reaped_total", {}))
|
|
234
|
-
.
|
|
398
|
+
.then(() => rosterReapOlderThanGuard.onSuccess(), rosterReapOlderThanGuard.onError);
|
|
235
399
|
// 修8(三路复审 absorb-2,接线 (b)):periodic scratchpad sweep — 无 E21 purge 兜到的孤儿目录(session 从未
|
|
236
400
|
// DELETE、purge 当次失败、local 后端无 purge coordinator)按 mtime 过期回收。SCRATCHPAD_SWEEP_TTL_MS
|
|
237
401
|
// (default 7d,0=禁用)。activeSessionIds 不传:活跃判据的诚实边界在 env-facts.ts 的 sweep 文档——TTL 7d
|
|
238
402
|
// 远大于任何在飞任务;一个 7 天零写入的 scratchpad 被回收是可接受的(它本就是临时区,fact 文案即如此宣示)。
|
|
403
|
+
// #151 车5:流内审批的对账收敛器 + 崩溃恢复扫描(两段都在 `runOnce` 内,见 approval-reconciler.ts
|
|
404
|
+
// 判据表 v2)。**不新起独立定时器**——复用本 tick(收敛延迟上界 = 一个 reaper tick;壳侧的体感由
|
|
405
|
+
// 车4 的 425 `Retry-After` 覆盖)。in-flight 守卫同邻居四腿:本腿时长随 PARKING 行数与两个读口的
|
|
406
|
+
// 库延迟增长,短 REAP_INTERVAL_SEC 下无守卫会逐 tick 叠罗汉压同一批行。
|
|
407
|
+
if (approvalReconciler && !approvalReconcileInFlight) {
|
|
408
|
+
approvalReconcileInFlight = true;
|
|
409
|
+
void approvalReconciler
|
|
410
|
+
.runOnce(Date.now())
|
|
411
|
+
.then(() => approvalReconcileGuard.onSuccess(), approvalReconcileGuard.onError)
|
|
412
|
+
.finally(() => (approvalReconcileInFlight = false));
|
|
413
|
+
}
|
|
239
414
|
if (config.scratchpadSweepTtlMs > 0) {
|
|
240
415
|
void sweepStaleScratchpads(config.localDataRoot ?? localRoot, { olderThanMs: config.scratchpadSweepTtlMs })
|
|
241
416
|
.then((removed) => {
|
|
242
417
|
if (removed > 0)
|
|
243
418
|
logger.info("scratchpads_swept", { removed });
|
|
244
419
|
})
|
|
245
|
-
.
|
|
420
|
+
.then(() => scratchpadSweepStaleGuard.onSuccess(), scratchpadSweepStaleGuard.onError);
|
|
246
421
|
}
|
|
247
422
|
}, config.reapIntervalSec * 1000);
|
|
248
423
|
reaper.unref?.();
|
|
@@ -42,12 +42,13 @@ import { applyRuntimeGovernance, stripDelegationTools } from "../runtime-governa
|
|
|
42
42
|
import { HttpError, encodeCheckpointScope } from "../security.js";
|
|
43
43
|
import { memorySpecForRequest } from "../memory-scope.js";
|
|
44
44
|
import { mcpForScenario } from "../config-center/facade.js";
|
|
45
|
-
import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, normalizeSuggestNextPrompts, promptProfileFromBody, resolveTaskLimits, retainBackgroundProcessesFromBody, taskAgentsSpecFragment, toolNameListFromBody } from "../spec-fields.js";
|
|
45
|
+
import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, normalizeSuggestNextPrompts, promptProfileFromBody, resolveTaskLimits, retainBackgroundProcessesFromBody, taskAgentsSpecFragment, toolMaterializeStrategyFromBody, toolNameListFromBody } from "../spec-fields.js";
|
|
46
46
|
import { cwdHonored, effectiveHostWorkspace, inProcessSingleUserLane, isValidCwd, parseAdditionalDirectories, satisfiedByProcessCwd, shellEnvMismatchCount } from "../task-cwd.js";
|
|
47
47
|
import { resolveRequestMcp } from "../task-mcp.js";
|
|
48
48
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, coercePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, providerDropsAppend, withPermissionMode } from "../task-settings.js";
|
|
49
49
|
import { enableForkFromBody, normalizeRetainSubagentSessions, selfOrchestrationFromBody } from "../task-workflow.js";
|
|
50
50
|
import { redactSecrets } from "../trace/redact.js";
|
|
51
|
+
import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
|
|
51
52
|
/** #152([2703] 案二):durable 部署上的 AskUserQuestion 门。活体面(QuestionCoordinator)缺席 ⇒ 原形
|
|
52
53
|
* `createDurableQuestionPolicy()`(恒 ask ⇒ 恒 durable park)。在场 ⇒ **判决时**按活流上下文分腿:
|
|
53
54
|
* 活流腿(bg/SSE,coordinator.runWithContext 包裹且投递面此刻可达,ALS 判)allow——工具执行落到
|
|
@@ -108,7 +109,11 @@ export function createResolveSpec(ctx) {
|
|
|
108
109
|
// allowedLanes ⇒ typed 403 `execution_lane_not_allowed`. Same caps car as the scenario ruling (zero extra
|
|
109
110
|
// RTT); fail-open + audit on blip. "in-process" = fleet's REMOTE_EXEC-unset core stub lane
|
|
110
111
|
// (named distinctly from the explicit `host` lane — same machine, different posture).
|
|
111
|
-
|
|
112
|
+
// #157 补痕交叉复审真发现(codex,2026-08-05):第三参不传就落 runtime-caps-resolver.ts 自己的私有
|
|
113
|
+
// `defaultLogger`(createLogger() 默认 minLevel="info")——与本部署真正按 config.logLevel 建的 `logger`
|
|
114
|
+
// 是两个独立实例,即便运维把 LOG_LEVEL 调到 debug 也打不到这条新增的一次性默认落痕行(补痕功能形同虚设)。
|
|
115
|
+
// 显式传本装配的真 logger,让该痕迹真的受部署日志级别管辖。
|
|
116
|
+
gateExecutionLane(await principalCaps?.executionRuling(auth?.principal), config.remoteExec?.provider ?? "in-process", logger);
|
|
112
117
|
// Resolve an unknown name to "default" so the WHOLE bundle stays coherent — tools/prompt AND
|
|
113
118
|
// skills/mcp fall back together. (selectScenario already falls back for tools/prompt; without
|
|
114
119
|
// resolving here, skills/mcp would still be filtered by the raw unknown name → any skill/MCP tagged
|
|
@@ -559,6 +564,10 @@ export function createResolveSpec(ctx) {
|
|
|
559
564
|
// [1144]/[1146] (core 1.328 R2): promptProfile 双形轴直通——纯呈现轴无租户门(deferTools 姿势);
|
|
560
565
|
// 缺省不挂键(引擎缺省 simple);非法值由 HTTP 门 400,resume 重放 defensive DROP。
|
|
561
566
|
...(() => { const v = promptProfileFromBody(body.promptProfile); return v ? { promptProfile: v } : {}; })(),
|
|
567
|
+
// [2856]②(core 5.14.0):deferred 工具激活后的 schema 供给策略,per-task 位。core 侧读法是
|
|
568
|
+
// `spec.toolMaterializeStrategy ?? env ?? "static"` ⇒ **缺省不挂键**才能让进程级 env 继续说话;
|
|
569
|
+
// 挂一个「缺省值」上去会把 env 车道悄悄夺权。非法值由 HTTP 门 400,resume 重放 defensive DROP。
|
|
570
|
+
...(() => { const v = toolMaterializeStrategyFromBody(body.toolMaterializeStrategy); return v ? { toolMaterializeStrategy: v } : {}; })(),
|
|
562
571
|
// [922]①/[915]② (core 1.296): interactiveTools 三态旋钮直通——boolean 上 spec(壳 -p 恒 stamp false=
|
|
563
572
|
// A3 类「headless 提问 park」根治腿;plan 显式 true 恒赢),undefined=core 自动判据(送达面探测)。
|
|
564
573
|
// per-run 工具面选择无跨租户面 ⇒ 无租户门;非 boolean 由 HTTP 门 400,resume 重放 typeof 检查天然 DROP。
|
|
@@ -857,7 +866,8 @@ export function createResolveSpec(ctx) {
|
|
|
857
866
|
// [820]③ scratchpadDir, hoisted from the envFacts block below so the SAME per-session dir feeds BOTH the
|
|
858
867
|
// `# Environment` fact and the fs-write gate's exemptDirs (single-source path via scratchpadDirFor inside
|
|
859
868
|
// ensureScratchpadDir). HOST-semantics lanes only + session required — rationale at the envFacts consumer.
|
|
860
|
-
|
|
869
|
+
// 判别式单一真源:补集(沙箱裁决 lane)是写门代理与工厂装饰器的挂载条件,两处各写一份正是漂移的成因。
|
|
870
|
+
const hostSemanticsLane = !isSandboxPathAdjudicationLane(config.remoteExec?.provider);
|
|
861
871
|
// G3([816]③/[820]③,[1840]§四):壳供 body.scratchpadDir 优先——壳(CC 形)有自己的 per-session
|
|
862
872
|
// scratchpad 约定路径,接受后事实/豁免/根围栏三处同源指向壳的目录(壳侧提示词与 server 写门不再
|
|
863
873
|
// 两个 scratchpad)。验收门 fail-closed(多租户/远程 lane/浅路径全拒收,warn 后回落自算)——
|
|
@@ -870,12 +880,20 @@ export function createResolveSpec(ctx) {
|
|
|
870
880
|
const scratchpadDir = shellScratchpad ??
|
|
871
881
|
(hostSemanticsLane && auth?.sessionId ? await ensureScratchpadDir(config.localDataRoot ?? localRoot, auth.sessionId) : undefined);
|
|
872
882
|
if (effectiveSettings) {
|
|
873
|
-
// [816]/[820]① fs-write gate wiring
|
|
874
|
-
//
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
//
|
|
883
|
+
// [816]/[820]① fs-write gate wiring —— 两形,按 lane 分:
|
|
884
|
+
//
|
|
885
|
+
// ① HOST-semantics lane(REMOTE_EXEC 未设或 "host"):core 的 createFsWriteGatePolicy 把每一个
|
|
886
|
+
// target/dir 交给给定 env 的**真实 fs**(exists/canonicalPath/readLink,dist 亲读),所以 env 必须
|
|
887
|
+
// 就是 hand 工具真正写的那块盘 —— 这里给 worker 本机的 NodeExecutionEnv(构造是纯字段赋值,不 spawn)。
|
|
888
|
+
//
|
|
889
|
+
// ② 沙箱 lane(e2b/k8s/ssh/adb/local-docker,#165):沙箱 env 由 core 在 spec **之后**才铸
|
|
890
|
+
// (executionEnvFactory),spec 期把 worker 本机 env 递进去会拿**别人机器上的 fs 事实**裁决
|
|
891
|
+
// (symlink/exists 全答错),而错误的 allow 比没有门更糟。解法是把「取 env」推迟到裁决时刻:
|
|
892
|
+
// `DeferredSandboxPathEnv` 是个代理,per-session slot 由工厂装饰器在铸 env 时填(boot/execution-env.ts
|
|
893
|
+
// 链尾),写门每次 canonicalize 都转发那台**真沙箱**的 exists/canonicalPath/fileInfo/readLink。
|
|
894
|
+
// 唯一不转发的是 `absolutePath`(相对路径基准之争,理由在下面那条臂与代理模块的文件头)。
|
|
895
|
+
// cwd 仍不铸 ⇒ 无 acceptDirs、相对路径恒 ask;scratchpad 豁免按 host 同形补回(真 canonicalize
|
|
896
|
+
// 在场,软链逃逸形会落回真身裁决)。sensitivePatterns 的 deny 腿与 isExempt 会话豁免探针照常施加。
|
|
879
897
|
//
|
|
880
898
|
// 修5(三路复审 W4,cwd 基准漂移):gate 的 cwd 基准=与 hands 真跑处同源,分三形:
|
|
881
899
|
// · session 注册过 cwd(cwdHonored 已在注册处把关)⇒ 用它——与 host factory 的 effectiveHostWorkspace
|
|
@@ -887,6 +905,32 @@ export function createResolveSpec(ctx) {
|
|
|
887
905
|
// (hand 工具的书面契约形)与 scratchpad exempt 均不受影响。
|
|
888
906
|
// · provider 未设(in-process host / run-local)⇒ hands 就在本进程 cwd 跑(无 factory),process.cwd()
|
|
889
907
|
// 正是真工作目录,保留。
|
|
908
|
+
// 两形**共有**的两件(都与 env/cwd 无关,故不随 lane 分叉——单一属主,两臂各抄一份正是漂移的成因):
|
|
909
|
+
const gateCommon = {
|
|
910
|
+
// ③ (core 1.295) sensitive-path write deny set:组合进 gate 腿的同一 combinePolicies 折叠
|
|
911
|
+
// (deny 恒赢 —— 豁免/acceptDirs 越不过;集合取舍 core 成文,server 只透传 config 旋钮:
|
|
912
|
+
// 缺省 = core RECOMMENDED_SENSITIVE_PATTERNS,SENSITIVE_WRITE_PATTERNS 显式替换/off 关闭)。
|
|
913
|
+
// 沙箱 lane 上它按**词法** key 的路径段匹配(#156)——glob 本就只看段名,不需要真 fs。
|
|
914
|
+
...(config.sensitiveWritePatterns.length > 0 ? { sensitivePatterns: config.sensitiveWritePatterns } : {}),
|
|
915
|
+
// [1557]§四 的 shellGate 旧家已随 #153 搬到 applyRuntimeGovernance(governance 层无条件施加,
|
|
916
|
+
// 见上方 governedBase)——wiring 不再承运该旋钮,FsWriteGateWiring.shellGate 字段一并拆除。
|
|
917
|
+
// [841]① / core 1.294 exemption seam:gate 产 ask 前查同一只 approval_exemption 店(与 decide
|
|
918
|
+
// remember="session" 的授予、ask 政策层探针同店同 canonical toolName 键空间)——「本会话不再询问」
|
|
919
|
+
// 对 fs-write 门同 turn 即时生效(父+继承子任务,ask 路径不再重入)。has() reject ⇒ core 按
|
|
920
|
+
// 未豁免处理(fail-closed 照 ask),与店契约的调用方纪律一致;命中打 info 审计(对齐
|
|
921
|
+
// approval_exempted 的短路留痕,不记路径,路径留在 core 的 allow message 里)。探针是 name-keyed
|
|
922
|
+
// 的(不做任何 fs 裁决),所以沙箱 lane 照接不误。
|
|
923
|
+
...(approvalExemptionStore && auth?.sessionId
|
|
924
|
+
? {
|
|
925
|
+
isExempt: async (toolName) => {
|
|
926
|
+
const hit = await approvalExemptionStore.has(auth.sessionId, toolName);
|
|
927
|
+
if (hit)
|
|
928
|
+
logger.info("fs_write_gate_exempted", { toolName, sessionId: auth.sessionId });
|
|
929
|
+
return hit;
|
|
930
|
+
},
|
|
931
|
+
}
|
|
932
|
+
: {}),
|
|
933
|
+
};
|
|
890
934
|
const fsWriteGate = hostSemanticsLane
|
|
891
935
|
? (() => {
|
|
892
936
|
const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
|
|
@@ -898,32 +942,52 @@ export function createResolveSpec(ctx) {
|
|
|
898
942
|
env: new NodeExecutionEnv({ cwd: gateCwd }),
|
|
899
943
|
cwd: gateCwd,
|
|
900
944
|
...(scratchpadDir ? { scratchpadDir } : {}),
|
|
901
|
-
|
|
902
|
-
// (deny 恒赢 —— 豁免/acceptDirs 越不过;集合取舍 core 成文,server 只透传 config 旋钮:
|
|
903
|
-
// 缺省 = core RECOMMENDED_SENSITIVE_PATTERNS,SENSITIVE_WRITE_PATTERNS 显式替换/off 关闭)。
|
|
904
|
-
...(config.sensitiveWritePatterns.length > 0 ? { sensitivePatterns: config.sensitiveWritePatterns } : {}),
|
|
905
|
-
// [1557]§四 的 shellGate 旧家已随 #153 搬到 applyRuntimeGovernance(governance 层无条件施加,
|
|
906
|
-
// 见上方 governedBase)——wiring 不再承运该旋钮,FsWriteGateWiring.shellGate 字段一并拆除。
|
|
907
|
-
// [841]① / core 1.294 exemption seam:gate 产 ask 前查同一只 approval_exemption 店(与 decide
|
|
908
|
-
// remember="session" 的授予、ask 政策层探针同店同 canonical toolName 键空间)——「本会话不再询问」
|
|
909
|
-
// 对 fs-write 门同 turn 即时生效(父+继承子任务,ask 路径不再重入)。has() reject ⇒ core 按
|
|
910
|
-
// 未豁免处理(fail-closed 照 ask),与店契约的调用方纪律一致;命中打 info 审计(对齐
|
|
911
|
-
// approval_exempted 的短路留痕,不记路径,路径留在 core 的 allow message 里)。
|
|
912
|
-
...(approvalExemptionStore && auth?.sessionId
|
|
913
|
-
? {
|
|
914
|
-
isExempt: async (toolName) => {
|
|
915
|
-
const hit = await approvalExemptionStore.has(auth.sessionId, toolName);
|
|
916
|
-
if (hit)
|
|
917
|
-
logger.info("fs_write_gate_exempted", { toolName, sessionId: auth.sessionId });
|
|
918
|
-
return hit;
|
|
919
|
-
},
|
|
920
|
-
}
|
|
921
|
-
: {}),
|
|
945
|
+
...gateCommon,
|
|
922
946
|
};
|
|
923
947
|
})()
|
|
924
|
-
:
|
|
925
|
-
|
|
926
|
-
|
|
948
|
+
: // 沙箱 lane(#165):裁决 env = `DeferredSandboxPathEnv` 代理 —— 裁决时刻(必在 prepare 之后)
|
|
949
|
+
// 去 per-session slot 里取本 task 的**真沙箱 env**,转发 exists/canonicalPath/fileInfo/readLink。
|
|
950
|
+
// 于是绝对路径的 symlink 真身、存在性都是真沙箱 fs 的事实,域内软链指向守卫段照样 deny;
|
|
951
|
+
// slot 空/歧义 ⇒ 转发面报错 ⇒ core 判 `unresolvedSymlink` ⇒ 写门 ask + sensitive deny(fail-closed)。
|
|
952
|
+
//
|
|
953
|
+
// **`cwd` 仍不给**(显式裁定,非过渡欠款):`cwd` 同时是 core 的 `rootPath`(相对路径基准)与
|
|
954
|
+
// acceptEdits 的 accept 域。沙箱里那个基准是 engine 跟踪的 cwd,不是 env 自己的 cwd,server 侧要
|
|
955
|
+
// 铸它得先建一个 per-task 跟踪面——那是独立一件(与 host 臂哨兵同族的漂移风险)。后果:相对路径
|
|
956
|
+
// 恒 ask,acceptEdits 在沙箱 lane 与 default 同形(全 ask),fail-safe 方向。
|
|
957
|
+
//
|
|
958
|
+
// **scratchpad 豁免按 host 同形补回**:`/tmp/scratchpad/<sessionId>`(remote-scratchpad.ts 单一
|
|
959
|
+
// 真源,e2b/k8s/local-docker/ssh 四 lane;adb 无此约定故无豁免)。#156 当年不豁免是因为词法版
|
|
960
|
+
// 看不见软链——域内一个指向域外的软链会被判成域内;现在 exemptDirs 与写目标两侧都过真
|
|
961
|
+
// canonicalize,逃逸形会解出域外真身而落回真身裁决,host lane 的同一保证在这里成立了。
|
|
962
|
+
// 注:远程 lane 的**根围栏**admit 走的是 core 对 `spec.envFacts.scratchpadDir` 的自建根(prepare 期
|
|
963
|
+
// canonicalize/实体化),而那条事实在 `SANDBOX_ENV_FACTS=false` 下不发——那种配置里根围栏先拒,
|
|
964
|
+
// 本豁免够不着(方向更严,不是放行面);host 臂的 additionalDirectories 折叠不受影响。
|
|
965
|
+
(() => {
|
|
966
|
+
const sandboxScratchpadDir = isRemoteScratchpadLane(config.remoteExec?.provider) ? remoteScratchpadDirFor(auth?.sessionId) : undefined;
|
|
967
|
+
return {
|
|
968
|
+
env: new DeferredSandboxPathEnv({
|
|
969
|
+
sessionId: auth?.sessionId,
|
|
970
|
+
slots: sandboxPathEnvSlots,
|
|
971
|
+
logger: { warn: (event, fields) => logger.warn(event, { ...(fields ?? {}), lane: config.remoteExec?.provider }) },
|
|
972
|
+
}),
|
|
973
|
+
...gateCommon,
|
|
974
|
+
...(sandboxScratchpadDir ? { scratchpadDir: sandboxScratchpadDir } : {}),
|
|
975
|
+
// codex 复审 B(2026-08-05,dist 亲读+实测):core 的 canonicalizeTarget 把反斜杠 UNC 形在**入口**
|
|
976
|
+
// 短路成 ok:true/key=原样——不经本 env,canon 便「成功」,gate 随后就咨询豁免探针;name-keyed
|
|
977
|
+
// 命中即 allow,「非 POSIX 绝对形一律 ask」就被豁免会话越过(相对形/盘符形没这个洞:它们的
|
|
978
|
+
// canon 失败先于豁免咨询)。沙箱臂因此给探针加 canonical-key 守门:只有 `/` 开头(=真经过本
|
|
979
|
+
// 代理的路径面)的 key 才享豁免,其余落回 defaultWrite 的 ask。host 臂不加
|
|
980
|
+
// (真 fs canonicalize 的 key 可信,[841]① 原契约不动)。
|
|
981
|
+
...(gateCommon.isExempt
|
|
982
|
+
? {
|
|
983
|
+
isExempt: (toolName, canonicalPath) => canonicalPath.startsWith("/") ? gateCommon.isExempt(toolName) : false,
|
|
984
|
+
}
|
|
985
|
+
: {}),
|
|
986
|
+
};
|
|
987
|
+
})();
|
|
988
|
+
// [1248]②/codex F2 — the workflow ask leg's session-exemption probe. Name-keyed and never fs-adjudicated,
|
|
989
|
+
// so it was on EVERY lane from day one (the fs gate has been too since #156, by a different route: a
|
|
990
|
+
// lane-specific adjudication env rather than no gate). Same store + canonical toolName key space as the
|
|
927
991
|
// fs-gate probe — one remember="session" grant serves both; without it the durable lane re-parks every
|
|
928
992
|
// later run_workflow call after an operator already granted "don't ask again this session".
|
|
929
993
|
const workflowGate = approvalExemptionStore && auth?.sessionId
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { verifyPromptArtifact } from "@sema-agent/core";
|
|
23
23
|
import { CORE_ENGINE_VERSION } from "../prompts-domain-validate.js";
|
|
24
24
|
import { SHA256_HEX_RE } from "../digest-form.js";
|
|
25
|
+
import { recordFailOpen } from "../observability/fail-open.js";
|
|
25
26
|
/** 判形半场(validateCenterPrompts/validatePromptsDomain/CORE_ENGINE_VERSION 及两 interface)已下沉到
|
|
26
27
|
* 中立叶子 `src/prompts-domain-validate.ts`(design/158 S6);消费者直引真源([2354] 兼容面全清,
|
|
27
28
|
* 此处兼容 re-export 已删)。 */
|
|
@@ -56,7 +57,9 @@ export function withPromptArtifactBackfill(inner, fetchRaw, log) {
|
|
|
56
57
|
log.warn("center_prompts_backfill_rejected", { digest: canonical, ...(v.ok ? { got: v.artifact.artifactDigest, code: "digest_mismatch" } : { code: v.code, reason: v.reason }) });
|
|
57
58
|
return undefined;
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
+
// cache best-effort —— #157:F 类兜底(工件已验签且已返回;失败的代价只是下次再取一次网络),
|
|
61
|
+
// 留痕是为了让"缓存其实一直没写成"不必靠回填流量异常来发现。
|
|
62
|
+
await inner.put(raw).catch(() => recordFailOpen("server.prompts.artifact-cache-write-failed", `digest=${canonical}`));
|
|
60
63
|
log.info("center_prompts_backfilled", { digest: canonical });
|
|
61
64
|
return raw;
|
|
62
65
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ToolSpec } from "@sema-agent/core";
|
|
2
|
+
/**
|
|
3
|
+
* OA scenario — tools are thin HTTP clients to the OA backend's existing APIs (read = /api/v1/*,
|
|
4
|
+
* write = /api/assets/*). The shared service token + the request's principal are forwarded so OA
|
|
5
|
+
* enforces THAT end-user's RBAC + audit (the service never holds per-user creds). Decoupled: the
|
|
6
|
+
* service knows no OA schema/DB — OA stays the source of truth (its /api/v1/meta is self-describing).
|
|
7
|
+
*/
|
|
8
|
+
export interface OaCtx {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
token: string;
|
|
11
|
+
principal?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function oaTools(ctx: OaCtx): ToolSpec[];
|
|
14
|
+
export declare const oaPrompt: import("@sema-agent/core").PromptProvider;
|
|
15
|
+
//# sourceMappingURL=oa-tools.d.ts.map
|