@sema-agent/server 7.44.0 → 7.45.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 -0
- package/README.zh-CN.md +2 -0
- package/USAGE.md +10 -1
- package/dist/approval-card.d.ts +5 -0
- package/dist/approval-card.js +2 -0
- package/dist/boot/config-center.js +22 -11
- package/dist/boot/execution-env.js +1 -0
- package/dist/boot/shutdown.d.ts +18 -2
- package/dist/boot/shutdown.js +54 -12
- package/dist/boot/side-query-lane.d.ts +31 -46
- package/dist/boot/side-query-lane.js +3 -15
- package/dist/boot/webfetch-summarize-lane.d.ts +3 -2
- package/dist/boot/webfetch-summarize-lane.js +1 -1
- package/dist/brain.js +32 -1
- package/dist/config-center/apply-effective.js +3 -2
- package/dist/config-types.d.ts +16 -1
- package/dist/config.js +68 -3
- package/dist/hooks/hook-llm.js +29 -20
- package/dist/hooks/hook-runner.js +3 -0
- package/dist/http/routes/capabilities.js +10 -1
- package/dist/http/routes/runs.js +4 -0
- package/dist/http/server.d.ts +12 -1
- package/dist/http/server.js +6 -3
- package/dist/leader/merge.js +4 -2
- package/dist/main.js +6 -2
- package/dist/memory-sync.js +15 -2
- package/dist/observability/fail-open.d.ts +10 -2
- package/dist/observability/fail-open.js +10 -2
- package/dist/orchestration/hardened-vm-runner.d.ts +29 -0
- package/dist/orchestration/hardened-vm-runner.js +52 -4
- package/dist/plugins/remote-env-ssh.d.ts +3 -0
- package/dist/plugins/remote-env-ssh.js +20 -1
- package/dist/plugins/send-user-file.js +2 -1
- package/dist/plugins/tidb-session-storage.d.ts +8 -6
- package/dist/plugins/tidb-session-storage.js +2 -5
- package/dist/rules-consent.d.ts +19 -16
- package/dist/rules-consent.js +18 -1
- package/dist/run-local.js +6 -1
- package/dist/session-titler.js +2 -1
- package/dist/sighup-idle.d.ts +16 -2
- package/dist/sighup-idle.js +10 -1
- package/dist/ssh-host-key.d.ts +65 -0
- package/dist/ssh-host-key.js +117 -0
- package/dist/text-bidi.d.ts +50 -0
- package/dist/text-bidi.js +8 -0
- package/dist/tool-approval.d.ts +56 -2
- package/dist/tool-approval.js +95 -14
- package/package.json +2 -2
package/dist/config-types.d.ts
CHANGED
|
@@ -518,6 +518,10 @@ export interface ServiceConfigFlat {
|
|
|
518
518
|
username: string;
|
|
519
519
|
privateKey: string;
|
|
520
520
|
mountPath?: string;
|
|
521
|
+
hostKey?: {
|
|
522
|
+
fingerprintSha256?: string;
|
|
523
|
+
knownHostsPath?: string;
|
|
524
|
+
};
|
|
521
525
|
} | {
|
|
522
526
|
provider: "adb";
|
|
523
527
|
serial: string;
|
|
@@ -965,6 +969,17 @@ export interface ServiceConfigFlat {
|
|
|
965
969
|
* set `PLAN_MODE_ENABLED=false` to opt out. USER-driven plan (`body.permissionMode=plan` → enablePlanMode +
|
|
966
970
|
* handsReadOnly, read-only from start) is unaffected/independent. */
|
|
967
971
|
planModeEnabled: boolean;
|
|
972
|
+
/**
|
|
973
|
+
* [5118]⑥(core 5.56 `Hooks.timeoutMs` 座)—— **每一只 hook 席位单次调用的时间上限**(ms)。
|
|
974
|
+
* `HOOKS_TIMEOUT_MS`;**缺席 = 不铸键**,由 core 自默认(600s = CC 自己的数;server 不复制上游默认)。
|
|
975
|
+
*
|
|
976
|
+
* 🔴 `0` 是有效值(core 契约逐字:写下零的部署说了一件事 —— 每个席位立即到期),不是「未设」;
|
|
977
|
+
* 坏值(非整数 / 负 / 超 `setTimeout` 32 位上限)**拒启**,解析见 config.ts `parseHooksTimeoutEnv`。
|
|
978
|
+
* 施加点 = `main.ts` 的 `deploymentHooks`(server 车道)与 `run-local.ts` 的同名记录(CLI 本地车道,
|
|
979
|
+
* merge-rescan 745 补折:该腿今日仅同步计数器席位、折上为防倒退)。server 车道一处施加两条腿同时
|
|
980
|
+
* 生效:既是 `RunnerDeps.hooks`,又是 `resolve-spec` 折进 task hooks 的部署基线。
|
|
981
|
+
*/
|
|
982
|
+
hooksTimeoutMs?: number;
|
|
968
983
|
/** SVC-2 (adversarial-review): the workflow_journal GC retention — the reaper purges journal rows older than
|
|
969
984
|
* this (bounds the heaviest, TaskResult-bearing table; a resume older than it re-runs live). Default 7 days.
|
|
970
985
|
* `WORKFLOW_JOURNAL_RETENTION_MS`. */
|
|
@@ -1401,7 +1416,7 @@ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled"
|
|
|
1401
1416
|
* config 这一层它只是一句声明。 */
|
|
1402
1417
|
export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "compliancePosture" | "lockedConfigKeys" | "retentionPolicy" | "retentionSweep" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
|
|
1403
1418
|
/** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
|
|
1404
|
-
export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes" | "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
|
|
1419
|
+
export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes" | "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
|
|
1405
1420
|
/** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
|
|
1406
1421
|
export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "sighupIdleGraceMs" | "parentPid" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
|
|
1407
1422
|
/** 组:observability(可观测)。 */
|
package/dist/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -9,6 +10,7 @@ import { isV2ScopeKey } from "./memory-scope.js";
|
|
|
9
10
|
import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
|
|
10
11
|
import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
|
|
11
12
|
import { RETENTION_MODES } from "./config-types.js";
|
|
13
|
+
import { normalizeSshFingerprint } from "./ssh-host-key.js";
|
|
12
14
|
export { RETENTION_MODES } from "./config-types.js";
|
|
13
15
|
function csv(name) {
|
|
14
16
|
return (process.env[name] ?? "")
|
|
@@ -362,6 +364,35 @@ function headerNameEnv(name, fallback) {
|
|
|
362
364
|
throw new Error(`env ${name}="${v}" must be a single header name ([A-Za-z0-9_-]+)`);
|
|
363
365
|
return v;
|
|
364
366
|
}
|
|
367
|
+
function parseHooksTimeoutEnv() {
|
|
368
|
+
const raw = process.env.HOOKS_TIMEOUT_MS;
|
|
369
|
+
if (raw === undefined || raw === "")
|
|
370
|
+
return undefined;
|
|
371
|
+
const n = parseNumOrFail("HOOKS_TIMEOUT_MS", raw);
|
|
372
|
+
if (!Number.isInteger(n) || n < 0 || n > 2147483647) {
|
|
373
|
+
throw new Error(`env HOOKS_TIMEOUT_MS="${raw}" must be an integer in [0, 2147483647] ms (0 = every hook seat expires immediately; unset = core's own default)`);
|
|
374
|
+
}
|
|
375
|
+
return n;
|
|
376
|
+
}
|
|
377
|
+
function parseSshHostKeyEnv() {
|
|
378
|
+
const fpRaw = process.env.SSH_HOST_FINGERPRINT;
|
|
379
|
+
const khRaw = process.env.SSH_KNOWN_HOSTS;
|
|
380
|
+
const fingerprintSha256 = fpRaw !== undefined && fpRaw !== "" ? normalizeSshFingerprint(fpRaw) : undefined;
|
|
381
|
+
let knownHostsPath;
|
|
382
|
+
if (khRaw !== undefined && khRaw !== "") {
|
|
383
|
+
try {
|
|
384
|
+
readFileSync(khRaw, "utf8");
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
throw new Error(`SSH_KNOWN_HOSTS=${JSON.stringify(khRaw)} is not a readable file (${e instanceof Error ? e.message : String(e)}) — ` +
|
|
388
|
+
`refusing to start rather than pretending host-key verification is configured (#296 / C-R21).`);
|
|
389
|
+
}
|
|
390
|
+
knownHostsPath = khRaw;
|
|
391
|
+
}
|
|
392
|
+
if (fingerprintSha256 === undefined && knownHostsPath === undefined)
|
|
393
|
+
return undefined;
|
|
394
|
+
return { ...(fingerprintSha256 !== undefined ? { fingerprintSha256 } : {}), ...(knownHostsPath !== undefined ? { knownHostsPath } : {}) };
|
|
395
|
+
}
|
|
365
396
|
function normalizeSshPrivateKey(raw) {
|
|
366
397
|
const v = raw.trim();
|
|
367
398
|
if (v.includes("BEGIN ") && v.includes("PRIVATE KEY")) {
|
|
@@ -416,6 +447,22 @@ export function applyAutoCompactWindow(m, explicit) {
|
|
|
416
447
|
delete target.autoCompactTokens;
|
|
417
448
|
return was1m ? "removed-1m" : "removed";
|
|
418
449
|
}
|
|
450
|
+
function noticeAutoCompactAboveWindow(slot, envName, m) {
|
|
451
|
+
const act = m.autoCompactTokens;
|
|
452
|
+
const window = m.contextWindow ?? 0;
|
|
453
|
+
if (act === undefined || window <= 0 || act <= window)
|
|
454
|
+
return;
|
|
455
|
+
CONFIG_NOTICES.push({
|
|
456
|
+
event: "auto_compact_tokens_above_context_window",
|
|
457
|
+
fields: {
|
|
458
|
+
slot,
|
|
459
|
+
model: m.id,
|
|
460
|
+
value: String(act),
|
|
461
|
+
contextWindow: String(window),
|
|
462
|
+
note: `${envName}=${act} is ABOVE the ${slot} model "${m.id}" physical context window (${window}) — core uses autoCompactTokens as the TRIGGER-side window, so a value above the physical window means autocompaction effectively never fires and the destructive context guard truncates history instead (the opposite of what this knob is for). The value is applied as configured (not clamped, not rejected); lower it below the window — CC's own shape is window-33000.`,
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
}
|
|
419
466
|
function parseStoreDomain(ctx) {
|
|
420
467
|
const { requirePrincipal } = ctx;
|
|
421
468
|
const dbBackendSet = !!process.env.DB_BACKEND;
|
|
@@ -577,12 +624,28 @@ function parseModelDomain() {
|
|
|
577
624
|
CONFIG_NOTICES.push({
|
|
578
625
|
event: "auto_compact_window_not_derived_1m",
|
|
579
626
|
fields: {
|
|
627
|
+
slot: "main",
|
|
628
|
+
model: model.id,
|
|
580
629
|
note: `model "${model.id}" declares a >=1M context window but no autoCompactTokens — earlier versions implicitly applied 967000 here, which is CC's claude-sonnet-5-only per-model value (A-065 P2-6 rescinded the width-based derivation). Set MODEL_AUTO_COMPACT_TOKENS for a dual-window trigger; without it core uses its plain W-33000 trigger geometry on the full window.`,
|
|
581
630
|
},
|
|
582
631
|
});
|
|
583
632
|
}
|
|
584
|
-
|
|
585
|
-
|
|
633
|
+
noticeAutoCompactAboveWindow("main", "MODEL_AUTO_COMPACT_TOKENS", model);
|
|
634
|
+
if (cheapModel) {
|
|
635
|
+
const cheapAct = optFinitePositiveEnv("MODEL_CHEAP_AUTO_COMPACT_TOKENS");
|
|
636
|
+
if (applyAutoCompactWindow(cheapModel, cheapAct) === "removed-1m") {
|
|
637
|
+
const inherited = process.env.MODEL_CHEAP_CONTEXT_WINDOW === undefined || process.env.MODEL_CHEAP_CONTEXT_WINDOW === "";
|
|
638
|
+
CONFIG_NOTICES.push({
|
|
639
|
+
event: "auto_compact_window_not_derived_1m",
|
|
640
|
+
fields: {
|
|
641
|
+
slot: "cheap",
|
|
642
|
+
model: cheapModel.id,
|
|
643
|
+
note: `cheap slot model "${cheapModel.id}" declares a >=1M context window (${cheapModel.contextWindow}${inherited ? ", inherited from the main model — MODEL_CHEAP_CONTEXT_WINDOW is unset" : ""}) but no autoCompactTokens — earlier versions implicitly applied 967000 to this slot too, which is CC's claude-sonnet-5-only per-model value (A-065 P2-6 rescinded the width-based derivation). Set MODEL_CHEAP_AUTO_COMPACT_TOKENS for a dual-window trigger on this slot (MODEL_AUTO_COMPACT_TOKENS is the MAIN slot's knob and is deliberately NOT inherited — it is declared against the main model's id/window); without it core uses its plain W-33000 trigger geometry on the full window for the subagent/summarize roles.`,
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
noticeAutoCompactAboveWindow("cheap", "MODEL_CHEAP_AUTO_COMPACT_TOKENS", cheapModel);
|
|
648
|
+
}
|
|
586
649
|
const models = { default: model, [model.id]: model };
|
|
587
650
|
if (cheapModel)
|
|
588
651
|
models[cheapModel.id] = cheapModel;
|
|
@@ -1189,6 +1252,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1189
1252
|
privateKey: normalizeSshPrivateKey(process.env.SSH_PRIVATE_KEY),
|
|
1190
1253
|
...((n) => (n !== undefined ? { port: n } : {}))(optNumEnvStrict("SSH_PORT")),
|
|
1191
1254
|
...(process.env.SSH_MOUNT_PATH ? { mountPath: process.env.SSH_MOUNT_PATH } : {}),
|
|
1255
|
+
...((hk) => (hk !== undefined ? { hostKey: hk } : {}))(parseSshHostKeyEnv()),
|
|
1192
1256
|
}
|
|
1193
1257
|
:
|
|
1194
1258
|
process.env.REMOTE_EXEC === "adb" && process.env.ADB_SERIAL
|
|
@@ -1244,6 +1308,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1244
1308
|
experimentalObserverAgents: boolEnv("EXPERIMENTAL_OBSERVER_AGENTS", false),
|
|
1245
1309
|
schedulerEnabled: postureOn("SCHEDULER_ENABLED"),
|
|
1246
1310
|
planModeEnabled: boolEnv("PLAN_MODE_ENABLED", true),
|
|
1311
|
+
...((n) => (n !== undefined ? { hooksTimeoutMs: n } : {}))(parseHooksTimeoutEnv()),
|
|
1247
1312
|
...(process.env.SCHEDULER_STORE_PATH ? { schedulerStorePath: process.env.SCHEDULER_STORE_PATH } : {}),
|
|
1248
1313
|
schedulerSessionWakeup: boolEnv("SCHEDULER_SESSION_WAKEUP", true),
|
|
1249
1314
|
schedulerSessionLifetime: boolEnv("SCHEDULER_SESSION_LIFETIME", true),
|
|
@@ -1514,7 +1579,7 @@ const AUTH_GROUP_KEYS = [
|
|
|
1514
1579
|
const ORCHESTRATION_GROUP_KEYS = [
|
|
1515
1580
|
"remoteExec", "worktreeIsolation", "leaderEnabled", "leaderFanoutEnabled", "routerEnabled", "selfOrchestrationEnabled",
|
|
1516
1581
|
"selfOrchestrationModels", "selfOrchestrationWorkerIsolation", "forkEnabled", "experimentalObserverAgents",
|
|
1517
|
-
"schedulerEnabled", "schedulerSessionWakeup", "schedulerSessionLifetime", "schedulerStorePath", "planModeEnabled", "workflowRunStoreBackend",
|
|
1582
|
+
"schedulerEnabled", "schedulerSessionWakeup", "schedulerSessionLifetime", "schedulerStorePath", "planModeEnabled", "hooksTimeoutMs", "workflowRunStoreBackend",
|
|
1518
1583
|
"workflowOrphanGraceMs", "workflowJournalRetentionMs", "workflowRunRetentionMs", "usageWindows", "workflowAgentsReadOnly",
|
|
1519
1584
|
"workflowSizeGuideline", "backgroundAgentRetentionMs", "backgroundAgentStaleRunningMs",
|
|
1520
1585
|
"backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
|
package/dist/hooks/hook-llm.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { adjudicateModelRoute, normalizeBaseUrl, resolveRouteCredential, routeRefusalText } from "@sema-agent/core";
|
|
1
2
|
import { matchCatalogModel } from "../model-select.js";
|
|
2
3
|
export function createHookLlm(deps) {
|
|
3
4
|
const { config, metrics } = deps;
|
|
@@ -34,6 +35,25 @@ export function createHookLlm(deps) {
|
|
|
34
35
|
return shapeFrom(requested, config.model, requested);
|
|
35
36
|
return { ok: false, error: `hook model "${requested}" is not in this deployment's catalog` };
|
|
36
37
|
};
|
|
38
|
+
const judgeRoute = (pick, perModelKey, deploymentKey, declaredRoot) => {
|
|
39
|
+
const credential = resolveRouteCredential({
|
|
40
|
+
...(perModelKey !== undefined ? { optionsApiKey: perModelKey } : {}),
|
|
41
|
+
...(deploymentKey !== undefined ? { configApiKey: deploymentKey } : {}),
|
|
42
|
+
});
|
|
43
|
+
const verdict = adjudicateModelRoute({ id: pick.catalogRef, baseUrl: pick.baseUrl }, credential, { ...(declaredRoot !== undefined ? { baseUrl: declaredRoot } : {}) });
|
|
44
|
+
if (!verdict.ok) {
|
|
45
|
+
return { ok: false, error: `hook model "${pick.catalogRef}" cannot be routed — ${routeRefusalText(verdict)}` };
|
|
46
|
+
}
|
|
47
|
+
if ((verdict.posture === "unpinned" || verdict.posture === "keyless") && normalizeBaseUrl(pick.baseUrl) !== normalizeBaseUrl(declaredRoot)) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
error: `hook model "${pick.catalogRef}" points at ${pick.baseUrl}, which this deployment did not declare as its own endpoint, ` +
|
|
51
|
+
`and no per-model credential resolved — refusing to send the prompt there (it embeds user content; an unauthenticated ` +
|
|
52
|
+
`request still hands that content to whoever operates that URL)`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { ok: true, credential };
|
|
56
|
+
};
|
|
37
57
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
38
58
|
const hookLlm = async ({ prompt, model, timeoutMs, system, maxOutputTokens }) => {
|
|
39
59
|
const pick = hookModelFor(model);
|
|
@@ -52,30 +72,20 @@ export function createHookLlm(deps) {
|
|
|
52
72
|
return { ok: false, error: `per-model key resolution for "${pick.catalogRef}" failed — refusing to fall back to the shared route credential (${String(e)})` };
|
|
53
73
|
}
|
|
54
74
|
}
|
|
55
|
-
const normUrl = (u) => {
|
|
56
|
-
try {
|
|
57
|
-
const x = new URL(u);
|
|
58
|
-
return `${x.protocol}//${x.host}${x.pathname.replace(/\/+$/, "")}`;
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return u.replace(/\/+$/, "");
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
75
|
let url;
|
|
65
76
|
let headers;
|
|
66
77
|
let body;
|
|
67
78
|
if (pick.api === "anthropic-messages") {
|
|
68
79
|
const routeBase = anthropicRouteBaseUrl();
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
71
|
-
return
|
|
72
|
-
}
|
|
80
|
+
const routed = judgeRoute(pick, perModelKey, config.anthropic?.authToken ?? config.anthropic?.apiKey, routeBase);
|
|
81
|
+
if (!routed.ok)
|
|
82
|
+
return routed;
|
|
73
83
|
url = `${pick.baseUrl.replace(/\/+$/, "")}/v1/messages`;
|
|
74
84
|
headers = {
|
|
75
85
|
"content-type": "application/json",
|
|
76
86
|
"anthropic-version": config.anthropic?.version ?? "2023-06-01",
|
|
77
|
-
...(
|
|
78
|
-
? { "x-api-key":
|
|
87
|
+
...(routed.credential.source === "per-model"
|
|
88
|
+
? { "x-api-key": routed.credential.apiKey ?? "" }
|
|
79
89
|
: config.anthropic?.authToken
|
|
80
90
|
? { authorization: `Bearer ${config.anthropic.authToken}` }
|
|
81
91
|
: config.anthropic?.apiKey
|
|
@@ -87,11 +97,10 @@ export function createHookLlm(deps) {
|
|
|
87
97
|
body = JSON.stringify({ model: pick.providerModelId, max_tokens: maxTokens, ...(system ? { system } : {}), messages: [{ role: "user", content: prompt }] });
|
|
88
98
|
}
|
|
89
99
|
else {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
100
|
+
const routed = judgeRoute(pick, perModelKey, config.gatewayApiKey, config.gatewayBaseUrl);
|
|
101
|
+
if (!routed.ok)
|
|
102
|
+
return routed;
|
|
103
|
+
const hookKey = routed.credential.apiKey;
|
|
95
104
|
url = `${pick.baseUrl.replace(/\/+$/, "")}/chat/completions`;
|
|
96
105
|
headers = { "content-type": "application/json", ...(hookKey ? { authorization: `Bearer ${hookKey}` } : {}) };
|
|
97
106
|
body = JSON.stringify({ model: pick.providerModelId, ...(maxOutputTokens !== undefined ? { max_tokens: maxOutputTokens } : {}), messages: [...(system ? [{ role: "system", content: system }] : []), { role: "user", content: prompt }], stream: false });
|
|
@@ -1035,6 +1035,9 @@ export function composeHooks(deployment, task) {
|
|
|
1035
1035
|
const d = deployment;
|
|
1036
1036
|
const t = task;
|
|
1037
1037
|
const out = {};
|
|
1038
|
+
const timeoutMs = d.timeoutMs ?? t.timeoutMs;
|
|
1039
|
+
if (timeoutMs !== undefined)
|
|
1040
|
+
out.timeoutMs = timeoutMs;
|
|
1038
1041
|
if (d.preToolUse || t.preToolUse) {
|
|
1039
1042
|
const bothObservational = (d.preToolUse === undefined || d.preToolUseObservational === true) &&
|
|
1040
1043
|
(t.preToolUse === undefined || t.preToolUseObservational === true);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { DEFAULT_EFFORT_LEVELS, expandTiers } from "@sema-agent/core";
|
|
1
|
+
import { DEFAULT_EFFORT_LEVELS, expandTiers, routePairingStatus } from "@sema-agent/core";
|
|
2
2
|
import { isModelAllowlisted } from "../../model-select.js";
|
|
3
|
+
import { resolveModelApiKey } from "../../key-resolver.js";
|
|
4
|
+
import { isSealedKeyPoison } from "../../sealed-key.js";
|
|
3
5
|
import { cwdHonored } from "../../task-cwd.js";
|
|
4
6
|
import { a2aInjectionHonored } from "../../task-a2a.js";
|
|
5
7
|
import { mcpInjectionHonored } from "../../task-mcp.js";
|
|
@@ -8,6 +10,12 @@ import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
|
8
10
|
import { projectMemoryEngineCapability } from "../../memory-posture.js";
|
|
9
11
|
import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
|
|
10
12
|
import { gatedPrincipal } from "../principal-gate.js";
|
|
13
|
+
function routePairingOf(deps, name, m) {
|
|
14
|
+
if (isSealedKeyPoison(deps.config.modelApiKeys?.[name]))
|
|
15
|
+
return "unknown";
|
|
16
|
+
const key = resolveModelApiKey(name, deps.config.modelApiKeyEnv ?? {}, process.env, deps.config.modelApiKeys ?? {});
|
|
17
|
+
return routePairingStatus(deps.routeJudgeBrain ?? {}, m, key !== undefined ? { apiKey: key } : undefined);
|
|
18
|
+
}
|
|
11
19
|
function retentionAdvertisement(config) {
|
|
12
20
|
const sweep = config.retentionSweep;
|
|
13
21
|
const policy = config.retentionPolicy;
|
|
@@ -146,6 +154,7 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
146
154
|
...(m.maxTokens ? { maxOutputTokens: m.maxTokens } : {}),
|
|
147
155
|
...(m.reasoning ? { supportedEffortLevels: [...DEFAULT_EFFORT_LEVELS] } : {}),
|
|
148
156
|
...(atAllowlist.length > 0 ? { atMentionable: isModelAllowlisted(name, atCatalog, atAllowlist) } : {}),
|
|
157
|
+
routePairing: routePairingOf(deps, name, m),
|
|
149
158
|
}));
|
|
150
159
|
const nonAlias = Object.entries(deps.config.models).filter(([n]) => n !== "default");
|
|
151
160
|
const defaultName = (nonAlias.find(([, m]) => m === deps.config.model) ?? nonAlias.find(([, m]) => m.id === deps.config.model.id))?.[0];
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -1069,6 +1069,10 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1069
1069
|
return;
|
|
1070
1070
|
}
|
|
1071
1071
|
const { status, body: respBody } = await deps.toolApproval.respond(id, principal, body, req);
|
|
1072
|
+
const retryAfterSec = respBody.retryAfterSec;
|
|
1073
|
+
if (status === 429 && typeof retryAfterSec === "number" && Number.isFinite(retryAfterSec) && retryAfterSec >= 0) {
|
|
1074
|
+
res.setHeader("retry-after", String(Math.ceil(retryAfterSec)));
|
|
1075
|
+
}
|
|
1072
1076
|
sendJson(res, status, respBody);
|
|
1073
1077
|
return;
|
|
1074
1078
|
}
|
package/dist/http/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import type { IncomingMessage } from "node:http";
|
|
3
|
-
import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type SideQuerySpec, type SideQueryResult } from "@sema-agent/core";
|
|
3
|
+
import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type SideQuerySpec, type SideQueryResult, type Brain } from "@sema-agent/core";
|
|
4
4
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
5
5
|
import type { ServiceConfig } from "../config-types.js";
|
|
6
6
|
import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
|
|
@@ -293,6 +293,17 @@ export interface ServiceSeamDeps {
|
|
|
293
293
|
* backend). Enables GET /v1/memory/export — present ONLY on a DB memory plane (MEMORY_ENGINE_BACKEND=
|
|
294
294
|
* pg|tidb); absent / file posture ⇒ 501 (single-user file memory exports by copying the memory dir). */
|
|
295
295
|
memoryExport?: (scope: string) => Promise<MemoryEntry[]>;
|
|
296
|
+
/**
|
|
297
|
+
* #345(core 5.57.0 `routePairingStatus`)—— **key↔URL 配对读面**:`GET /v1/models` 每行的
|
|
298
|
+
* `routePairing` 标注就是拿这一只判的。main.ts 接的是**装配好的那只 brain 本身**(不是摘出来的
|
|
299
|
+
* 函数引用:A-057.2 那条「裸摘引用丢接收者」的教训同款,且 core 的装饰器族是按 `stream` 的同一条
|
|
300
|
+
* 路再分派 `adjudicateRoute` 的,整只传才不会绕过路由/快切层)。
|
|
301
|
+
*
|
|
302
|
+
* 🔴 缺席 ⇒ 该面每行标 `"unknown"`,**绝不猜**:一只没有 `adjudicateRoute` 面的自定义 brain 是
|
|
303
|
+
* core 明令「callers must treat that as 'no judgment', never as OK or as broken」的形。测试夹具
|
|
304
|
+
* 不接这一件时读面字节逐字不变(标注仍在,值恒 `"unknown"`)。
|
|
305
|
+
*/
|
|
306
|
+
routeJudgeBrain?: Pick<Brain, "adjudicateRoute">;
|
|
296
307
|
/** 142-S2.5(设计 §2.5;裁定 plan 随数据走,core 零运行时半场):one full sync round for one
|
|
297
308
|
* (scope, peer) — main.ts wires `performMemorySync` over the DB memory backend + the sync-cursor
|
|
298
309
|
* store. Enables POST /v1/memory/sync/:scope — present ONLY on a DB memory plane
|
package/dist/http/server.js
CHANGED
|
@@ -1277,7 +1277,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1277
1277
|
await appendPromptManifest(ap, deps.promptManifests, taskId);
|
|
1278
1278
|
if (reSuspended || vrReopened) {
|
|
1279
1279
|
await appendModelUsageDelta(ap, deps.modelUsage, taskId);
|
|
1280
|
-
const vrParkCallId = reSuspended ? await parkToolCallId(vr, deps.checkpointStore) : undefined;
|
|
1280
|
+
const vrParkCallId = reSuspended ? await parkToolCallId({ ...vr, status: reParkStatus }, deps.checkpointStore) : undefined;
|
|
1281
1281
|
await ap(reSuspended ? reParkStatus : "suspended", { gate: vr.checkpointGate ?? null, ...(vrParkCallId !== undefined ? { toolCallId: vrParkCallId } : {}), ...(vrReopened ? { reopened: vr.errorCode ?? null } : {}) });
|
|
1282
1282
|
}
|
|
1283
1283
|
else {
|
|
@@ -1532,9 +1532,12 @@ export function createHttpServer(rawDeps) {
|
|
|
1532
1532
|
...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}),
|
|
1533
1533
|
}, driveResume)
|
|
1534
1534
|
: driveResume();
|
|
1535
|
-
|
|
1535
|
+
const withElicitation = () => deps.elicitation && taskId
|
|
1536
1536
|
? deps.elicitation.runWithContext({ taskId, owner: principal ?? null, emit: (frame) => { const { type, ...rest } = frame; return append(type, rest); }, abortSignal: cancelCtrl.signal }, withApproval)
|
|
1537
|
-
: withApproval()
|
|
1537
|
+
: withApproval();
|
|
1538
|
+
return await (deps.sendUserFile && taskId
|
|
1539
|
+
? deps.sendUserFile.runWithContext({ taskId, emit: async (frame) => { const { type, ...rest } = frame; await append(type, rest); } }, withElicitation)
|
|
1540
|
+
: withElicitation());
|
|
1538
1541
|
});
|
|
1539
1542
|
}
|
|
1540
1543
|
catch (e) {
|
package/dist/leader/merge.js
CHANGED
|
@@ -61,7 +61,9 @@ export async function mergeBranches(reports, baseSha, deps) {
|
|
|
61
61
|
deps.logger?.warn?.("integration_repair_error", { round: round + 1, err: e instanceof Error ? e.message : String(e) });
|
|
62
62
|
break;
|
|
63
63
|
}
|
|
64
|
+
repairMeta = { rounds: round + 1, fixed: false, costUsd: cost };
|
|
64
65
|
test = await env.exec(deps.testCmd, { cwd: repo });
|
|
66
|
+
repairMeta = { rounds: round + 1, fixed: test.exitCode === 0, costUsd: cost };
|
|
65
67
|
}
|
|
66
68
|
repairMeta = { rounds: round, fixed: test.exitCode === 0, costUsd: cost };
|
|
67
69
|
deps.logger?.info?.("integration_repair_done", { ...repairMeta });
|
|
@@ -88,7 +90,7 @@ export async function mergeBranches(reports, baseSha, deps) {
|
|
|
88
90
|
if (!integrated.stdout.trim()) {
|
|
89
91
|
return { ok: false, phase: "push", reason: "integrated patch is empty (nothing applied over base) — refusing to push", quarantine: mergeable.map((r) => r.workerId), ...costTail };
|
|
90
92
|
}
|
|
91
|
-
const evald = await evaluatePushHold(
|
|
93
|
+
const evald = await evaluatePushHold(mergeable, measure, integrated.stdout, deps);
|
|
92
94
|
if (evald.measure !== undefined)
|
|
93
95
|
measure = evald.measure;
|
|
94
96
|
if (evald.hold) {
|
|
@@ -126,7 +128,7 @@ export async function mergeBranches(reports, baseSha, deps) {
|
|
|
126
128
|
await destroy().catch(() => { });
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
|
-
async function evaluatePushHold(
|
|
131
|
+
async function evaluatePushHold(mergeable, observeMeasure, integratedPatch, deps) {
|
|
130
132
|
const heldCandidate = () => delimitUntrusted("CANDIDATE PATCH (untrusted worker code)", integratedPatch);
|
|
131
133
|
const carried = mergeable.map((r) => r.repairTerminal).filter((t) => t !== undefined && t !== "fixed");
|
|
132
134
|
if (carried.length > 0) {
|
package/dist/main.js
CHANGED
|
@@ -136,7 +136,10 @@ async function main() {
|
|
|
136
136
|
const toolTracer = config.toolTrace ? createToolTracer(logger) : undefined;
|
|
137
137
|
if (toolTracer)
|
|
138
138
|
logger.info("tool_trace_enabled", {});
|
|
139
|
-
const deploymentHooks =
|
|
139
|
+
const deploymentHooks = {
|
|
140
|
+
...(toolTracer ? composeHooks(createPermissionDeniedMeter(metrics), toolTracer) : createPermissionDeniedMeter(metrics)),
|
|
141
|
+
...(config.hooksTimeoutMs !== undefined ? { timeoutMs: config.hooksTimeoutMs } : {}),
|
|
142
|
+
};
|
|
140
143
|
const { sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion, workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink, workflowRecoverOpts, workflowAgentRegistry, subagentSteerRegistry, } = createWorkflowOrchestration({ config, logger, metrics, localRoot, backend, getRunStore: () => runStore });
|
|
141
144
|
let parkedAskRedeem;
|
|
142
145
|
const { elicitation, question, toolApproval, durableEnabled, streamApprovalGate, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({
|
|
@@ -533,6 +536,7 @@ async function main() {
|
|
|
533
536
|
};
|
|
534
537
|
const seams = {
|
|
535
538
|
sessionAudit,
|
|
539
|
+
routeJudgeBrain: brain,
|
|
536
540
|
purgeSession: purgeSession ? purgeSession : undefined,
|
|
537
541
|
subagentTaskOutput: (handle, access) => backgroundAgentOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
|
|
538
542
|
taskHandleOutput: (handle, access) => taskHandleOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
|
|
@@ -723,7 +727,7 @@ async function main() {
|
|
|
723
727
|
else {
|
|
724
728
|
resolveFleetRecovery();
|
|
725
729
|
}
|
|
726
|
-
installShutdownHandlers({
|
|
730
|
+
void installShutdownHandlers({
|
|
727
731
|
config, logger, server, reaper, fleetReconcile, otelExporter, breakerState, costQuota, rateLimiter,
|
|
728
732
|
version: serviceVersion(),
|
|
729
733
|
runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState,
|
package/dist/memory-sync.js
CHANGED
|
@@ -154,15 +154,28 @@ export function parseMemorySyncRequest(body, scope) {
|
|
|
154
154
|
function baselineStub(id, rev, scope, slug) {
|
|
155
155
|
return { id, rev, scope, slug: slug ?? `sync-baseline-stub/${id}`, frontmatter: {}, body: "" };
|
|
156
156
|
}
|
|
157
|
+
function malformedEntryReason(err) {
|
|
158
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
159
|
+
return `malformed_entry: core cannot read this entry's frontmatter shape (${msg.slice(0, 200)}) — the entry is refused and never stored; fix the frontmatter (a present \`distilled\` block needs planId/at/carrierRev/inputs[]) and resend`;
|
|
160
|
+
}
|
|
157
161
|
export async function performMemorySync(backend, cursors, scope, req, now = Date.now) {
|
|
158
162
|
const conflicts = [];
|
|
159
163
|
const rejected = new Map();
|
|
164
|
+
const malformed = new Set();
|
|
160
165
|
for (const e of req.entries) {
|
|
161
|
-
|
|
166
|
+
let computed;
|
|
167
|
+
try {
|
|
168
|
+
computed = computeEntryRev(e);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
malformed.add(e.id);
|
|
172
|
+
rejected.set(e.id, malformedEntryReason(err));
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
162
175
|
if (computed !== e.rev)
|
|
163
176
|
rejected.set(e.id, `rev_mismatch: claimed ${JSON.stringify(e.rev)} but the content hashes to ${JSON.stringify(computed)} (rev is core's computeEntryRev content-hash contract)`);
|
|
164
177
|
}
|
|
165
|
-
for (const f of screenInboundEntries(req.entries)) {
|
|
178
|
+
for (const f of screenInboundEntries(malformed.size === 0 ? req.entries : req.entries.filter((e) => !malformed.has(e.id)))) {
|
|
166
179
|
const gate = `inbound_gate: ${f.findings.map((x) => `${x.code}: ${x.reason}`).join("; ")}`;
|
|
167
180
|
const prior = rejected.get(f.id);
|
|
168
181
|
rejected.set(f.id, prior !== undefined ? `${prior}; ${gate}` : gate);
|
|
@@ -121,9 +121,17 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
121
121
|
readonly cls: "F";
|
|
122
122
|
readonly note: "[4913]:durable park 投影腿在 strip 之前拿结果里的 checkpointToken 换 `pendingAction.toolCallId`(cs.get 一次读),那次读**抛了**(store 抖动 / 滚动升级期 checkpoint 格式 version guard / 行已被并发消费)⇒ 本次 park 的 wire 投影**缺 `toolCallId` 键**,其余键(gate/checkpointId)照旧。放行的最坏后果 = 消费方(cli 判别子 v3 写者②)退回该键到货前的行为——同族多兄弟 durable park 分不出主角、诚实全不标(cli 侧 S3 局限,成文的旧现状),纯展示/关联面,不参与任何门/CAS/resume 判定,故 F 类。绝不挡终局路径是承重的:park 投影点全在 done/suspended 事件写链上,让它抛会把一次 store 抖动升级成整条 run 终局写失败。必须留痕:键缺席与「tool-less park 本就无键」([1995]② OMITTED 契约)在 wire 上同形,不计数则「投影为什么总缺」在遥测里永不显形。";
|
|
123
123
|
};
|
|
124
|
-
readonly "server.model-key.
|
|
124
|
+
readonly "server.model-key.degrade-hop-borrows-gateway-key": {
|
|
125
125
|
readonly cls: "P-DEBT";
|
|
126
|
-
readonly note: "
|
|
126
|
+
readonly note: "[4998] BROKEN 实锤的**留痕半场**(#345 codex 对抗复审 R1-[high] 的可采纳部分,亲验落实)。`brain.ts` 的反应式降级 hop 在降级目标**没有自己的凭据**时回落主网关 key(`own ?? config.gatewayApiKey`),而那只 fallback brain 是用 `fallbackModel.baseUrl` 当 `config.baseUrl` 造的 —— 于是 core 5.57.0 的配对裁决器看到的是「部署凭据 + 它自己的声明根」⇒ 判 `paired` ⇒ 照发。🔴 **上游的法在这条路上结构性看不见问题**:不是 core 漏判,是**本仓把一个撒谎的 config 递给了它**(把主网关的 key 与降级目标的根说成一对)。真读数:`test/brain.test.ts` 的「既有腿不回归」格实测 `Bearer primary-gateway-key` 打到 `http://degrade-gw.test/v1/chat/completions`。为什么记债而不是本批修:正解要么是「off-root 且无自 key ⇒ 降级链按不可用后退」(改的是**可用性**轴,会让一类既有部署的降级当场失效),要么是「把目标自己的凭据改走 per-model 位(`Model.headers`)让 core 判得见」(改核心装配 + 既有钉字节),两者都是独立设计件、不是提货批的尾巴。判据 = 解不出目标自己的 key ∧ 目标 baseUrl 既不是 `gatewayBaseUrl` 也不在 `gatewayFallbackUrls`(后者按同文件成文契约 = 同一逻辑网关/同一 key 有效域,不算换家),归一用 core 的 `normalizeBaseUrl`(与本仓其它配对判定同一只)。⚠️ 与已撤线的 `off-route-model-uses-gateway-key` **不是同一条路**:那条的铸点是 side-query/WebFetch 的 per-model auth 座,而座位恒以**被请求的那只**模型被调用、位置在装饰器栈**之外** —— 它从来没有、也不可能观察到降级 hop 换上的 `fallbackModel`。所以这不是「撤早了」,是一条**一直没人看**的姊妹路。";
|
|
127
|
+
};
|
|
128
|
+
readonly "server.exec-lane.ssh-host-key-unverified": {
|
|
129
|
+
readonly cls: "P-DEBT";
|
|
130
|
+
readonly note: "#296(A-052.2,clay 裁定 C-R21):`REMOTE_EXEC=ssh` 且**两只主机密钥旋钮都缺席**(`SSH_HOST_FINGERPRINT` / `SSH_KNOWN_HOSTS`)⇒ 本次连接接受**任何**主机密钥,等价 `ssh -o StrictHostKeyChecking=no`。放行的最坏后果:一次中间人拿到这条会话的**全部命令、全部产物、以及 agent 在那台机器上的全部动作**(私钥仍在控制面手里、拿不走,design/61 §5 #2)。⇒ 保护型欠账,不是体验回退。**方向是裁定的、不是遗漏**:本 lane 的既有部署形是「AI 编排批量部署到一批 SSH 可达的机器」,那些机器的指纹通常不在任何 known_hosts 里,默认拒连 = 一次静默的可用性断裂;C-R21 裁「在场即严格,缺席即响亮」。补偿三件:①**每连接**一条 `ssh_host_key_unverified` warn(不是每进程一次 —— 一条长期裸连的 lane 必须在每条连接上都看得见);②本计数(遥测里「有多少条连接是裸连」是一个可观测的数,曲线应当只降不升);③两旋钮任一在场即**严格校验、不符拒连**(拒因带两半指纹 + `ssh-keyscan` 指路)。终局 = 部署把旋钮配上(那时本计数归零);旋钮不配而想收紧的部署级开关(「无旋钮即拒启」)是下一件,不在 C-R21 射程。";
|
|
131
|
+
};
|
|
132
|
+
readonly "server.approval.park-tombstone-global-cap-refused": {
|
|
133
|
+
readonly cls: "F";
|
|
134
|
+
readonly note: "#343(黑板 [5100] Q3):park 墓碑表(#329 迟到回决的 wire approvalId → 店内坐标指路条)**全局帽已满、而本 principal 还没到自己的 per-principal 子帽** ⇒ 这条新墓碑**不落**(拒新,一格都不挤别人)。放行的最坏后果 = 这一只 ask 的 **live 腿指路便利**缺席,壳按 Yes 拿到的是 404 `unknown_or_other_replica` —— 也就是 #329 之前的那一格逐字行为,既不会错兑也不会双兑;**完整兑付面一个字节没动**(park 行本体在 durable 店未失,`GET /v1/approvals` 列表 + `/v1/approvals/:sessionId/decide` 仍可回决),故 F 类而不是保护型。为什么方向是「拒新」而不是修前的「FIFO 挤最旧」:那个形让**甲的用量决定乙的可用性**(已鉴权租户量产 park 终局 ask 即可把别家墓碑全部挤出,merge-rescan 744 confirmed),而 park 是常态路由、不需要任何异常手法。必须留痕:「表被别人打满」与「这张卡本来就没有墓碑」在 wire 上完全同形,不计数则运维只能靠「用户说按 Yes 老是 404」这种间接症状发现,而那正是 [4845] 排障代价实证过的那条路。detail 带 owner(判「压力来自谁」的第一手线索)。本 tag 有**两条臂、两个根因**,detail 判别:(a) 全局帽满且本 principal 未超子帽 ⇒ 拒新(detail 带 owner);(b) 本 principal 超子帽但名下条目**全在飞**、无一可逐 ⇒ 拒新(detail 带 `reason=all-inflight`)。超子帽且**有**可逐条目的常态臂逐自己最旧的一条(量产者自伤,设计内有界表纪律)才不走本 tag。";
|
|
127
135
|
};
|
|
128
136
|
};
|
|
129
137
|
/** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
|
|
@@ -112,9 +112,17 @@ export const FAIL_OPEN_TAGS = {
|
|
|
112
112
|
cls: "F",
|
|
113
113
|
note: "[4913]:durable park 投影腿在 strip 之前拿结果里的 checkpointToken 换 `pendingAction.toolCallId`(cs.get 一次读),那次读**抛了**(store 抖动 / 滚动升级期 checkpoint 格式 version guard / 行已被并发消费)⇒ 本次 park 的 wire 投影**缺 `toolCallId` 键**,其余键(gate/checkpointId)照旧。放行的最坏后果 = 消费方(cli 判别子 v3 写者②)退回该键到货前的行为——同族多兄弟 durable park 分不出主角、诚实全不标(cli 侧 S3 局限,成文的旧现状),纯展示/关联面,不参与任何门/CAS/resume 判定,故 F 类。绝不挡终局路径是承重的:park 投影点全在 done/suspended 事件写链上,让它抛会把一次 store 抖动升级成整条 run 终局写失败。必须留痕:键缺席与「tool-less park 本就无键」([1995]② OMITTED 契约)在 wire 上同形,不计数则「投影为什么总缺」在遥测里永不显形。",
|
|
114
114
|
},
|
|
115
|
-
"server.model-key.
|
|
115
|
+
"server.model-key.degrade-hop-borrows-gateway-key": {
|
|
116
116
|
cls: "P-DEBT",
|
|
117
|
-
note: "
|
|
117
|
+
note: "[4998] BROKEN 实锤的**留痕半场**(#345 codex 对抗复审 R1-[high] 的可采纳部分,亲验落实)。`brain.ts` 的反应式降级 hop 在降级目标**没有自己的凭据**时回落主网关 key(`own ?? config.gatewayApiKey`),而那只 fallback brain 是用 `fallbackModel.baseUrl` 当 `config.baseUrl` 造的 —— 于是 core 5.57.0 的配对裁决器看到的是「部署凭据 + 它自己的声明根」⇒ 判 `paired` ⇒ 照发。🔴 **上游的法在这条路上结构性看不见问题**:不是 core 漏判,是**本仓把一个撒谎的 config 递给了它**(把主网关的 key 与降级目标的根说成一对)。真读数:`test/brain.test.ts` 的「既有腿不回归」格实测 `Bearer primary-gateway-key` 打到 `http://degrade-gw.test/v1/chat/completions`。为什么记债而不是本批修:正解要么是「off-root 且无自 key ⇒ 降级链按不可用后退」(改的是**可用性**轴,会让一类既有部署的降级当场失效),要么是「把目标自己的凭据改走 per-model 位(`Model.headers`)让 core 判得见」(改核心装配 + 既有钉字节),两者都是独立设计件、不是提货批的尾巴。判据 = 解不出目标自己的 key ∧ 目标 baseUrl 既不是 `gatewayBaseUrl` 也不在 `gatewayFallbackUrls`(后者按同文件成文契约 = 同一逻辑网关/同一 key 有效域,不算换家),归一用 core 的 `normalizeBaseUrl`(与本仓其它配对判定同一只)。⚠️ 与已撤线的 `off-route-model-uses-gateway-key` **不是同一条路**:那条的铸点是 side-query/WebFetch 的 per-model auth 座,而座位恒以**被请求的那只**模型被调用、位置在装饰器栈**之外** —— 它从来没有、也不可能观察到降级 hop 换上的 `fallbackModel`。所以这不是「撤早了」,是一条**一直没人看**的姊妹路。",
|
|
118
|
+
},
|
|
119
|
+
"server.exec-lane.ssh-host-key-unverified": {
|
|
120
|
+
cls: "P-DEBT",
|
|
121
|
+
note: "#296(A-052.2,clay 裁定 C-R21):`REMOTE_EXEC=ssh` 且**两只主机密钥旋钮都缺席**(`SSH_HOST_FINGERPRINT` / `SSH_KNOWN_HOSTS`)⇒ 本次连接接受**任何**主机密钥,等价 `ssh -o StrictHostKeyChecking=no`。放行的最坏后果:一次中间人拿到这条会话的**全部命令、全部产物、以及 agent 在那台机器上的全部动作**(私钥仍在控制面手里、拿不走,design/61 §5 #2)。⇒ 保护型欠账,不是体验回退。**方向是裁定的、不是遗漏**:本 lane 的既有部署形是「AI 编排批量部署到一批 SSH 可达的机器」,那些机器的指纹通常不在任何 known_hosts 里,默认拒连 = 一次静默的可用性断裂;C-R21 裁「在场即严格,缺席即响亮」。补偿三件:①**每连接**一条 `ssh_host_key_unverified` warn(不是每进程一次 —— 一条长期裸连的 lane 必须在每条连接上都看得见);②本计数(遥测里「有多少条连接是裸连」是一个可观测的数,曲线应当只降不升);③两旋钮任一在场即**严格校验、不符拒连**(拒因带两半指纹 + `ssh-keyscan` 指路)。终局 = 部署把旋钮配上(那时本计数归零);旋钮不配而想收紧的部署级开关(「无旋钮即拒启」)是下一件,不在 C-R21 射程。",
|
|
122
|
+
},
|
|
123
|
+
"server.approval.park-tombstone-global-cap-refused": {
|
|
124
|
+
cls: "F",
|
|
125
|
+
note: "#343(黑板 [5100] Q3):park 墓碑表(#329 迟到回决的 wire approvalId → 店内坐标指路条)**全局帽已满、而本 principal 还没到自己的 per-principal 子帽** ⇒ 这条新墓碑**不落**(拒新,一格都不挤别人)。放行的最坏后果 = 这一只 ask 的 **live 腿指路便利**缺席,壳按 Yes 拿到的是 404 `unknown_or_other_replica` —— 也就是 #329 之前的那一格逐字行为,既不会错兑也不会双兑;**完整兑付面一个字节没动**(park 行本体在 durable 店未失,`GET /v1/approvals` 列表 + `/v1/approvals/:sessionId/decide` 仍可回决),故 F 类而不是保护型。为什么方向是「拒新」而不是修前的「FIFO 挤最旧」:那个形让**甲的用量决定乙的可用性**(已鉴权租户量产 park 终局 ask 即可把别家墓碑全部挤出,merge-rescan 744 confirmed),而 park 是常态路由、不需要任何异常手法。必须留痕:「表被别人打满」与「这张卡本来就没有墓碑」在 wire 上完全同形,不计数则运维只能靠「用户说按 Yes 老是 404」这种间接症状发现,而那正是 [4845] 排障代价实证过的那条路。detail 带 owner(判「压力来自谁」的第一手线索)。本 tag 有**两条臂、两个根因**,detail 判别:(a) 全局帽满且本 principal 未超子帽 ⇒ 拒新(detail 带 owner);(b) 本 principal 超子帽但名下条目**全在飞**、无一可逐 ⇒ 拒新(detail 带 `reason=all-inflight`)。超子帽且**有**可逐条目的常态臂逐自己最旧的一条(量产者自伤,设计内有界表纪律)才不走本 tag。",
|
|
118
126
|
},
|
|
119
127
|
};
|
|
120
128
|
export function failOpenTagForDroppedFrame(frameType) {
|
|
@@ -62,6 +62,35 @@ export interface HardenedVmLimits {
|
|
|
62
62
|
* A script exceeding it OOMs the WORKER (killed), never the shared main process. Default 128. */
|
|
63
63
|
maxHeapMb?: number;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* The realm test itself, exported so there is exactly ONE ruler for "did this rejection come from an untrusted
|
|
67
|
+
* script?" in the process. A promise minted INSIDE a vm context fails `instanceof` against the HOST `Promise`
|
|
68
|
+
* (different intrinsic per realm) — that is the whole judgement.
|
|
69
|
+
*
|
|
70
|
+
* 🔴 Why it is exported (A-070 二轮重扫, lifecycle-pairing, double-opus CONFIRMED): the guard below is NOT the
|
|
71
|
+
* only `unhandledRejection` listener in a real boot. `src/boot/shutdown.ts` installs one at boot (#320① crash
|
|
72
|
+
* last words) that exits(1); Node walks EVERY listener for the event, so containment cannot be expressed by
|
|
73
|
+
* one listener returning early — whoever exits wins regardless of registration order. The containment DECISION
|
|
74
|
+
* therefore lives in the boot handler, and it must use this same ruler rather than minting a second one.
|
|
75
|
+
*/
|
|
76
|
+
export declare function isScriptRealmRejection(promise: unknown): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Summarize an unhandled rejection reason for a log line WITHOUT running anything the (untrusted) script
|
|
79
|
+
* controls. Both rejection listeners in this process use it.
|
|
80
|
+
*
|
|
81
|
+
* 🔴 Why not `String(reason)` / `${reason}` / `reason.message` (codex adversarial-review F1, REPRODUCED):
|
|
82
|
+
* the reason of a script-realm rejection is a LIVE object from the vm context, and every readout path is
|
|
83
|
+
* script-controlled code running in the HOST turn with no vm timeout over it:
|
|
84
|
+
* · `String(x)` / template literals invoke `Symbol.toPrimitive` / `toString` / `valueOf`;
|
|
85
|
+
* · a property read invokes a getter, and on a Proxy even `getOwnPropertyDescriptor` is a trap.
|
|
86
|
+
* `Promise.reject({ toString() { throw ... } })` therefore threw OUT of the guard, became an
|
|
87
|
+
* `uncaughtException` and exited(1) — i.e. an untrusted script could escape containment and kill the process
|
|
88
|
+
* by choosing its rejection value; the loop variant (`toString(){ for(;;){} }`) wedges the event loop instead.
|
|
89
|
+
* So: PRIMITIVES only (their coercion cannot run user code), everything else gets a fixed placeholder. The
|
|
90
|
+
* cost is honest and stated — an operator sees "a script rejection was contained", not its message; the
|
|
91
|
+
* message channel for a rejection nobody awaited does not exist anyway.
|
|
92
|
+
*/
|
|
93
|
+
export declare function describeRejectionReason(reason: unknown): string;
|
|
65
94
|
export declare function installScriptRealmRejectionGuard(): void;
|
|
66
95
|
/**
|
|
67
96
|
* The membrane handed to a surface's `buildGlobals` callback to construct the CONTEXT-LOCAL bindings the
|