@sema-agent/server 7.107.0 → 7.109.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +72 -1
- package/USAGE.md +51 -25
- package/dist/approval-ask-audit-store.d.ts +62 -4
- package/dist/approval-ask-audit-store.js +67 -11
- package/dist/approval-ask-machine.d.ts +9 -25
- package/dist/approval-ask-machine.js +5 -2
- package/dist/approval-card.d.ts +32 -3
- package/dist/approval-card.js +29 -2
- package/dist/approval.js +16 -13
- package/dist/bench/s1/live-deps.d.ts +4 -5
- package/dist/bench/s1/live-deps.js +9 -10
- package/dist/boot/coordinators.d.ts +4 -0
- package/dist/boot/coordinators.js +3 -2
- package/dist/boot/leader.js +0 -2
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +3 -8
- package/dist/boot/shutdown.d.ts +8 -0
- package/dist/boot/shutdown.js +31 -8
- package/dist/boot/stage-06-runners.d.ts +1 -0
- package/dist/boot/stage-06-runners.js +6 -3
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stage-10-http-server.js +3 -1
- package/dist/brain.js +2 -1
- package/dist/capabilities/scenarios.d.ts +3 -2
- package/dist/config-catalog.js +1 -3
- package/dist/config-center/apply-effective.js +2 -0
- package/dist/config-center/types.d.ts +7 -0
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +2 -9
- package/dist/config.js +2 -5
- package/dist/deployment-governance.js +2 -1
- package/dist/fleet-client.d.ts +10 -2
- package/dist/fleet-client.js +14 -8
- package/dist/http/admission.js +8 -2
- package/dist/http/memory-compliance-failure-reply.d.ts +41 -0
- package/dist/http/memory-compliance-failure-reply.js +73 -0
- package/dist/http/memory-store-refusal-reply.d.ts +2 -2
- package/dist/http/resume-legs.js +3 -2
- package/dist/http/route-ctx.d.ts +3 -3
- package/dist/http/routes/capabilities.js +1 -0
- package/dist/http/routes/diagnostics.js +2 -0
- package/dist/http/routes/memory-compliance.js +2 -34
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/sessions.js +3 -11
- package/dist/http/routes/side-query.js +7 -2
- package/dist/http/routes/tasks.js +2 -1
- package/dist/http/server.d.ts +2 -2
- package/dist/http/wire-types.d.ts +8 -0
- package/dist/leader/diffup.d.ts +53 -0
- package/dist/leader/diffup.js +95 -1
- package/dist/leader/leader.d.ts +0 -6
- package/dist/leader/leader.js +1 -1
- package/dist/leader/repair-wire.js +1 -1
- package/dist/leader/wire.d.ts +3 -10
- package/dist/leader/wire.js +39 -13
- package/dist/memory-store-refusal.d.ts +2 -1
- package/dist/model-entry-headers.d.ts +41 -0
- package/dist/model-entry-headers.js +61 -0
- package/dist/model-entry-refusal.d.ts +6 -0
- package/dist/model-entry-refusal.js +4 -0
- package/dist/model-reach-scope.d.ts +55 -0
- package/dist/model-reach-scope.js +16 -0
- package/dist/model-unreachable-notice.d.ts +38 -0
- package/dist/model-unreachable-notice.js +72 -0
- package/dist/observability/fail-open.d.ts +5 -5
- package/dist/observability/fail-open.js +5 -5
- package/dist/observability/run-terminal.d.ts +2 -1
- package/dist/outbound-dispatcher.d.ts +69 -0
- package/dist/outbound-dispatcher.js +123 -3
- package/dist/plugins/approval-ask-store-file.d.ts +2 -1
- package/dist/plugins/approval-ask-store-file.js +3 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
- package/dist/plugins/approval-ask-store-memory.js +7 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +18 -0
- package/dist/plugins/approval-ask-store-sql.js +7 -0
- package/dist/plugins/park-commit-watch.d.ts +48 -0
- package/dist/plugins/park-commit-watch.js +87 -0
- package/dist/request-key-closure.js +1 -1
- package/dist/router/route-orchestration.js +7 -2
- package/dist/run-local.js +2 -3
- package/dist/runs.js +3 -2
- package/dist/runtime-governance.d.ts +3 -3
- package/dist/runtime-governance.js +3 -2
- package/dist/task-settings.js +4 -2
- package/dist/tool-approval.d.ts +99 -15
- package/dist/tool-approval.js +75 -10
- package/dist/tool-rule-names.d.ts +25 -0
- package/dist/tool-rule-names.js +5 -0
- package/dist/trace/core-keyset-guard.d.ts +14 -2
- package/dist/trace/engine-notice-wire.d.ts +3 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/project.d.ts +3 -1
- package/dist/trace/project.js +2 -3
- package/dist/trace/self-minted-wire.d.ts +71 -0
- package/dist/trace/self-minted-wire.js +28 -0
- package/dist/unattended-permission-mode.d.ts +54 -93
- package/dist/unattended-permission-mode.js +8 -61
- package/package.json +3 -3
|
@@ -4,7 +4,7 @@ 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 {
|
|
7
|
+
import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
|
|
8
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";
|
|
9
9
|
import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
|
|
10
10
|
import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
|
|
@@ -526,17 +526,9 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
526
526
|
sendJson(res, 200, await faces.erase({ requestId: raw.requestId, select: { sessionId }, ...(raw.allowUnevidenced !== undefined ? { allowUnevidenced: raw.allowUnevidenced } : {}) }));
|
|
527
527
|
}
|
|
528
528
|
catch (err) {
|
|
529
|
-
const
|
|
530
|
-
if (
|
|
531
|
-
sendError(res, 400, "config.memory_erasure_request", err instanceof Error ? err.message : "invalid memory erasure request");
|
|
529
|
+
const withheld = (coreMessage) => deps.logger?.warn?.("session_memory_erase_refused", { sessionId, err: coreMessage });
|
|
530
|
+
if (sentMemoryComplianceFailure(res, err, { kind: "session_owner", withheld }))
|
|
532
531
|
return;
|
|
533
|
-
}
|
|
534
|
-
const outside = targetOutsideStoreOf(err);
|
|
535
|
-
if (outside !== undefined) {
|
|
536
|
-
deps.logger?.warn?.("session_memory_erase_refused", { sessionId, err: outside.message });
|
|
537
|
-
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");
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
532
|
deps.logger?.warn?.("session_memory_erase_failed", { sessionId, err: String(err) });
|
|
541
533
|
sendError(res, 500, "internal.error", "session memory erasure failed");
|
|
542
534
|
}
|
|
@@ -3,7 +3,7 @@ import { HttpError } from "../../security.js";
|
|
|
3
3
|
import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../../model-select.js";
|
|
4
4
|
import { modelUnavailableMessage } from "../../model-entry-refusal.js";
|
|
5
5
|
import { redactSecrets } from "../../trace/redact.js";
|
|
6
|
-
import { admitProviderEcho } from "../../observability/run-terminal.js";
|
|
6
|
+
import { admitProviderEcho, redactErrorMessage } from "../../observability/run-terminal.js";
|
|
7
7
|
import { sendJson, sendError, httpErrorCode, msg } from "../send.js";
|
|
8
8
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
9
9
|
import { assertNever } from "../route-ctx.js";
|
|
@@ -35,6 +35,11 @@ function shapeSideQueryEcho(result) {
|
|
|
35
35
|
const { responseModel: _outOfShape, ...admitted } = result;
|
|
36
36
|
return admitted;
|
|
37
37
|
}
|
|
38
|
+
function projectSideQueryResult(result) {
|
|
39
|
+
const shaped = shapeSideQueryEcho(result);
|
|
40
|
+
const redacted = redactErrorMessage(shaped.errorMessage);
|
|
41
|
+
return redacted === undefined || redacted === shaped.errorMessage ? shaped : { ...shaped, errorMessage: redacted };
|
|
42
|
+
}
|
|
38
43
|
export async function handleSideQuery(req, res, match, ctx) {
|
|
39
44
|
const { deps } = ctx;
|
|
40
45
|
const { readJson, rateLimited, quotaExceeded, leaseDenied } = ctx.helpers;
|
|
@@ -107,7 +112,7 @@ export async function handleSideQuery(req, res, match, ctx) {
|
|
|
107
112
|
deps.sideQueryAccounting?.(principal, { model: modelId, usage: rr.usage });
|
|
108
113
|
deps.metrics?.inc?.("side_query_total", { result: "ok" });
|
|
109
114
|
if (!res.writableEnded)
|
|
110
|
-
sendJson(res, 200,
|
|
115
|
+
sendJson(res, 200, projectSideQueryResult(result));
|
|
111
116
|
}
|
|
112
117
|
catch (e) {
|
|
113
118
|
deps.metrics?.inc?.("side_query_total", { result: "error" });
|
|
@@ -27,6 +27,7 @@ import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate
|
|
|
27
27
|
import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
|
|
28
28
|
import { buildSyncSubmitBody } from "../sync-submit-body.js";
|
|
29
29
|
import { assertNever } from "../route-ctx.js";
|
|
30
|
+
import { withModelReachScope } from "../../model-reach-scope.js";
|
|
30
31
|
const DISCONNECT_PARK_GRACE_MS = 10_000;
|
|
31
32
|
function sseData(res, payload) {
|
|
32
33
|
const anchor = payload.eventId;
|
|
@@ -644,7 +645,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
644
645
|
});
|
|
645
646
|
}
|
|
646
647
|
const withApproval = () => approvalCtx && deps.toolApproval ? deps.toolApproval.runWithContext(approvalCtx, withQuestion) : withQuestion();
|
|
647
|
-
await (deps.sendUserFile
|
|
648
|
+
await withModelReachScope(prepared.spec.sessionId, () => deps.sendUserFile
|
|
648
649
|
? deps.sendUserFile.runWithContext({
|
|
649
650
|
taskId: askTaskId,
|
|
650
651
|
emit: async (f) => {
|
package/dist/http/server.d.ts
CHANGED
|
@@ -105,8 +105,8 @@ export interface ServiceCoreDeps {
|
|
|
105
105
|
* ⚠️ 7.69.0(R-1)起 **resume 家族也递这一位** —— 四条 resume 腿收敛到 `resolveResumeSpec` 一只 resolver,
|
|
106
106
|
* 通告口接一次、四腿同得(见 http/server.ts 的 `resolveResumeSpec` 顶注);此处旧注写作「resume 家族缺席」
|
|
107
107
|
* 已过期,按新行为改。
|
|
108
|
-
* `opts.unattendedLeg`(S-877 候裁 18,codex r3 F1):续跑家族在 ctx 带无人值守标(`CheckpointCtx.unattendedLeg`)时递 `true` ——
|
|
109
|
-
*
|
|
108
|
+
* `opts.unattendedLeg`(S-877 候裁 18,codex r3 F1;S-934 ① 改形):续跑家族在 ctx 带无人值守标(`CheckpointCtx.unattendedLeg`)时递 `true` ——
|
|
109
|
+
* 装配器在**全部折叠之后**经首跑同一只合并口 `withUnattendedLeg` 重挂那条腿(`default` + 按类作答的 onAsk)。只由续跑腿
|
|
110
110
|
* 按服务端写的 ctx 位递,绝不从请求体读。 */
|
|
111
111
|
resolveSpec: (body: TaskRequestBody, req: IncomingMessage | undefined, auth?: RequestAuth, opts?: {
|
|
112
112
|
leg?: "fresh" | "resume";
|
|
@@ -293,6 +293,14 @@ export interface TaskRequestBody {
|
|
|
293
293
|
* 探测:本键在不在受理面由**键闭集**自己回答 —— 没接线的 server 版本对它 400 `request.body_shape`
|
|
294
294
|
* 并逐字列名,所以**不加能力位**(一个键一条探测路,不长第二条)。 */
|
|
295
295
|
excludeAllTools?: boolean;
|
|
296
|
+
/** S-946(cli L-920 请托;CC 2.1.280 `--disable-slash-commands`「Disable all skills」,cli280.js:240760 整张命令表置空):
|
|
297
|
+
* **这一发一只技能都不装** —— 场景出厂技能(启动期烤入 + 中心下发)、调用方 `skills`、子代定义的 `agents[].skills`
|
|
298
|
+
* 一个都不进 `TaskSpec`。`true` ⇒ `TaskSpec.skills = []`(core 对空表:零 `<skills>` 块、`Skill` 工具不挂);
|
|
299
|
+
* `false` / 缺席 = 技能照旧。值形门与 {@link excludeAllTools} **同一只**(非 boolean ⇒ 400 `request.field_invalid`);
|
|
300
|
+
* 与非空 `skills` 或任一非空 `agents[].skills` 同带 ⇒ 400 `request.field_conflict`(体内自相矛盾,拒在受理门)。
|
|
301
|
+
* 续跑重放逐字重放存量体 ⇒ 同一只写点同判(坏形 defensive:只认 literal `true`)。无租户门(收窄,不跨租户)。
|
|
302
|
+
* 探测位 `GET /v1/capabilities` 的 `disableSkills`(与本键同一个词)。 */
|
|
303
|
+
disableSkills?: boolean;
|
|
296
304
|
/** [ref]② (core 1.314 工具面控制批): roster 真卸载清单(schema 不上 wire,manifest 收窄)。wire 工具名;
|
|
297
305
|
* tighten-only(core 继承不变量 union 进子 spawn)。数组-of-string fail-loud at submit。 */
|
|
298
306
|
excludeTools?: string[];
|
package/dist/leader/diffup.d.ts
CHANGED
|
@@ -27,6 +27,59 @@ export declare function buildUploadScript(p: {
|
|
|
27
27
|
}): string;
|
|
28
28
|
/** The objective suffix that makes the upload the worker's last task step (no URL through the model). */
|
|
29
29
|
export declare function uploadStepSuffix(workDir: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* S-949 —— the INCREMENTAL upload loop's lifetime protocol (single writer; the wire only runs its two verbs).
|
|
32
|
+
*
|
|
33
|
+
* While a worker env lives, a background loop re-runs the staged upload script every {@link UPLOAD_LOOP_PERIOD_SEC}
|
|
34
|
+
* seconds, so the worker's fixed diff key tracks its latest COMMITTED progress and salvage survives an env hard-crash
|
|
35
|
+
* (#16 tail: run 019eca71's "4 commits vaporized"). The loop must NOT outlive the env: the pre-S-949 form
|
|
36
|
+
* (`nohup sh -c 'while true; …' &`) relied on "the pod dies, the loop dies", which holds only where the env IS a
|
|
37
|
+
* pod — on an env whose shell is the host's (`RemoteHostExecutionEnv`, core `NodeExecutionEnv`) teardown does not
|
|
38
|
+
* reap detached processes (by contract), and the loop became a ppid-1 orphan (254 of them on one box, 2026-10-01).
|
|
39
|
+
*
|
|
40
|
+
* ONE mechanism for every lane, carried entirely by the env's ordinary `exec` (no lane branch, no process-group or
|
|
41
|
+
* `/proc` assumption, never a signal to a pid that might have been reused):
|
|
42
|
+
* - **run token** `.leader-upload-loop.gen` — the generation allowed to run. `start` draws the next number from
|
|
43
|
+
* the monotonic counter `.leader-upload-loop.seq` (never reset by `stop` — a counter that restarted at 1 after a
|
|
44
|
+
* `stop` past its bound would hand a fresh loop the generation of a revoked loop still finishing its upload, and
|
|
45
|
+
* that one would carry on), writes it here BEFORE forking and hands it to the loop; the loop runs only while this
|
|
46
|
+
* file still names its generation. So a later `start` (a resumed leg re-staging the same workspace) supersedes an
|
|
47
|
+
* earlier loop, `stop` (which removes the file) ends every loop, and a workspace deleted out from under it ends it
|
|
48
|
+
* too — the token check IS the loop's only exit, there is no second mechanism;
|
|
49
|
+
* - **one pid file per generation** `.leader-upload-loop.pid.<gen>` — written by `start` right after the fork (the
|
|
50
|
+
* wire awaits `start`, so it is on disk before any `stop` can run) and removed by that loop as it exits. `stop`
|
|
51
|
+
* waits until no generation is left, so a superseded loop whose upload was still in flight at the re-stage is
|
|
52
|
+
* waited for too (codex r1 F2). It only PROBES a pid (`kill -0`): a loop that died abnormally does not hold it up;
|
|
53
|
+
* - **bounded uploads** — the loop runs each upload with a PUT time bound ({@link UPLOAD_LOOP_UPLOAD_MAX_SEC},
|
|
54
|
+
* `curl --max-time`), so a hung object store cannot pin a revoked loop alive (codex r1 F1: on a host env that was
|
|
55
|
+
* an orphan stuck in curl). `stopWaitSec` must exceed that bound plus one tick (the builder refuses otherwise);
|
|
56
|
+
* - **ordering** — the loop checks the token right before every upload and runs it in the foreground, so once
|
|
57
|
+
* `stop` returns 0 no loop upload of any generation is running or can start: the teardown belt upload that follows
|
|
58
|
+
* is the last write to the diff key. A `stop` past its bound (a hung `git format-patch`, or a pid file whose pid
|
|
59
|
+
* now belongs to someone else) exits non-zero — the wire warns and still runs the belt (dropping the belt would
|
|
60
|
+
* lose the uncommitted-work capture it exists for).
|
|
61
|
+
*/
|
|
62
|
+
/** Seconds between two loop runs of the upload script. Single writer: the wire passes THIS constant; tests inject a
|
|
63
|
+
* short period through {@link buildUploadLoopScript}'s parameter. Deliberately not a deployment knob. */
|
|
64
|
+
export declare const UPLOAD_LOOP_PERIOD_SEC = 90;
|
|
65
|
+
/** Where the loop script is staged — next to the upload script, same `workDir` rule. */
|
|
66
|
+
export declare function uploadLoopScriptPath(workDir: string): string;
|
|
67
|
+
/** The two lifecycle verbs the wire runs through the env's ordinary exec (closed set — `run` is the loop's own,
|
|
68
|
+
* internal entry point and is never issued from outside). */
|
|
69
|
+
export type UploadLoopVerb = "start" | "stop";
|
|
70
|
+
/** The shell line for one lifecycle verb. `stop` revokes the run token FIRST and independently of the script (a loop
|
|
71
|
+
* whose script file went missing still sees its token gone and exits), then lets the script's `stop` wait for it. */
|
|
72
|
+
export declare function uploadLoopCommand(workDir: string, verb: UploadLoopVerb): string;
|
|
73
|
+
/** The staged loop script (see the protocol note above). The three bounds are interpolated into the shell ⇒
|
|
74
|
+
* validated here, fail-closed: positive integers, and `stopWaitSec > uploadMaxSec + 1` (stop outlasts one bounded
|
|
75
|
+
* upload plus the loop's one-second tick — otherwise "stop returned ⇒ no upload in flight" would be false by
|
|
76
|
+
* construction). */
|
|
77
|
+
export declare function buildUploadLoopScript(p: {
|
|
78
|
+
workDir: string;
|
|
79
|
+
periodSec: number;
|
|
80
|
+
uploadMaxSec?: number;
|
|
81
|
+
stopWaitSec?: number;
|
|
82
|
+
}): string;
|
|
30
83
|
/**
|
|
31
84
|
* Fetch the worker-uploaded patch over a presigned GET (control plane; creds stay adapter-held). Returns the
|
|
32
85
|
* same result shape as pullDiff so merge consumes either path. 🔴 The presigned URL is a short-lived bearer
|
package/dist/leader/diffup.js
CHANGED
|
@@ -8,9 +8,10 @@ export function buildUploadScript(p) {
|
|
|
8
8
|
"#!/bin/sh",
|
|
9
9
|
"# leader diff-out (design/68 F2(a)) — exports this worker's branch as a patch series. Do not edit.",
|
|
10
10
|
"set -e",
|
|
11
|
+
'case "$1" in ""|*[!0-9]*) M="" ;; *) M="--max-time $1" ;; esac',
|
|
11
12
|
`cd ${p.repoDir}`,
|
|
12
13
|
`git format-patch --stdout ${p.baseSha}..HEAD > ${p.workDir}/.leader-out.patch`,
|
|
13
|
-
`curl -fsS -X PUT -T ${p.workDir}/.leader-out.patch '${putUrl}'`,
|
|
14
|
+
`curl -fsS $M -X PUT -T ${p.workDir}/.leader-out.patch '${putUrl}'`,
|
|
14
15
|
`echo leader-diff-uploaded`,
|
|
15
16
|
"",
|
|
16
17
|
].join("\n");
|
|
@@ -18,6 +19,99 @@ export function buildUploadScript(p) {
|
|
|
18
19
|
export function uploadStepSuffix(workDir) {
|
|
19
20
|
return `\n\nMANDATORY FINAL STEP: after your final commit, run exactly: sh ${workDir}/.leader-upload.sh — it exports your branch diff for integration. Do not modify, inspect, or skip it; if it fails, fix your git state (commit your work) and run it again.`;
|
|
20
21
|
}
|
|
22
|
+
export const UPLOAD_LOOP_PERIOD_SEC = 90;
|
|
23
|
+
const UPLOAD_LOOP_UPLOAD_MAX_SEC = 25;
|
|
24
|
+
const UPLOAD_LOOP_STOP_WAIT_SEC = 30;
|
|
25
|
+
export function uploadLoopScriptPath(workDir) {
|
|
26
|
+
return `${workDir}/.leader-upload-loop.sh`;
|
|
27
|
+
}
|
|
28
|
+
const uploadLoopTokenPath = (workDir) => `${workDir}/.leader-upload-loop.gen`;
|
|
29
|
+
export function uploadLoopCommand(workDir, verb) {
|
|
30
|
+
const script = uploadLoopScriptPath(workDir);
|
|
31
|
+
switch (verb) {
|
|
32
|
+
case "start":
|
|
33
|
+
return `sh ${script} start`;
|
|
34
|
+
case "stop":
|
|
35
|
+
return `rm -f ${uploadLoopTokenPath(workDir)}; sh ${script} stop`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function buildUploadLoopScript(p) {
|
|
39
|
+
const uploadMaxSec = p.uploadMaxSec ?? UPLOAD_LOOP_UPLOAD_MAX_SEC;
|
|
40
|
+
const stopWaitSec = p.stopWaitSec ?? UPLOAD_LOOP_STOP_WAIT_SEC;
|
|
41
|
+
for (const [name, n] of [["periodSec", p.periodSec], ["uploadMaxSec", uploadMaxSec], ["stopWaitSec", stopWaitSec]]) {
|
|
42
|
+
if (!Number.isInteger(n) || n < 1)
|
|
43
|
+
throw new Error(`buildUploadLoopScript: ${name} must be a positive integer, got ${JSON.stringify(n)}`);
|
|
44
|
+
}
|
|
45
|
+
if (stopWaitSec <= uploadMaxSec + 1) {
|
|
46
|
+
throw new Error(`buildUploadLoopScript: stopWaitSec (${stopWaitSec}) must exceed uploadMaxSec (${uploadMaxSec}) + 1 — stop has to outlast one bounded upload plus the loop's tick`);
|
|
47
|
+
}
|
|
48
|
+
const script = uploadLoopScriptPath(p.workDir);
|
|
49
|
+
return [
|
|
50
|
+
"#!/bin/sh",
|
|
51
|
+
"# leader incremental upload loop (S-949) — re-runs .leader-upload.sh while this env lives. Do not edit.",
|
|
52
|
+
"# start: launch ONE detached loop (an earlier loop here sees its generation superseded and exits); record its pid.",
|
|
53
|
+
"# stop: revoke the run token, then wait until no loop of any generation is left (uploads in flight finish first).",
|
|
54
|
+
"# run: the loop itself (internal) — runs only while the token file still names its generation.",
|
|
55
|
+
`G=${uploadLoopTokenPath(p.workDir)}`,
|
|
56
|
+
`Q=${p.workDir}/.leader-upload-loop.seq`,
|
|
57
|
+
`P=${p.workDir}/.leader-upload-loop.pid`,
|
|
58
|
+
"stop_all() {",
|
|
59
|
+
' rm -f "$G"',
|
|
60
|
+
" i=0",
|
|
61
|
+
" while :; do",
|
|
62
|
+
" live=",
|
|
63
|
+
' for f in "$P".*; do',
|
|
64
|
+
' [ -f "$f" ] || continue',
|
|
65
|
+
" p=",
|
|
66
|
+
' read -r p 2>/dev/null < "$f"',
|
|
67
|
+
' if [ -n "$p" ] && kill -0 "$p" 2>/dev/null; then live="$live $p"; else rm -f "$f"; fi',
|
|
68
|
+
" done",
|
|
69
|
+
' [ -z "$live" ] && return 0',
|
|
70
|
+
` if [ "$i" -ge ${stopWaitSec} ]; then`,
|
|
71
|
+
` echo "leader upload loop(s) still running ${stopWaitSec}s after the run token was revoked (pid$live)" >&2`,
|
|
72
|
+
" return 1",
|
|
73
|
+
" fi",
|
|
74
|
+
" sleep 1",
|
|
75
|
+
" i=$((i + 1))",
|
|
76
|
+
" done",
|
|
77
|
+
"}",
|
|
78
|
+
'case "$1" in',
|
|
79
|
+
"start)",
|
|
80
|
+
' command -v nohup >/dev/null 2>&1 || { echo "nohup not found — upload loop not started" >&2; exit 1; }',
|
|
81
|
+
" g=0",
|
|
82
|
+
' [ -f "$Q" ] && read -r g < "$Q"',
|
|
83
|
+
" case $g in ''|*[!0-9]*) g=0 ;; esac",
|
|
84
|
+
" g=$((g + 1))",
|
|
85
|
+
' echo "$g" > "$Q"',
|
|
86
|
+
' echo "$g" > "$G"',
|
|
87
|
+
` nohup sh ${script} run "$g" </dev/null >/dev/null 2>&1 &`,
|
|
88
|
+
' echo "$!" > "$P.$g"',
|
|
89
|
+
" ;;",
|
|
90
|
+
"run)",
|
|
91
|
+
" n=0",
|
|
92
|
+
' while read -r g 2>/dev/null < "$G" && [ "$g" = "$2" ]; do',
|
|
93
|
+
` if [ "$n" -ge ${p.periodSec} ]; then`,
|
|
94
|
+
" n=0",
|
|
95
|
+
` sh ${p.workDir}/.leader-upload.sh ${uploadMaxSec} >/dev/null 2>&1 || true`,
|
|
96
|
+
" else",
|
|
97
|
+
" sleep 1",
|
|
98
|
+
" n=$((n + 1))",
|
|
99
|
+
" fi",
|
|
100
|
+
" done",
|
|
101
|
+
' rm -f "$P.$2"',
|
|
102
|
+
" exit 0",
|
|
103
|
+
" ;;",
|
|
104
|
+
"stop)",
|
|
105
|
+
" stop_all",
|
|
106
|
+
" ;;",
|
|
107
|
+
"*)",
|
|
108
|
+
' echo "usage: sh $0 start|stop" >&2',
|
|
109
|
+
" exit 2",
|
|
110
|
+
" ;;",
|
|
111
|
+
"esac",
|
|
112
|
+
"",
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
21
115
|
export async function fetchUploadedDiff(getUrl, opts = {}) {
|
|
22
116
|
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_DIFF_BYTES;
|
|
23
117
|
const f = opts.fetchImpl ?? fetch;
|
package/dist/leader/leader.d.ts
CHANGED
|
@@ -16,7 +16,6 @@ import { type RunnerLike, type WorkerReport } from "./fanout.js";
|
|
|
16
16
|
import { type ExecStreamLike } from "./diffout.js";
|
|
17
17
|
import { type MergeDeps, type MergeResult } from "./merge.js";
|
|
18
18
|
import { type TaskSpec, type VerifyConfig, type CheckpointToken, type TerminalCause, type ToolSpec, type RepairTerminal, type OracleTier, type RepairResult } from "@sema-agent/core";
|
|
19
|
-
import { type UnattendedLeg } from "../unattended-permission-mode.js";
|
|
20
19
|
/** [ref] Slice 6: drive a worker's resource-suspend auto-resume to a terminal (or bound) state. Exported
|
|
21
20
|
* for unit tests. Loops `resume(token, slice)` WHILE the result's terminal cause is a park on a
|
|
22
21
|
* `resource_limit` gate. Exits on: a terminal cause that is not a park, a park on any OTHER gate
|
|
@@ -68,11 +67,6 @@ export interface ProvisionedWorker {
|
|
|
68
67
|
export interface LeaderDeps {
|
|
69
68
|
/** Plan a task into N disjoint-file sub-tasks (MVP: simple/hand-authored; adaptive replan = scale). */
|
|
70
69
|
plan: (task: string) => Promise<SubtaskSpec[]>;
|
|
71
|
-
/** S-877 候裁 15 / 18 —— 本车道有手的 run 并进的无人值守腿(模式 = 运维旋钮 `UNATTENDED_PERMISSION_MODE`、写面 roots、模式释放写保护时的
|
|
72
|
-
* 写保护拒层),由 `wire.ts` 经 `createUnattendedLeg` 铸一次递进来。本文件自己起的有手 run(模块门修复)与 `repair-wire.ts` 的单代理修复环按
|
|
73
|
-
* `sub.spec` 起 run、不经 provisionWorker —— 两处都经 `withUnattendedLeg` 并进这一份。**必填**(缺席 = core「Absent = default」⇒ 无人可答的腿上
|
|
74
|
-
* 写与非只读命令被当场拒,codex F1 的病)。 */
|
|
75
|
-
unattendedLeg: UnattendedLeg;
|
|
76
70
|
/** Provision a leader-owned worker (its env seeded with base at `branch` + an env-bound runner). */
|
|
77
71
|
provisionWorker: (sub: SubtaskSpec) => Promise<ProvisionedWorker>;
|
|
78
72
|
/** Ephemeral no-creds integration sandbox + the credentialed Coordinator push (merge(b) deps). */
|
package/dist/leader/leader.js
CHANGED
|
@@ -201,7 +201,7 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
|
|
|
201
201
|
}
|
|
202
202
|
deps.logger?.info?.("module_repair", { workerId: r.workerId, attempt: attempt + 1, reason: redactHead(verdict.reason ?? "", 500) });
|
|
203
203
|
try {
|
|
204
|
-
const rr = await w.runner.runTaskStream(withUnattendedLeg({ ...sub.spec, objective: deps.moduleGate.repairObjective(sub, safeVerdictReason), sessionId: `${w.sessionId}-r${attempt + 1}` }
|
|
204
|
+
const rr = await w.runner.runTaskStream(withUnattendedLeg({ ...sub.spec, objective: deps.moduleGate.repairObjective(sub, safeVerdictReason), sessionId: `${w.sessionId}-r${attempt + 1}` })).result();
|
|
205
205
|
if (r.stats && rr?.stats)
|
|
206
206
|
r.stats = { ...r.stats, tokens: (r.stats.tokens ?? 0) + (rr.stats.tokens ?? 0), costMicroUsd: (r.stats.costMicroUsd ?? 0) + (rr.stats.costMicroUsd ?? 0) };
|
|
207
207
|
}
|
|
@@ -87,7 +87,7 @@ export function attachRepairLoopDeps(deps, resolve) {
|
|
|
87
87
|
deps.repairLoop = {
|
|
88
88
|
run: async (solo, sub) => {
|
|
89
89
|
const cfg = await resolve(solo, sub);
|
|
90
|
-
const implSpec = withUnattendedLeg({ ...sub.spec, sessionId: solo.sessionId }
|
|
90
|
+
const implSpec = withUnattendedLeg({ ...sub.spec, sessionId: solo.sessionId });
|
|
91
91
|
return runRepairLoopForLeader(solo, implSpec, cfg);
|
|
92
92
|
},
|
|
93
93
|
};
|
package/dist/leader/wire.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Brain, type Model, type ModelRoles, type ModelPricing, type TaskSpec, type ExecutionEnvFactory, type RemoteExecutionEnv, type ToolResultStore, type SessionStore, type ExecutionEnv, type RunnerDeps
|
|
1
|
+
import { type Brain, type Model, type ModelRoles, type ModelPricing, type TaskSpec, type ExecutionEnvFactory, type RemoteExecutionEnv, type ToolResultStore, type SessionStore, type ExecutionEnv, type RunnerDeps } from "@sema-agent/core";
|
|
2
2
|
import type { CheckpointStoreFull } from "../plugins/store-backend.js";
|
|
3
3
|
import { type LeaderDeps, type LeaderResult } from "./leader.js";
|
|
4
4
|
import { type DeploymentPostureSeats } from "../boot/runner-deps.js";
|
|
@@ -19,14 +19,6 @@ export interface LeaderWireConfig {
|
|
|
19
19
|
* REMOTE_EXEC (e2b or k8s/Kata) — workers and the integration sandbox run on envs from THIS factory.
|
|
20
20
|
* Requires `s3` (diff-out is the worker's self-upload; there is no leader-held env to pull from). */
|
|
21
21
|
envFactory?: ExecutionEnvFactory;
|
|
22
|
-
/**
|
|
23
|
-
* S-877 候裁 15 → B —— 本车道**有手**的每一条 run(worker 两车道、集成沙箱修复、模块门修复、单代理修复环)的
|
|
24
|
-
* `TaskSpec.permissionMode`。**必填**:缺席 = core「Absent = `default`」⇒ 写与非只读命令在无人可答的这条腿上被
|
|
25
|
-
* headless 当场拒(codex F1 的病);必填让每一个构造本配置的地方都得表态,不许静默落回那一形。
|
|
26
|
-
* 唯一取值点 = `boot/leader.ts` 调 `unattendedPermissionMode(config)`(`src/unattended-permission-mode.ts`,与 run-local 同一只);
|
|
27
|
-
* 本文件经 `createUnattendedLeg` / `withUnattendedLeg` 铸并(模式 + 写面 roots + 候裁 18 的写保护拒层)。planner 路由无手,不读。
|
|
28
|
-
*/
|
|
29
|
-
unattendedPermissionMode: PermissionMode;
|
|
30
22
|
/** Durable suspend/resume for SUB-WORKERS ([ref] C4): when the seat yields a value, that worker task carries
|
|
31
23
|
* the durable question/ask policy + checkpoint store, so a gated ask/AskUserQuestion SUSPENDS the worker
|
|
32
24
|
* (checkpoint + paused env) instead of failing — the trigger replan-lite C4 arbitrates on. Mirrors main.ts's
|
|
@@ -165,7 +157,8 @@ export interface LeaderWireConfig {
|
|
|
165
157
|
/** The env surface wire needs (both the E2B and k8s adapters implement core's RemoteExecutionEnv). */
|
|
166
158
|
type WireEnv = Pick<RemoteExecutionEnv, "exec" | "writeFile">;
|
|
167
159
|
/**
|
|
168
|
-
* [ref] —— leader 自铸沙盒的**回收闸**(集成沙盒 / repair-loop grader
|
|
160
|
+
* [ref] —— leader 自铸沙盒的**回收闸**(集成沙盒 / repair-loop grader 各一只;判据本身 S-949 起抽成下方
|
|
161
|
+
* {@link assertLeaderOwnedEnvReapable},worker 腿同判)。
|
|
169
162
|
*
|
|
170
163
|
* core 契约:`ExecutionEnv` 基面**没有** `destroy`;只有实现了 `RemoteExecutionEnv` 的适配器才有
|
|
171
164
|
* (`core/remote-env.d.ts`),而「这只 env 到底有没有」是**运行期结构事实**,core 自己也只用结构探测
|
package/dist/leader/wire.js
CHANGED
|
@@ -5,13 +5,13 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { Runner, combinePolicies, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, delimitUntrusted, hasDestroy, } from "@sema-agent/core";
|
|
8
|
-
import {
|
|
8
|
+
import { withUnattendedLeg } from "../unattended-permission-mode.js";
|
|
9
9
|
import { RemoteContainerExecutionEnv } from "../plugins/remote-env-e2b.js";
|
|
10
10
|
import { E2B_SANDBOX_HOME } from "../execution-lane-caps.js";
|
|
11
11
|
import { createDurableAskPolicy } from "../approval.js";
|
|
12
12
|
import { gitSubprocessEnv } from "../server-secret-env.js";
|
|
13
13
|
import { presignS3Url } from "../plugins/s3-presign.js";
|
|
14
|
-
import { buildUploadScript, uploadStepSuffix, fetchUploadedDiff, withStaging } from "./diffup.js";
|
|
14
|
+
import { buildUploadScript, buildUploadLoopScript, uploadLoopCommand, uploadLoopScriptPath, uploadStepSuffix, fetchUploadedDiff, withStaging, UPLOAD_LOOP_PERIOD_SEC } from "./diffup.js";
|
|
15
15
|
import { pullDiff, safeRev } from "./diffout.js";
|
|
16
16
|
import { runLeaderTask } from "./leader.js";
|
|
17
17
|
import { attachRepairLoopDeps } from "./repair-wire.js";
|
|
@@ -37,10 +37,13 @@ const asIntegrationEnv = (env) => ({
|
|
|
37
37
|
throw r.error; },
|
|
38
38
|
});
|
|
39
39
|
export function createEnvReaper(env, what) {
|
|
40
|
+
assertLeaderOwnedEnvReapable(env, what);
|
|
41
|
+
return async () => void (await env.destroy());
|
|
42
|
+
}
|
|
43
|
+
function assertLeaderOwnedEnvReapable(env, what) {
|
|
40
44
|
if (!hasDestroy(env)) {
|
|
41
|
-
throw new Error(`${what}: the execution-env factory returned an env with no destroy() — the leader OWNS this dedicated sandbox and
|
|
45
|
+
throw new Error(`${what}: the execution-env factory returned an env with no destroy() — the leader OWNS this dedicated sandbox and destroy() is its only teardown (the leader's own reaper for the integration / grader sandboxes; core's run teardown for a worker, where the staged incremental-upload loop and the belt upload ride it), so without it the sandbox and whatever the leader started inside it leak until the backend's own timeout reaps them — and a host-shell env has no such timeout. Deploy a factory whose env implements RemoteExecutionEnv.destroy for the leader lane.`);
|
|
42
46
|
}
|
|
43
|
-
return async () => void (await env.destroy());
|
|
44
47
|
}
|
|
45
48
|
export async function stageWorkerEnv(env, opts) {
|
|
46
49
|
const { seedCmd, repo, branch, uploadScriptPath, uploadScript } = opts;
|
|
@@ -133,13 +136,32 @@ export function createCoordinatorPush(durableRemote, targetRef, ident) {
|
|
|
133
136
|
export function createLeaderRunner(cfg) {
|
|
134
137
|
const W = cfg.workspace ?? "/home/user";
|
|
135
138
|
const repo = `${W}/repo`;
|
|
139
|
+
const uploadLoopScript = buildUploadLoopScript({ workDir: W, periodSec: UPLOAD_LOOP_PERIOD_SEC });
|
|
140
|
+
const startUploadLoop = async (env, workerId) => {
|
|
141
|
+
try {
|
|
142
|
+
const wr = await env.writeFile(uploadLoopScriptPath(W), uploadLoopScript);
|
|
143
|
+
if (!wr.ok)
|
|
144
|
+
throw wr.error;
|
|
145
|
+
await sh(env)(uploadLoopCommand(W, "start"));
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
cfg.logger?.warn?.("leader_upload_loop_start_failed", { workerId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const stopUploadLoop = async (env, workerId) => {
|
|
152
|
+
try {
|
|
153
|
+
await sh(env)(uploadLoopCommand(W, "stop"));
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
cfg.logger?.warn?.("leader_upload_loop_stop_failed", { workerId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
|
|
157
|
+
}
|
|
158
|
+
};
|
|
136
159
|
const ident = cfg.git ?? { name: "leader", email: "leader@local" };
|
|
137
160
|
const { leaderTimeoutMs, workerLimits, presignTtlSec, resourceSuspend: resourceCfg } = leaderResourceConfig();
|
|
138
161
|
const { repairRounds, repairBudgetUsd, repairLoopOn, measureGatesOn, repairLoopAttempts, oracleFlakyK, replanBudgetUsd, } = leaderLoopConfig({ repairRounds: cfg.repairRounds });
|
|
139
162
|
const onNoticeSeat = createEngineNoticeSeat(cfg.logger);
|
|
140
163
|
const governanceSeat = cfg.governance ?? {};
|
|
141
164
|
const deploymentPostureSeat = () => cfg.deploymentPosture?.() ?? {};
|
|
142
|
-
const unattendedLeg = () => createUnattendedLeg(cfg.unattendedPermissionMode, deploymentPostureSeat());
|
|
143
165
|
const tracerSeat = cfg.tracer ? { tracer: cfg.tracer } : {};
|
|
144
166
|
const usageWindowSeat = cfg.usageWindows && cfg.usageWindowStore ? { usageWindows: cfg.usageWindows, usageWindowStore: cfg.usageWindowStore } : {};
|
|
145
167
|
const promptSourceSeat = cfg.promptSource ? { promptSource: cfg.promptSource } : {};
|
|
@@ -176,7 +198,7 @@ export function createLeaderRunner(cfg) {
|
|
|
176
198
|
...principalSpec(),
|
|
177
199
|
sessionId: `leader-repair-${Date.now()}-${round}`,
|
|
178
200
|
limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
|
|
179
|
-
}
|
|
201
|
+
}))
|
|
180
202
|
.result();
|
|
181
203
|
if (res.terminal.kind !== "completed")
|
|
182
204
|
throw new Error(`repair round ${round} did not complete (status=${outwardPlane(res.terminal).status})`);
|
|
@@ -311,7 +333,7 @@ export function createLeaderRunner(cfg) {
|
|
|
311
333
|
const script = buildUploadScript({ repoDir: repo, baseSha, putUrl, workDir: W });
|
|
312
334
|
const stage = async (env) => {
|
|
313
335
|
await stageWorkerEnv(env, { seedCmd, repo, branch: sub.branch, uploadScriptPath: `${W}/.leader-upload.sh`, uploadScript: script });
|
|
314
|
-
|
|
336
|
+
await startUploadLoop(env, sub.workerId);
|
|
315
337
|
};
|
|
316
338
|
return {
|
|
317
339
|
workerId: sub.workerId, sessionId, branch: sub.branch,
|
|
@@ -320,12 +342,17 @@ export function createLeaderRunner(cfg) {
|
|
|
320
342
|
brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat(), ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat,
|
|
321
343
|
...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}),
|
|
322
344
|
...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}),
|
|
323
|
-
executionEnvFactory: async (ctx) =>
|
|
324
|
-
|
|
325
|
-
|
|
345
|
+
executionEnvFactory: async (ctx) => {
|
|
346
|
+
const env = await envFactory(ctx);
|
|
347
|
+
assertLeaderOwnedEnvReapable(env, "leader worker sandbox");
|
|
348
|
+
return withStaging(env, stage, async (e) => {
|
|
349
|
+
await stopUploadLoop(e, sub.workerId);
|
|
350
|
+
await sh(e)(`cd ${repo} && git add -A && git -c user.name='leader-belt' -c user.email='belt@leader' commit -qm 'belt: auto-commit uncommitted worker output' || true; sh ${W}/.leader-upload.sh`);
|
|
351
|
+
});
|
|
352
|
+
},
|
|
326
353
|
})),
|
|
327
354
|
fetchDiff: () => fetchUploadedDiff(getUrl),
|
|
328
|
-
spec: withUnattendedLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` }
|
|
355
|
+
spec: withUnattendedLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` }),
|
|
329
356
|
};
|
|
330
357
|
}
|
|
331
358
|
if (!cfg.e2bApiKey)
|
|
@@ -340,7 +367,7 @@ export function createLeaderRunner(cfg) {
|
|
|
340
367
|
runner: keepCtxWarm(new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat(), ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}), ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}), executionEnv: env })),
|
|
341
368
|
diffEnv: env,
|
|
342
369
|
destroy: () => env.destroy().then(() => { }),
|
|
343
|
-
spec: withUnattendedLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() }
|
|
370
|
+
spec: withUnattendedLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() }),
|
|
344
371
|
};
|
|
345
372
|
};
|
|
346
373
|
const provisionIntegrationSandbox = async () => {
|
|
@@ -382,7 +409,6 @@ export function createLeaderRunner(cfg) {
|
|
|
382
409
|
const deps = {
|
|
383
410
|
plan,
|
|
384
411
|
provisionWorker,
|
|
385
|
-
unattendedLeg: unattendedLeg(),
|
|
386
412
|
provisionIntegrationSandbox,
|
|
387
413
|
push,
|
|
388
414
|
testCmd: body.testCmd,
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ## 两条到达车道,两个读者(本模块各给一只,别处不许再写第二份判据)
|
|
10
10
|
* ① **抛出面**(抹除 / 出处 / 外源列表 / 清标 / bundle 导出导入 —— 凡触 store 树的 backend 面):`.code` 就是码
|
|
11
|
-
* ⇒ {@link targetOutsideStoreOf}。HTTP 映射在 `http/memory-store-refusal-reply.ts`(409
|
|
11
|
+
* ⇒ {@link targetOutsideStoreOf}。HTTP 映射在 `http/memory-store-refusal-reply.ts`(409,operator 面一处字面量)与
|
|
12
|
+
* `http/memory-compliance-failure-reply.ts`(合规面两受众的那一只;会话所有者受众换固定句,S-930)。
|
|
12
13
|
* ② **报告面**(harvest `incident`):core 契约写明 report 车道上码**只在文本里** —— `sidecar_corrupt` 的 detail 恰两种形
|
|
13
14
|
* `memory commit refused: memory.target_outside_store: …` / `memory control plane refused: memory.target_outside_store: …`
|
|
14
15
|
* (core `docs/wire-consumption/7.31.1.md` 的 harvest 行逐字)⇒ {@link targetOutsideStoreInIncident} 只认**这两个 lead 紧跟码**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 白名单 = 名字 → 读它的线路(闭集;**加一个名字 = 加一行并写明哪条线路读它、为什么是部署的事**)。
|
|
3
|
+
* `anthropic-beta`:Anthropic Messages 的 beta 开关(逗号分隔的 beta 名;core anthropic 腿会把自己的 beta 并进同一个头)。
|
|
4
|
+
*/
|
|
5
|
+
export declare const CATALOG_ENTRY_HEADER_LANES: {
|
|
6
|
+
readonly "anthropic-beta": readonly ["anthropic-messages"];
|
|
7
|
+
};
|
|
8
|
+
export type CatalogEntryHeaderName = keyof typeof CATALOG_ENTRY_HEADER_LANES;
|
|
9
|
+
export declare const CATALOG_ENTRY_HEADER_NAMES: readonly CatalogEntryHeaderName[];
|
|
10
|
+
/** 判过的袋(只含白名单里的名字,值逐字)。 */
|
|
11
|
+
export type CatalogEntryHeaders = Readonly<Partial<Record<CatalogEntryHeaderName, string>>>;
|
|
12
|
+
/** 值的长度上界(字符)。`anthropic-beta` 的真实值是几枚逗号分隔的 beta 名,远在此下;上界只挡「把别的东西塞进来」。 */
|
|
13
|
+
export declare const MAX_HEADER_VALUE_CHARS = 1024;
|
|
14
|
+
/**
|
|
15
|
+
* 拒因 / warn 里名字的回显尺:token 形、≤64、且脱敏器不改它 ⇒ 照写;否则固定占位(名字位也可能被塞进秘密,值更是从不回显)。
|
|
16
|
+
*/
|
|
17
|
+
export declare function displayHeaderName(name: string): string;
|
|
18
|
+
/** 判决:`ok` ⇒ 判过的袋(`undefined` = 条目没声明任何头:缺席与 `{}` 同一个读法,都不铸键);否则 ⇒ 坏字段坐标 + 一句不带值的理由。 */
|
|
19
|
+
export type CatalogEntryHeadersVerdict = {
|
|
20
|
+
readonly ok: true;
|
|
21
|
+
readonly headers: CatalogEntryHeaders | undefined;
|
|
22
|
+
} | {
|
|
23
|
+
readonly ok: false;
|
|
24
|
+
readonly fields: readonly string[];
|
|
25
|
+
readonly issue: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* **唯一判据**:条目的 `headers` 原值(`unknown`:承载形,不信类型)+ 条目的线路词原值(`api`;两条读腿恒补齐它,
|
|
29
|
+
* 绕过读腿的输入缺它 ⇒ 按「没有线路读它」拒 —— 判据与铸点读同一个原值,不另补一份 schema 缺省)。
|
|
30
|
+
* 整只判:任一名字 / 值不合 ⇒ 整只不收(留下好的那几个 = 让运维以为整袋生效了)。坏字段逐个列出(`headers.<名>`)。
|
|
31
|
+
*/
|
|
32
|
+
export declare function readCatalogEntryHeaders(raw: unknown, api: unknown): CatalogEntryHeadersVerdict;
|
|
33
|
+
/**
|
|
34
|
+
* **唯一铸点**(`toModel` 调):判过的袋 → `Model.headers` 的键。缺席 ⇒ 不铸键(一个 `undefined` 值的键也不铸:
|
|
35
|
+
* 缺席只有一种拼法)。判不过 ⇒ **抛**:到这里的条目已经过 S-704 逐条判据(判不过的进 `modelRefusals`、到不了 `toModel`),
|
|
36
|
+
* 抛 = 两处判据分叉了,stage 整候选拒、响亮,不静默丢头。
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildModelHeadersKey(entryName: string, raw: unknown, api: unknown): {
|
|
39
|
+
headers?: Record<string, string>;
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=model-entry-headers.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { redactSecrets } from "./trace/redact.js";
|
|
2
|
+
export const CATALOG_ENTRY_HEADER_LANES = {
|
|
3
|
+
"anthropic-beta": ["anthropic-messages"],
|
|
4
|
+
};
|
|
5
|
+
export const CATALOG_ENTRY_HEADER_NAMES = Object.keys(CATALOG_ENTRY_HEADER_LANES);
|
|
6
|
+
export const MAX_HEADER_VALUE_CHARS = 1024;
|
|
7
|
+
const HEADER_VALUE_SHAPE = /^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?$/;
|
|
8
|
+
const HEADER_NAME_TOKEN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/;
|
|
9
|
+
export function displayHeaderName(name) {
|
|
10
|
+
return HEADER_NAME_TOKEN.test(name) && redactSecrets(name) === name ? name : "(unprintable header name)";
|
|
11
|
+
}
|
|
12
|
+
function isCatalogEntryHeaderName(name) {
|
|
13
|
+
return Object.hasOwn(CATALOG_ENTRY_HEADER_LANES, name);
|
|
14
|
+
}
|
|
15
|
+
export function readCatalogEntryHeaders(raw, api) {
|
|
16
|
+
if (raw === undefined)
|
|
17
|
+
return { ok: true, headers: undefined };
|
|
18
|
+
const proto = typeof raw === "object" && raw !== null ? Object.getPrototypeOf(raw) : undefined;
|
|
19
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw) || (proto !== Object.prototype && proto !== null)) {
|
|
20
|
+
return { ok: false, fields: ["headers"], issue: `headers: expected a plain object of { <header name>: <value> } (allowed names: ${CATALOG_ENTRY_HEADER_NAMES.join(", ")})` };
|
|
21
|
+
}
|
|
22
|
+
const fields = [];
|
|
23
|
+
const issues = [];
|
|
24
|
+
const bag = {};
|
|
25
|
+
for (const name of Object.keys(raw)) {
|
|
26
|
+
const shown = displayHeaderName(name);
|
|
27
|
+
const field = `headers.${shown}`;
|
|
28
|
+
const value = Reflect.get(raw, name);
|
|
29
|
+
if (!isCatalogEntryHeaderName(name)) {
|
|
30
|
+
const canonical = CATALOG_ENTRY_HEADER_NAMES.find((n) => n === name.toLowerCase());
|
|
31
|
+
fields.push(field);
|
|
32
|
+
issues.push(canonical !== undefined
|
|
33
|
+
? `${field}: write the header name in its canonical spelling "${canonical}" (one spelling per header)`
|
|
34
|
+
: `${field}: not in the closed allowlist of catalog request headers (${CATALOG_ENTRY_HEADER_NAMES.join(", ")}); credentials ride sealedApiKey / apiKeyEnv, never a header`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const lanes = CATALOG_ENTRY_HEADER_LANES[name];
|
|
38
|
+
if (typeof api !== "string" || !lanes.includes(api)) {
|
|
39
|
+
fields.push(field);
|
|
40
|
+
issues.push(`${field}: only read on the ${lanes.join(" / ")} lane — this entry ${typeof api === "string" ? `is api "${api}"` : "declares no api"}, where the header would be sent and do nothing`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value !== "string" || value.length > MAX_HEADER_VALUE_CHARS || !HEADER_VALUE_SHAPE.test(value)) {
|
|
44
|
+
fields.push(field);
|
|
45
|
+
issues.push(`${field}: the value must be a non-empty string of visible ASCII (spaces only between words, at most ${MAX_HEADER_VALUE_CHARS} characters)`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
bag[name] = value;
|
|
49
|
+
}
|
|
50
|
+
if (fields.length > 0)
|
|
51
|
+
return { ok: false, fields: [...new Set(fields)], issue: issues.join("; ") };
|
|
52
|
+
return { ok: true, headers: Object.keys(bag).length > 0 ? bag : undefined };
|
|
53
|
+
}
|
|
54
|
+
export function buildModelHeadersKey(entryName, raw, api) {
|
|
55
|
+
const verdict = readCatalogEntryHeaders(raw, api);
|
|
56
|
+
if (!verdict.ok) {
|
|
57
|
+
throw new Error(`catalog entry "${entryName}" reached the Model mint with unjudged headers (${verdict.fields.join(", ")}) — judgeCatalogEntry must refuse it first; this is a server defect, not a config error`);
|
|
58
|
+
}
|
|
59
|
+
return verdict.headers === undefined ? {} : { headers: { ...verdict.headers } };
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=model-entry-headers.js.map
|
|
@@ -43,9 +43,15 @@ export interface CatalogEntryLike {
|
|
|
43
43
|
readonly name: string;
|
|
44
44
|
readonly id: string;
|
|
45
45
|
readonly provider: string;
|
|
46
|
+
/** S-931:线路词(`headers` 的线路判据读它;原值,不信这张型)。 */
|
|
47
|
+
readonly api?: unknown;
|
|
48
|
+
/** S-931:供应商附加头的承载形(`unknown`:判据在 `model-entry-headers.ts`,不信这张型)。 */
|
|
49
|
+
readonly headers?: unknown;
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
52
|
* **逐条判据**:`undefined` = 本部署能完整服务它。判序即语义:先形(不合形的条目谈不上走哪条腿),再腿。
|
|
53
|
+
* 形分两段、同一个码(`config.model_entry_invalid`):settings-schema 的 `ModelEntry`,再是 S-931 的 `headers`(白名单 + 线路 + 值形,
|
|
54
|
+
* 判据单属主 `model-entry-headers.ts`;settings-schema 6.3.0 的 `ModelEntry` 还没有这个键 —— 候裁加字段,届时词表不另抄)。
|
|
49
55
|
*
|
|
50
56
|
* 形判的属主是 settings-schema 的 `ModelEntry`(两条读腿用的就是它)—— 本仓不手抄任何一条字段规则;这里只是
|
|
51
57
|
* 在第二道边界(绕过读腿的输入)上**再问一次同一个属主**,并把答案变成一条带码的拒因。只取 `success` 与 issue
|