@sema-agent/server 7.100.0 → 7.101.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +22 -1
- package/README.md +12 -1
- package/README.zh-CN.md +9 -1
- package/USAGE.md +80 -9
- package/dist/approval-card.d.ts +24 -12
- package/dist/approval-card.js +4 -3
- package/dist/approval-policy-settlement.d.ts +31 -0
- package/dist/approval-policy-settlement.js +16 -0
- package/dist/approval-reconciler.d.ts +8 -1
- package/dist/approval-reconciler.js +5 -4
- package/dist/boot/config-center.js +86 -20
- package/dist/boot/execution-env.js +20 -2
- package/dist/boot/parked-revive-gate.js +1 -0
- package/dist/boot/reapers.js +1 -0
- package/dist/boot/resolve-spec.js +25 -8
- package/dist/boot/runner-deps.js +9 -3
- package/dist/boot/stage-02-registry-pull.js +9 -0
- package/dist/boot/stage-10-http-server.js +2 -0
- package/dist/boot/stores.js +4 -0
- package/dist/config-catalog.js +5 -3
- package/dist/config-center/apply-effective.d.ts +25 -0
- package/dist/config-center/apply-effective.js +144 -69
- package/dist/config-center/apply-ledger.d.ts +12 -2
- package/dist/config-center/apply-ledger.js +4 -1
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/http-client.d.ts +4 -1
- package/dist/config-center/http-client.js +15 -10
- package/dist/config-center/read-warnings.d.ts +115 -6
- package/dist/config-center/read-warnings.js +115 -5
- package/dist/config-center/types.d.ts +6 -0
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +15 -0
- package/dist/config-provider.d.ts +4 -0
- package/dist/config-provider.js +64 -9
- package/dist/config-types.d.ts +37 -8
- package/dist/config.js +87 -22
- package/dist/deployment-governance.d.ts +45 -12
- package/dist/deployment-governance.js +37 -9
- package/dist/env-name-allowlist-knobs.d.ts +37 -0
- package/dist/env-name-allowlist-knobs.js +24 -0
- package/dist/fleet/fleet-bus.d.ts +30 -4
- package/dist/fleet/fleet-bus.js +20 -4
- package/dist/fleet/fleet-reconciler.d.ts +29 -6
- package/dist/fleet/fleet-reconciler.js +109 -79
- package/dist/fleet-client.d.ts +2 -1
- package/dist/hooks/hook-llm.js +13 -4
- package/dist/host-lsp-manager.d.ts +14 -2
- package/dist/host-lsp-manager.js +7 -6
- package/dist/http/admission.js +22 -7
- package/dist/http/memory-store-refusal-reply.d.ts +24 -0
- package/dist/http/memory-store-refusal-reply.js +10 -0
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/capabilities.js +3 -3
- package/dist/http/routes/memory-bundle.js +3 -0
- package/dist/http/routes/memory-compliance.js +3 -5
- package/dist/http/routes/memory-origin.js +3 -5
- package/dist/http/routes/sessions.js +7 -6
- package/dist/http/routes/side-query.js +10 -4
- package/dist/instruction-source-notice.d.ts +97 -0
- package/dist/instruction-source-notice.js +78 -0
- package/dist/memory-layer-legacy-lock.d.ts +14 -0
- package/dist/memory-layer-legacy-lock.js +74 -0
- package/dist/memory-operator-faces.js +1 -17
- package/dist/memory-store-refusal.d.ts +37 -0
- package/dist/memory-store-refusal.js +14 -0
- package/dist/model-entry-refusal.d.ts +130 -0
- package/dist/model-entry-refusal.js +157 -0
- package/dist/model-provider.d.ts +9 -3
- package/dist/model-route-endpoint.d.ts +1 -1
- package/dist/model-select.d.ts +49 -4
- package/dist/model-select.js +48 -17
- package/dist/observability/fail-open.d.ts +13 -1
- package/dist/observability/fail-open.js +13 -1
- package/dist/observability/metrics.js +1 -0
- package/dist/observability/secret-env-scrub.d.ts +27 -4
- package/dist/observability/secret-env-scrub.js +22 -7
- package/dist/plugins/caching-session-store.d.ts +6 -0
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/local-session-store.d.ts +20 -25
- package/dist/plugins/local-session-store.js +111 -64
- package/dist/plugins/pg-session-storage.d.ts +7 -0
- package/dist/plugins/pg-session-storage.js +19 -0
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +3 -6
- package/dist/plugins/tidb-pool.js +5 -0
- package/dist/plugins/tidb-session-store.d.ts +13 -0
- package/dist/plugins/tidb-session-store.js +14 -0
- package/dist/plugins/web-search.d.ts +2 -1
- package/dist/plugins/web-search.js +43 -24
- package/dist/project-memory.d.ts +10 -0
- package/dist/project-memory.js +28 -14
- package/dist/run-local.d.ts +17 -15
- package/dist/run-local.js +115 -15
- package/dist/sealed-key.d.ts +5 -2
- package/dist/server-secret-env.d.ts +23 -1
- package/dist/server-secret-env.js +9 -0
- package/dist/tool-approval.d.ts +74 -31
- package/dist/tool-approval.js +102 -49
- package/dist/trace/core-keyset-guard.d.ts +27 -5
- package/dist/trace/engine-notice-wire.d.ts +27 -1
- package/dist/trace/engine-notice-wire.js +8 -0
- package/dist/trace/wire-projection-faces.d.ts +2 -2
- package/dist/trace/wire-projection-faces.js +2 -2
- package/package.json +2 -2
- package/dist/memory-layer-preflight.d.ts +0 -46
- package/dist/memory-layer-preflight.js +0 -121
|
@@ -11,7 +11,7 @@ export const FLEET_RECONCILE_COOLDOWN_MS = 60_000;
|
|
|
11
11
|
export const FLEET_RECONCILE_WATCH_MAX = 200;
|
|
12
12
|
export const FLEET_RECONCILE_WATCH_TTL_MS = 30 * 60_000;
|
|
13
13
|
const emptyTally = () => ({
|
|
14
|
-
scanned: 0, skippedLive: 0, skippedParked: 0, retired: 0, orphaned: 0, childrenSwept: 0, workflowsRetired: 0, reads: 0, cooling: false, failOpen: false,
|
|
14
|
+
scanned: 0, skippedLive: 0, skippedParked: 0, retired: 0, orphaned: 0, childrenSwept: 0, childrenKeptAlive: 0, workflowsRetired: 0, reads: 0, cooling: false, failOpen: false,
|
|
15
15
|
});
|
|
16
16
|
function rotate(rows, cursor) {
|
|
17
17
|
if (cursor === undefined || rows.length === 0)
|
|
@@ -22,6 +22,22 @@ function rotate(rows, cursor) {
|
|
|
22
22
|
return [...rows.slice(at), ...rows.slice(0, at)];
|
|
23
23
|
}
|
|
24
24
|
const SILENT_FOR_A_CYCLE = FLEET_RECONCILE_INTERVAL_MS;
|
|
25
|
+
function bgAgentVouches(status) {
|
|
26
|
+
switch (status) {
|
|
27
|
+
case "running":
|
|
28
|
+
case "parked":
|
|
29
|
+
return true;
|
|
30
|
+
case "completed":
|
|
31
|
+
case "failed":
|
|
32
|
+
case "killed":
|
|
33
|
+
return false;
|
|
34
|
+
default: {
|
|
35
|
+
const unhandled = status;
|
|
36
|
+
recordFailOpen("server.fleet.agent-status-unmapped", String(unhandled));
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
25
41
|
export function createFleetReconciler(deps) {
|
|
26
42
|
const now = deps.now ?? (() => Date.now());
|
|
27
43
|
const lastFrameAt = new Map();
|
|
@@ -86,11 +102,11 @@ export function createFleetReconciler(deps) {
|
|
|
86
102
|
coolingUntil = undefined;
|
|
87
103
|
const deadline = now() + FLEET_RECONCILE_READ_BUDGET_MS;
|
|
88
104
|
const budgetLeft = () => tally.reads < FLEET_RECONCILE_MAX_ROWS && now() < deadline;
|
|
89
|
-
const
|
|
105
|
+
const budgetedRead = async (read) => {
|
|
90
106
|
tally.reads++;
|
|
91
107
|
let t;
|
|
92
|
-
|
|
93
|
-
|
|
108
|
+
return await Promise.race([
|
|
109
|
+
read(),
|
|
94
110
|
new Promise((_r, reject) => {
|
|
95
111
|
t = setTimeout(() => reject(new ReadAborted(`fleet reconcile read exceeded ${FLEET_RECONCILE_READ_BUDGET_MS}ms`)), Math.max(0, deadline - now()));
|
|
96
112
|
if (typeof t.unref === "function")
|
|
@@ -100,30 +116,43 @@ export function createFleetReconciler(deps) {
|
|
|
100
116
|
if (t !== undefined)
|
|
101
117
|
clearTimeout(t);
|
|
102
118
|
});
|
|
103
|
-
return rec;
|
|
104
119
|
};
|
|
120
|
+
const readRun = (id) => budgetedRead(() => deps.runs.getRun(id));
|
|
105
121
|
const snap = deps.bus.snapshot();
|
|
106
122
|
for (const r of snap.tasks)
|
|
107
123
|
if (!lastFrameAt.has(r.id))
|
|
108
124
|
lastFrameAt.set(r.id, nowMs);
|
|
109
125
|
const idle = (id) => nowMs - (lastFrameAt.get(id) ?? nowMs) >= SILENT_FOR_A_CYCLE;
|
|
110
126
|
const present = new Set(snap.tasks.map((r) => r.id));
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
const parentsOnBus = new Set(present);
|
|
128
|
+
const stillSilent = (row) => {
|
|
129
|
+
const cur = deps.bus.taskRow(row.id);
|
|
130
|
+
return cur !== undefined && cur.cycleSeq === row.cycleSeq && idle(row.id);
|
|
131
|
+
};
|
|
132
|
+
const keptAlive = (row, why) => {
|
|
133
|
+
tally.childrenKeptAlive++;
|
|
134
|
+
deps.logger?.info("fleet_row_reconciled", { taskId: row.id, disposition: "child_kept_alive", why, instanceId: deps.instanceId ?? null });
|
|
135
|
+
};
|
|
136
|
+
const judgeSilentRow = async (row, exit) => {
|
|
137
|
+
const handle = deps.bus.childOwnerKeys(row).handle;
|
|
138
|
+
if (handle !== undefined && deps.agents !== undefined && row.scope !== undefined) {
|
|
139
|
+
if (!budgetLeft())
|
|
140
|
+
return "over_budget";
|
|
141
|
+
const scope = row.scope;
|
|
142
|
+
const rec = await budgetedRead(() => deps.agents.get(handle, scope));
|
|
143
|
+
if (rec !== null && bgAgentVouches(rec.status)) {
|
|
144
|
+
keptAlive(row, "bg_registry");
|
|
145
|
+
return "settled";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const hasPendingAsk = deps.hasPendingAsk;
|
|
149
|
+
if (hasPendingAsk !== undefined && deps.bus.childOwnerKeys(row).sessionIds.some((id) => hasPendingAsk(id))) {
|
|
150
|
+
keptAlive(row, "pending_ask");
|
|
151
|
+
return "settled";
|
|
126
152
|
}
|
|
153
|
+
if (stillSilent(row))
|
|
154
|
+
exit();
|
|
155
|
+
return "settled";
|
|
127
156
|
};
|
|
128
157
|
const retire = (row, rec) => {
|
|
129
158
|
deps.bus.publishTask({
|
|
@@ -132,14 +161,14 @@ export function createFleetReconciler(deps) {
|
|
|
132
161
|
retiredBy: "reconcile",
|
|
133
162
|
...fleetRunResiduals(rec.result),
|
|
134
163
|
});
|
|
135
|
-
|
|
164
|
+
parentsOnBus.delete(row.id);
|
|
136
165
|
deps.bus.removeTask(row.id, { ...(row.cycleSeq !== undefined ? { cycleSeq: row.cycleSeq } : {}) });
|
|
137
166
|
lastFrameAt.delete(row.id);
|
|
138
167
|
tally.retired++;
|
|
139
168
|
deps.metrics?.inc("fleet_rows_reconciled_total", { disposition: "retired" });
|
|
140
169
|
};
|
|
141
170
|
const orphanExit = (row, why) => {
|
|
142
|
-
|
|
171
|
+
parentsOnBus.delete(row.id);
|
|
143
172
|
deps.bus.removeTask(row.id, { removeReason: "orphaned", ...(row.cycleSeq !== undefined ? { cycleSeq: row.cycleSeq } : {}) });
|
|
144
173
|
lastFrameAt.delete(row.id);
|
|
145
174
|
if (deps.runs && watch.size < FLEET_RECONCILE_WATCH_MAX)
|
|
@@ -148,6 +177,61 @@ export function createFleetReconciler(deps) {
|
|
|
148
177
|
deps.metrics?.inc("fleet_rows_reconciled_total", { disposition: "orphaned" });
|
|
149
178
|
deps.logger?.info("fleet_row_reconciled", { taskId: row.id, disposition: "orphaned", why, instanceId: deps.instanceId ?? null });
|
|
150
179
|
};
|
|
180
|
+
const childExit = (row, parentId) => {
|
|
181
|
+
deps.bus.removeTask(row.id, { removeReason: "orphaned", ...(row.cycleSeq !== undefined ? { cycleSeq: row.cycleSeq } : {}) });
|
|
182
|
+
lastFrameAt.delete(row.id);
|
|
183
|
+
parentsOnBus.delete(row.id);
|
|
184
|
+
if (present.has(parentId)) {
|
|
185
|
+
tally.childrenSwept++;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
tally.orphaned++;
|
|
189
|
+
deps.metrics?.inc("fleet_rows_reconciled_total", { disposition: "orphaned_child" });
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const judgeTopLevel = async (row) => {
|
|
193
|
+
tally.scanned++;
|
|
194
|
+
if (deps.isLiveOnThisReplica(row.id)) {
|
|
195
|
+
tally.skippedLive++;
|
|
196
|
+
return "settled";
|
|
197
|
+
}
|
|
198
|
+
if (!deps.runs)
|
|
199
|
+
return "settled";
|
|
200
|
+
if (!budgetLeft())
|
|
201
|
+
return "over_budget";
|
|
202
|
+
const rec = await readRun(row.id);
|
|
203
|
+
if (deps.isLiveOnThisReplica(row.id)) {
|
|
204
|
+
tally.skippedLive++;
|
|
205
|
+
return "settled";
|
|
206
|
+
}
|
|
207
|
+
const nowRow = deps.bus.taskRow(row.id);
|
|
208
|
+
if (nowRow === undefined)
|
|
209
|
+
return "settled";
|
|
210
|
+
if (nowRow.cycleSeq !== row.cycleSeq)
|
|
211
|
+
return "settled";
|
|
212
|
+
if (rec === undefined) {
|
|
213
|
+
if (!idle(row.id))
|
|
214
|
+
return "settled";
|
|
215
|
+
return judgeSilentRow(row, () => orphanExit(row, "durable_row_absent"));
|
|
216
|
+
}
|
|
217
|
+
if (isTerminalRunStatus(rec.status)) {
|
|
218
|
+
retire(row, rec);
|
|
219
|
+
return "settled";
|
|
220
|
+
}
|
|
221
|
+
if (isParkedRunStatus(rec.status)) {
|
|
222
|
+
tally.skippedParked++;
|
|
223
|
+
return "settled";
|
|
224
|
+
}
|
|
225
|
+
orphanExit(row, runProvenElsewhere(rec, deps.instanceId) ? "durable_running_other_replica" : "durable_running_no_local_leg");
|
|
226
|
+
return "settled";
|
|
227
|
+
};
|
|
228
|
+
const judgeChild = async (row, parentId) => {
|
|
229
|
+
if (parentsOnBus.has(parentId))
|
|
230
|
+
return "settled";
|
|
231
|
+
if (!idle(row.id))
|
|
232
|
+
return "settled";
|
|
233
|
+
return judgeSilentRow(row, () => childExit(row, parentId));
|
|
234
|
+
};
|
|
151
235
|
try {
|
|
152
236
|
for (const [id, w] of [...watch]) {
|
|
153
237
|
if (nowMs - w.at > FLEET_RECONCILE_WATCH_TTL_MS) {
|
|
@@ -171,53 +255,11 @@ export function createFleetReconciler(deps) {
|
|
|
171
255
|
tally.retired++;
|
|
172
256
|
}
|
|
173
257
|
for (const row of rotate(snap.tasks, taskCursor)) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
tally.scanned++;
|
|
177
|
-
if (deps.isLiveOnThisReplica(row.id)) {
|
|
178
|
-
tally.skippedLive++;
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
if (!deps.runs)
|
|
182
|
-
continue;
|
|
183
|
-
if (!budgetLeft()) {
|
|
258
|
+
const step = row.parentId === undefined ? await judgeTopLevel(row) : await judgeChild(row, row.parentId);
|
|
259
|
+
if (step === "over_budget") {
|
|
184
260
|
taskCursor = row.id;
|
|
185
261
|
break;
|
|
186
262
|
}
|
|
187
|
-
const rec = await readRun(row.id);
|
|
188
|
-
if (deps.isLiveOnThisReplica(row.id)) {
|
|
189
|
-
tally.skippedLive++;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
const nowRow = deps.bus.taskRow(row.id);
|
|
193
|
-
if (nowRow === undefined)
|
|
194
|
-
continue;
|
|
195
|
-
if (nowRow.cycleSeq !== row.cycleSeq)
|
|
196
|
-
continue;
|
|
197
|
-
if (rec === undefined) {
|
|
198
|
-
if (idle(row.id))
|
|
199
|
-
orphanExit(row, "durable_row_absent");
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
if (isTerminalRunStatus(rec.status)) {
|
|
203
|
-
retire(row, rec);
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
if (isParkedRunStatus(rec.status)) {
|
|
207
|
-
tally.skippedParked++;
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
orphanExit(row, runProvenElsewhere(rec, deps.instanceId) ? "durable_running_other_replica" : "durable_running_no_local_leg");
|
|
211
|
-
}
|
|
212
|
-
for (const row of snap.tasks) {
|
|
213
|
-
if (row.parentId === undefined || present.has(row.parentId))
|
|
214
|
-
continue;
|
|
215
|
-
if (!idle(row.id))
|
|
216
|
-
continue;
|
|
217
|
-
deps.bus.removeTask(row.id, { removeReason: "orphaned", ...(row.cycleSeq !== undefined ? { cycleSeq: row.cycleSeq } : {}) });
|
|
218
|
-
lastFrameAt.delete(row.id);
|
|
219
|
-
tally.orphaned++;
|
|
220
|
-
deps.metrics?.inc("fleet_rows_reconciled_total", { disposition: "orphaned_child" });
|
|
221
263
|
}
|
|
222
264
|
if (wfGet) {
|
|
223
265
|
for (const wf of rotate(snap.workflows, wfCursor)) {
|
|
@@ -227,19 +269,7 @@ export function createFleetReconciler(deps) {
|
|
|
227
269
|
wfCursor = wf.id;
|
|
228
270
|
break;
|
|
229
271
|
}
|
|
230
|
-
|
|
231
|
-
let wfTimer;
|
|
232
|
-
const run = await Promise.race([
|
|
233
|
-
wfGet(wf.id),
|
|
234
|
-
new Promise((_r, reject) => {
|
|
235
|
-
wfTimer = setTimeout(() => reject(new ReadAborted("fleet reconcile workflow read over budget")), Math.max(0, deadline - now()));
|
|
236
|
-
if (typeof wfTimer.unref === "function")
|
|
237
|
-
wfTimer.unref();
|
|
238
|
-
}),
|
|
239
|
-
]).finally(() => {
|
|
240
|
-
if (wfTimer !== undefined)
|
|
241
|
-
clearTimeout(wfTimer);
|
|
242
|
-
});
|
|
272
|
+
const run = await budgetedRead(() => wfGet(wf.id));
|
|
243
273
|
if (!run || !isTerminalWorkflowStatus(run.status))
|
|
244
274
|
continue;
|
|
245
275
|
if (wf.scope !== undefined && run.scope !== wf.scope)
|
package/dist/fleet-client.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* - **可选分层(D3)**:仅当 center 已配置(sema-registry lane 的 baseUrl+token+worker)∧ `FLEET_ADVERTISE_ADDRESS`
|
|
5
5
|
* 显式声明时才启动 —— 不配 center 的部署零行为变化。address 不自猜(k8s pod IP / docker alias / 127.0.0.1:port
|
|
6
6
|
* 只有部署环境自己知道,故用显式 env)。
|
|
7
|
-
* - **announce=注册=心跳同形状幂等 upsert**:boot 后 POST 一次 + 每 heartbeatMs 续;`ready =
|
|
7
|
+
* - **announce=注册=心跳同形状幂等 upsert**:boot 后 POST 一次 + 每 heartbeatMs 续;`ready` 今 = `!draining`(实现只读
|
|
8
|
+
* `isDraining()`,announce 本身在 boot 之后才起;S-740 起与 /health 就绪同一只谓词 —— 车OC 6b 注释改真,实现归 S-740)。
|
|
8
9
|
* - **draining 翻转即刻再 announce**(第一时间自摘流,比任何观测都快);退出前 DELETE(显式注销,幂等)。
|
|
9
10
|
* - **usage 批报**:tracer 的 `brain.call` 已带 ALS principal —— FleetUsageAccumulator 按 principal×model
|
|
10
11
|
* 累计,每窗口(默认 60s,半开 [windowStartMs, windowEndMs))POST /api/fleet/usage。幂等键
|
package/dist/hooks/hook-llm.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { adjudicateModelRoute, joinRequestUrl, resolveRouteCredential, routeKeyOf, routeRefusalText } from "@sema-agent/core";
|
|
2
|
-
import { matchCatalogModel } from "../model-select.js";
|
|
2
|
+
import { matchCatalogModel, refusedCatalogEntry } from "../model-select.js";
|
|
3
|
+
import { modelUnavailableMessage } from "../model-entry-refusal.js";
|
|
3
4
|
import { resolveOpenaiRouteEndpoint } from "../model-route-endpoint.js";
|
|
4
5
|
import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
|
|
5
6
|
export function buildHookFailureText(raw) {
|
|
@@ -32,21 +33,29 @@ export function createHookLlm(deps) {
|
|
|
32
33
|
};
|
|
33
34
|
const hookModelFor = (requested) => {
|
|
34
35
|
const shape = (ref) => shapeFrom(ref, config.models[ref], config.models[ref]?.id ?? ref);
|
|
36
|
+
const unavailable = (field, ref) => {
|
|
37
|
+
if (config.models[ref] !== undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
const hit = refusedCatalogEntry(ref, config.modelRefusals);
|
|
40
|
+
return hit === undefined ? undefined : { ok: false, error: modelUnavailableMessage(field, ref, hit.name, hit.refusal) };
|
|
41
|
+
};
|
|
35
42
|
if (requested === undefined) {
|
|
36
43
|
const sum = config.roles.summarize;
|
|
37
44
|
const roleModel = typeof sum === "string" ? sum : sum && typeof sum === "object" && "model" in sum && typeof sum.model === "string" ? sum.model : undefined;
|
|
38
45
|
if (roleModel !== undefined)
|
|
39
|
-
return config.models[roleModel] ? shape(roleModel) : shapeFrom(config.model.id, config.model, roleModel);
|
|
46
|
+
return unavailable("summarize role model", roleModel) ?? (config.models[roleModel] ? shape(roleModel) : shapeFrom(config.model.id, config.model, roleModel));
|
|
40
47
|
return shapeFrom(config.model.id, config.model, config.model.id);
|
|
41
48
|
}
|
|
42
49
|
if (config.models[requested])
|
|
43
50
|
return shape(requested);
|
|
44
|
-
|
|
51
|
+
if (Object.hasOwn(config.modelRefusals ?? {}, requested))
|
|
52
|
+
return unavailable("hook model", requested);
|
|
53
|
+
const byId = matchCatalogModel(requested, config.models, config.modelRefusals);
|
|
45
54
|
if (byId !== undefined)
|
|
46
55
|
return shape(byId);
|
|
47
56
|
if (requested === config.model.id)
|
|
48
57
|
return shapeFrom(requested, config.model, requested);
|
|
49
|
-
return { ok: false, error: `hook model "${requested}" is not in this deployment's catalog` };
|
|
58
|
+
return unavailable("hook model", requested) ?? { ok: false, error: `hook model "${requested}" is not in this deployment's catalog` };
|
|
50
59
|
};
|
|
51
60
|
const judgeRoute = (pick, perModelKey, deploymentKey, declaredRoot) => {
|
|
52
61
|
const credential = resolveRouteCredential({
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* `scrubServerSecrets` 的结果)。
|
|
10
10
|
*
|
|
11
11
|
* ## 规则(一句)
|
|
12
|
-
* 语言服务器子进程的 env = {@link scrubServerSecrets}(process.env)
|
|
12
|
+
* 语言服务器子进程的 env = {@link scrubServerSecrets}(process.env) ∪ 部署方点名放回的键(S-699,见下节;缺省空名单)——
|
|
13
|
+
* 剥除与模型可驱动的 host shell(`inheritEnv:"scrub"`)
|
|
13
14
|
* 和 project-memory 的 git 子进程**同一只函数**:目录 `type:"secret"` 键按名剥 ∘ core 形状规则按形剥。只交 core 的默认
|
|
14
15
|
* 会漏掉形状规则认不出的本仓凭据键(今天是 `OTEL_EXPORTER_OTLP_HEADERS`),那正是 S-684 当初的病形。
|
|
15
16
|
* 剥掉了什么经既有观测面留痕(site = `server.lsp.spawn-env`,每 site×key 一次披露 + 逐次计数)—— core 那侧
|
|
@@ -19,11 +20,22 @@
|
|
|
19
20
|
* env 在**构造时**取一次快照(boot / run-local 起腿时),而不是 core 默认那样每次 spawn 现取:`NodeLspManagerOptions.env`
|
|
20
21
|
* 是一份静态记录。本仓的 `process.env` 是 boot 期输入(热配置走配置中心,不改 `process.env`),两者在本仓部署上同值。
|
|
21
22
|
*
|
|
23
|
+
* ## 点名放行(S-699,`LSP_ENV_ALLOW`)
|
|
24
|
+
* 剥除会连语言服务器自己的真实依赖一起剥掉(`.npmrc` 的 `${NPM_TOKEN}` 插值、rust-analyzer `cargo metadata` 私有 registry 的
|
|
25
|
+
* `CARGO_REGISTRIES_<名>_TOKEN` —— 形状规则按名判,分不出「给语言服务器用的令牌」与「编排器的令牌」)。出路 = 部署方点名:
|
|
26
|
+
* env = {@link scrubServerSecrets}(process.env) 之上把 `config.lspEnvAllow` 点到的键经 {@link allowlistedEnv}(与 host shell
|
|
27
|
+
* `inheritEnv:[names]` 同一只谓词)从原 env 原样放回;没点到的照旧剥。名单由 `config.ts` 解析(形错拒启;
|
|
28
|
+
* `REQUIRE_PRINCIPAL=true` 下落空名单并 warn 一次),本函数只读 `config.lspEnvAllow`,不在这里再判一次姿态。
|
|
29
|
+
* 放回的键不是剥除 ⇒ 不进 `secret_env_scrubbed` 的键表与计数;剥掉的键经既有现场 `server.lsp.spawn-env` 一行 warn
|
|
30
|
+
* (带当前放行名单与出路句),这一行就是「剥了什么」的用户可见面 —— 语言服务器侧从不报。
|
|
31
|
+
*
|
|
22
32
|
* ## 两个调用点
|
|
23
33
|
* `boot/execution-env.ts`(服务的 host 车道)与 `run-local.ts`(纯本地 CLI)此前各写一遍 `new NodeLspManager({ log })`;
|
|
24
34
|
* 现在都经本函数 —— 构造形只有一份,剥除规则也就只能有一份(源码对账钉在 `test/secret-env-scrub-disclosure.test.ts`)。
|
|
35
|
+
* 两处都交整份 `config`(类型只要 `lspEnvAllow` 一格),放行名单因此只有一个读点。
|
|
25
36
|
*/
|
|
26
37
|
import { NodeLspManager } from "@sema-agent/core";
|
|
38
|
+
import type { ServiceConfig } from "./config-types.js";
|
|
27
39
|
/** 构造宿主车道的本地语言服务器管理器(`create*`:返回活对象)。`log` = 管理器的观测钩(open/heal/degrade/evict 事件)。 */
|
|
28
|
-
export declare function createHostLspManager(log: (event: string, fields: Record<string, unknown>) => void): NodeLspManager;
|
|
40
|
+
export declare function createHostLspManager(config: Pick<ServiceConfig, "lspEnvAllow">, log: (event: string, fields: Record<string, unknown>) => void): NodeLspManager;
|
|
29
41
|
//# sourceMappingURL=host-lsp-manager.d.ts.map
|
package/dist/host-lsp-manager.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { NodeLspManager } from "@sema-agent/core";
|
|
2
2
|
import { recordSecretEnvScrub } from "./observability/secret-env-scrub.js";
|
|
3
|
-
import { scrubServerSecrets } from "./server-secret-env.js";
|
|
4
|
-
export function createHostLspManager(log) {
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
3
|
+
import { allowlistedEnv, scrubServerSecrets } from "./server-secret-env.js";
|
|
4
|
+
export function createHostLspManager(config, log) {
|
|
5
|
+
const scrubFindings = [];
|
|
6
|
+
const composed = { ...scrubServerSecrets(process.env, scrubFindings), ...allowlistedEnv(config.lspEnvAllow, process.env) };
|
|
7
|
+
const findings = scrubFindings.filter((f) => composed[f.key] === undefined);
|
|
8
|
+
recordSecretEnvScrub("server.lsp.spawn-env", findings, config.lspEnvAllow);
|
|
8
9
|
const env = {};
|
|
9
|
-
for (const [k, v] of Object.entries(
|
|
10
|
+
for (const [k, v] of Object.entries(composed))
|
|
10
11
|
if (v !== undefined)
|
|
11
12
|
env[k] = v;
|
|
12
13
|
return new NodeLspManager({ log, env });
|
package/dist/http/admission.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isThinkingLevel, expandTiers } from "@sema-agent/core";
|
|
2
|
-
import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
|
|
2
|
+
import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
|
|
3
|
+
import { modelUnavailableMessage } from "../model-entry-refusal.js";
|
|
3
4
|
import { HttpError } from "../security.js";
|
|
4
5
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, SETTINGS_PERMISSION_MODE_WORDS, isSettingsPermissionMode, parseTaskSettings, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
|
|
5
6
|
import { taskBodyKeyIssue } from "../request-key-closure.js";
|
|
@@ -39,7 +40,15 @@ const MAX_IMAGES_PER_REQUEST = 20;
|
|
|
39
40
|
const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
|
|
40
41
|
function mentionableNames(config, catalog) {
|
|
41
42
|
const allowlist = config.atModelAllowlist ?? [];
|
|
42
|
-
return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist));
|
|
43
|
+
return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist, config.modelRefusals));
|
|
44
|
+
}
|
|
45
|
+
function refuseUnavailableModel(res, config, field, ref) {
|
|
46
|
+
const hit = refusedCatalogEntry(ref, config.modelRefusals);
|
|
47
|
+
if (hit === undefined)
|
|
48
|
+
return false;
|
|
49
|
+
const message = modelUnavailableMessage(field, ref.slice(0, 120), hit.name, hit.refusal);
|
|
50
|
+
sendError(res, 400, "request.model_unavailable", message);
|
|
51
|
+
return true;
|
|
43
52
|
}
|
|
44
53
|
export function createAdmission(ctx) {
|
|
45
54
|
const { deps, readJson, runnerFor } = ctx;
|
|
@@ -419,8 +428,10 @@ export function createAdmission(ctx) {
|
|
|
419
428
|
return null;
|
|
420
429
|
}
|
|
421
430
|
const agentsBare = deps.config.models ?? {};
|
|
422
|
-
const agentsCatalog =
|
|
431
|
+
const agentsCatalog = selectableCatalog(agentsBare, deps.config.tiers, deps.config.modelRefusals);
|
|
423
432
|
const um = unknownTaskAgentModelRef(body.agents, agentsCatalog);
|
|
433
|
+
if (um !== undefined && refuseUnavailableModel(res, deps.config, `agents[${um.index}].model`, um.ref))
|
|
434
|
+
return null;
|
|
424
435
|
if (um !== undefined) {
|
|
425
436
|
sendError(res, 400, "request.unknown_reference", `unknown agents[${um.index}].model "${um.ref.slice(0, 120)}" — not in the configured catalog (a catalog name or tier word)`, { available: mentionableNames(deps.config, agentsCatalog) });
|
|
426
437
|
return null;
|
|
@@ -494,8 +505,10 @@ export function createAdmission(ctx) {
|
|
|
494
505
|
return null;
|
|
495
506
|
}
|
|
496
507
|
const bare = deps.config.models ?? {};
|
|
497
|
-
const catalog =
|
|
498
|
-
if (matchCatalogModel(body.model, catalog) === undefined) {
|
|
508
|
+
const catalog = selectableCatalog(bare, deps.config.tiers, deps.config.modelRefusals);
|
|
509
|
+
if (matchCatalogModel(body.model, catalog, deps.config.modelRefusals) === undefined) {
|
|
510
|
+
if (refuseUnavailableModel(res, deps.config, "model", body.model))
|
|
511
|
+
return null;
|
|
499
512
|
sendError(res, 400, "request.unknown_reference", `unknown model "${body.model.slice(0, 120)}" — not in the configured catalog (name, tier word, or id)`, { available: mentionableNames(deps.config, catalog) });
|
|
500
513
|
return null;
|
|
501
514
|
}
|
|
@@ -506,8 +519,10 @@ export function createAdmission(ctx) {
|
|
|
506
519
|
return null;
|
|
507
520
|
}
|
|
508
521
|
const bare = deps.config.models ?? {};
|
|
509
|
-
const catalog =
|
|
510
|
-
if (matchCatalogModel(body.compactionModel, catalog) === undefined) {
|
|
522
|
+
const catalog = selectableCatalog(bare, deps.config.tiers, deps.config.modelRefusals);
|
|
523
|
+
if (matchCatalogModel(body.compactionModel, catalog, deps.config.modelRefusals) === undefined) {
|
|
524
|
+
if (refuseUnavailableModel(res, deps.config, "compactionModel", body.compactionModel))
|
|
525
|
+
return null;
|
|
511
526
|
sendError(res, 400, "request.unknown_reference", `unknown compactionModel "${body.compactionModel.slice(0, 120)}" — not in the configured catalog (name, tier word, or id)`, { available: mentionableNames(deps.config, catalog) });
|
|
512
527
|
return null;
|
|
513
528
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core 7.31.1 #1094 `memory.target_outside_store` 的 **operator 面 HTTP 映射** —— 唯一一处字面量(判据在
|
|
3
|
+
* `src/memory-store-refusal.ts`)。挂在每一个触 store 树的 operator 记忆口的已分类收尾上:
|
|
4
|
+
* `routes/memory-compliance.ts`(出处 / 抹除)· `routes/memory-origin.ts`(外源列表 / 清标;**不含**只读控制面的清标账列表 ——
|
|
5
|
+
* core 的 wire 表逐字:只碰控制面的面不判,给它挂一条永远收不到的码路径正是附录 A 活触发门要防的形)·
|
|
6
|
+
* `routes/memory-bundle.ts`(导出 / 导入)。会话所有者的自助抹除口**不走这里**(`routes/sessions.ts`:同码、固定文案,见那里的注)。
|
|
7
|
+
* `POST /v1/admin/memory/consolidation/run` 也不走这里:那口把 core 的**任何**有码拒一律收成 409 `memory.consolidation_refused`
|
|
8
|
+
* (码进文案,附录 A 该行),本码已在那条臂上,不再给它第二个码。
|
|
9
|
+
*
|
|
10
|
+
* ## 为什么 409
|
|
11
|
+
* core 的读法:operator 要处置的拒(去掉 store 里那只通到外面的目录链),不是可重试的 IO 故障,也不是对调用方的裁决。与
|
|
12
|
+
* `memory.erasure_census_incomplete` 同一类(**当前 store 状态**下这次做不了;状态修好后重发即改判)⇒ 409,不 500(服务器没坏)、
|
|
13
|
+
* 不 4xx 请求族(请求没写错)。抹除面:链先于请求在场 ⇒ core 在提交点之前拒,零删除、零 journal(真 core 格钉在
|
|
14
|
+
* `test/memory-compliance-http.test.ts`);链在提交点之后才换上 ⇒ 已删的几条算数、journal 留盘 —— core 原句会带
|
|
15
|
+
* `after the commit point; the journal is kept`,本层原样转发、不嗅它分类(codex 一审 C1)。
|
|
16
|
+
*
|
|
17
|
+
* ## 文案原样
|
|
18
|
+
* core 的诊断句点名动作、路径与解析到哪 —— 那是运维找到那只链的唯一材料,与本族其它 core 码同一条「码与文案都不改写」纪律。
|
|
19
|
+
* 只对 operator 面这么做:这几口都过了 operator 门,宿主路径对这个受众不是泄露。
|
|
20
|
+
*/
|
|
21
|
+
import type { ServerResponse } from "node:http";
|
|
22
|
+
/** `err` 是 `memory.target_outside_store` ⇒ 409 带码 + core 原句,返回 true(已应答);否则 false,一个字节都不写。 */
|
|
23
|
+
export declare function sentMemoryStoreRefusal(res: ServerResponse, err: unknown): boolean;
|
|
24
|
+
//# sourceMappingURL=memory-store-refusal-reply.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { targetOutsideStoreOf } from "../memory-store-refusal.js";
|
|
2
|
+
import { sendError } from "./send.js";
|
|
3
|
+
export function sentMemoryStoreRefusal(res, err) {
|
|
4
|
+
const refusal = targetOutsideStoreOf(err);
|
|
5
|
+
if (refusal === undefined)
|
|
6
|
+
return false;
|
|
7
|
+
sendError(res, 409, "memory.target_outside_store", refusal.message);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=memory-store-refusal-reply.js.map
|
|
@@ -288,6 +288,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
288
288
|
...(s.deadline !== undefined ? { deadline: s.deadline } : {}),
|
|
289
289
|
...(s.hasBidiControls === true ? { hasBidiControls: true } : {}),
|
|
290
290
|
...(s.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
291
|
+
...(s.mandated === true ? { mandated: true } : {}),
|
|
291
292
|
...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
|
|
292
293
|
...(s.origin !== undefined ? { origin: s.origin } : {}),
|
|
293
294
|
...(s.ruleStoreUnreadable !== undefined ? { ruleStoreUnreadable: s.ruleStoreUnreadable } : {}),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
|
|
2
|
-
import { isModelAllowlisted } from "../../model-select.js";
|
|
2
|
+
import { isModelAllowlisted, selectableCatalog } from "../../model-select.js";
|
|
3
3
|
import { resolveModelApiKey } from "../../key-resolver.js";
|
|
4
4
|
import { isSealedKeyPoison } from "../../sealed-key.js";
|
|
5
5
|
import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
|
|
@@ -228,7 +228,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
228
228
|
return;
|
|
229
229
|
}
|
|
230
230
|
case "models": {
|
|
231
|
-
const atCatalog =
|
|
231
|
+
const atCatalog = selectableCatalog(deps.config.models, deps.config.tiers, deps.config.modelRefusals);
|
|
232
232
|
const atAllowlist = deps.config.atModelAllowlist ?? [];
|
|
233
233
|
const models = Object.entries(deps.config.models)
|
|
234
234
|
.filter(([name]) => name !== "default")
|
|
@@ -242,7 +242,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
242
242
|
...(m.maxTokens ? { maxOutputTokens: m.maxTokens } : {}),
|
|
243
243
|
...((levels) => (levels !== undefined ? { supportedEffortLevels: [...levels] } : {}))(reasoningEffortLevelsOf(m)),
|
|
244
244
|
...(m.compat !== undefined ? { compat: m.compat } : {}),
|
|
245
|
-
...(atAllowlist.length > 0 ? { atMentionable: isModelAllowlisted(name, atCatalog, atAllowlist) } : {}),
|
|
245
|
+
...(atAllowlist.length > 0 ? { atMentionable: isModelAllowlisted(name, atCatalog, atAllowlist, deps.config.modelRefusals) } : {}),
|
|
246
246
|
routePairing: routePairingOf(deps, name, m),
|
|
247
247
|
}));
|
|
248
248
|
const nonAlias = Object.entries(deps.config.models).filter(([n]) => n !== "default");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { sendJson, sendError } from "../send.js";
|
|
3
|
+
import { sentMemoryStoreRefusal } from "../memory-store-refusal-reply.js";
|
|
3
4
|
import { buildOriginFencedBundle, readOriginAware } from "../../memory-origin-fence.js";
|
|
4
5
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
5
6
|
import { assertNever } from "../route-ctx.js";
|
|
@@ -35,6 +36,8 @@ function sentClassifiedFailure(res, ctx, face, err) {
|
|
|
35
36
|
const code = codeOf(err);
|
|
36
37
|
if (code !== undefined && sentCoreCode(res, code, err instanceof Error ? err.message : String(err)))
|
|
37
38
|
return true;
|
|
39
|
+
if (sentMemoryStoreRefusal(res, err))
|
|
40
|
+
return true;
|
|
38
41
|
ctx.deps.logger?.warn?.(`memory_bundle_${face}_failed`, { err: String(err) });
|
|
39
42
|
return false;
|
|
40
43
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ControlPlaneCorruptError } from "@sema-agent/core";
|
|
3
3
|
import { erasureInputSmugglesPrototypeKey } from "../../memory-operator-faces.js";
|
|
4
|
-
import {
|
|
4
|
+
import { sentMemoryStoreRefusal } from "../memory-store-refusal-reply.js";
|
|
5
5
|
import { sendJson, sendError } from "../send.js";
|
|
6
6
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
7
7
|
import { assertNever } from "../route-ctx.js";
|
|
@@ -36,13 +36,11 @@ function codeOf(err) {
|
|
|
36
36
|
return typeof code === "string" ? code : undefined;
|
|
37
37
|
}
|
|
38
38
|
function sentClassifiedFailure(res, ctx, face, err) {
|
|
39
|
-
if (face === "erase" && err instanceof MemoryLayerUnwritableError) {
|
|
40
|
-
sendError(res, 409, "memory.layer_unwritable", `memory layer directories are not writable on disk: ${err.dirList} — refused before the engine was called: nothing was written and no journal was left. Restore the owner write bit with \`chmod u+w ${err.chmodTargets}\` and retry. This is a known issue of the memory engine's mount: it leaves a layer that a session reads but does not write read-only on disk (directories 0555, entries 0444) and does not restore it, so a later project session can lock it again`);
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
39
|
const coreCode = codeOf(err);
|
|
44
40
|
if (coreCode !== undefined && sentCoreCode(res, coreCode, err instanceof Error ? err.message : String(err)))
|
|
45
41
|
return true;
|
|
42
|
+
if (sentMemoryStoreRefusal(res, err))
|
|
43
|
+
return true;
|
|
46
44
|
if (err instanceof ControlPlaneCorruptError) {
|
|
47
45
|
sendError(res, 500, "internal.memory_control_plane_corrupt", err.message);
|
|
48
46
|
return true;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ControlPlaneCorruptError } from "@sema-agent/core";
|
|
3
3
|
import { sendJson, sendError } from "../send.js";
|
|
4
|
+
import { sentMemoryStoreRefusal } from "../memory-store-refusal-reply.js";
|
|
4
5
|
import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
|
|
5
|
-
import { MemoryLayerUnwritableError } from "../../memory-layer-preflight.js";
|
|
6
6
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
7
7
|
import { assertNever } from "../route-ctx.js";
|
|
8
8
|
export const MEMORY_ORIGIN_EXTERNAL_PATH = "/v1/memory/origin/external";
|
|
@@ -48,15 +48,13 @@ function codeOf(err) {
|
|
|
48
48
|
return typeof code === "string" ? code : undefined;
|
|
49
49
|
}
|
|
50
50
|
function sentClassifiedFailure(res, ctx, face, err) {
|
|
51
|
-
if (face === "clear" && err instanceof MemoryLayerUnwritableError) {
|
|
52
|
-
sendError(res, 409, "memory.layer_unwritable", `memory layer directories are not writable on disk: ${err.dirList} — refused before the engine was called: nothing was written and no journal was left. Restore the owner write bit with \`chmod u+w ${err.chmodTargets}\` and retry. This is a known issue of the memory engine's mount: it leaves a layer that a session reads but does not write read-only on disk (directories 0555, entries 0444) and does not restore it, so a later project session can lock it again`);
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
51
|
if (face === "clear") {
|
|
56
52
|
const coreCode = codeOf(err);
|
|
57
53
|
if (coreCode !== undefined && sentClearCode(res, coreCode, err instanceof Error ? err.message : String(err)))
|
|
58
54
|
return true;
|
|
59
55
|
}
|
|
56
|
+
if (face !== "clearances" && sentMemoryStoreRefusal(res, err))
|
|
57
|
+
return true;
|
|
60
58
|
if (err instanceof ControlPlaneCorruptError) {
|
|
61
59
|
sendError(res, 500, "internal.memory_control_plane_corrupt", err.message);
|
|
62
60
|
return true;
|
|
@@ -4,12 +4,12 @@ import { isUuidV7, isUuidShape, legacyUnownedSessionOk } from "../../security.js
|
|
|
4
4
|
import { windowMessages, truncateMessageBlobs } from "../../audit.js";
|
|
5
5
|
import { redactSecrets } from "../../trace/redact.js";
|
|
6
6
|
import { recordFailOpen } from "../../observability/fail-open.js";
|
|
7
|
+
import { targetOutsideStoreOf } from "../../memory-store-refusal.js";
|
|
7
8
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
|
|
8
9
|
import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
|
|
9
10
|
import { sendJson, sendError, sendSessionPolicyError, sseHeaders } from "../send.js";
|
|
10
11
|
import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "../principal-gate.js";
|
|
11
12
|
import { assertNever } from "../route-ctx.js";
|
|
12
|
-
import { MemoryLayerUnwritableError } from "../../memory-layer-preflight.js";
|
|
13
13
|
const MCP_STATUS_TTL_MS = 5_000;
|
|
14
14
|
const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
|
|
15
15
|
const A2A_STATUS_TTL_MS = 5_000;
|
|
@@ -500,16 +500,17 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
500
500
|
sendJson(res, 200, await faces.erase({ requestId: raw.requestId, select: { sessionId }, ...(raw.allowUnevidenced !== undefined ? { allowUnevidenced: raw.allowUnevidenced } : {}) }));
|
|
501
501
|
}
|
|
502
502
|
catch (err) {
|
|
503
|
-
if (err instanceof MemoryLayerUnwritableError) {
|
|
504
|
-
deps.logger?.warn?.("session_memory_erase_layer_unwritable", { sessionId, dirs: err.dirs });
|
|
505
|
-
sendError(res, 409, "memory.layer_unwritable", "memory erasure refused: a memory layer directory on this server is not writable on disk, so the erase was stopped before anything was written (nothing was deleted, no journal was left). Ask whoever operates this deployment to restore the owner write bit on the memory layer directories (a known issue of the memory engine's mount), then retry with the same requestId");
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
503
|
const code = err.code;
|
|
509
504
|
if (code === "config.memory_erasure_request") {
|
|
510
505
|
sendError(res, 400, "config.memory_erasure_request", err instanceof Error ? err.message : "invalid memory erasure request");
|
|
511
506
|
return;
|
|
512
507
|
}
|
|
508
|
+
const outside = targetOutsideStoreOf(err);
|
|
509
|
+
if (outside !== undefined) {
|
|
510
|
+
deps.logger?.warn?.("session_memory_erase_refused", { sessionId, err: outside.message });
|
|
511
|
+
sendError(res, 409, "memory.target_outside_store", "session memory erasure refused — a directory in this deployment's memory store leads outside it (a link), so this erasure did not complete (a refusal before its commit point changed nothing; one after it keeps the deletions already made and a journal that finishes the rest once the link is removed); ask the operator to remove the link, then resend with the same requestId");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
513
514
|
deps.logger?.warn?.("session_memory_erase_failed", { sessionId, err: String(err) });
|
|
514
515
|
sendError(res, 500, "internal.error", "session memory erasure failed");
|
|
515
516
|
}
|