@sema-agent/server 7.121.0 → 7.122.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 +98 -0
- package/README.md +1 -1
- package/README.zh-CN.md +3 -1
- package/USAGE.md +35 -14
- package/dist/approval-reconciler.d.ts +8 -0
- package/dist/approval-reconciler.js +72 -52
- package/dist/auto-mode-face.d.ts +9 -2
- package/dist/auto-mode-face.js +5 -3
- package/dist/bench/s1/live-deps.js +1 -0
- package/dist/boot/leader.js +1 -0
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +2 -1
- package/dist/boot/resolve-spec.js +1 -4
- package/dist/boot/shutdown.js +6 -1
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/capabilities/memory-notice.d.ts +1 -1
- package/dist/config-catalog.js +3 -3
- package/dist/config-center/center-request.d.ts +6 -9
- package/dist/config-center/center-request.js +11 -27
- package/dist/config-types.d.ts +11 -8
- package/dist/config-types.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +23 -35
- package/dist/fleet/drain-entry.d.ts +28 -3
- package/dist/fleet/drain-entry.js +3 -0
- package/dist/fleet/serving-lease.js +13 -15
- package/dist/governance-overrides.d.ts +7 -2
- package/dist/http/admission.js +7 -5
- package/dist/http/approval-sweeps.js +2 -2
- package/dist/http/checkpoint-ctx.d.ts +60 -11
- package/dist/http/checkpoint-ctx.js +23 -9
- package/dist/http/dispatch.js +17 -5
- package/dist/http/resume-legs.js +8 -8
- package/dist/http/route-ctx.d.ts +19 -3
- package/dist/http/route-table.d.ts +3 -0
- package/dist/http/route-table.js +4 -0
- package/dist/http/routes/admin-drain.d.ts +5 -2
- package/dist/http/routes/admin-drain.js +14 -2
- package/dist/http/routes/approvals-assistant.d.ts +1 -0
- package/dist/http/routes/approvals-assistant.js +6 -6
- package/dist/http/routes/capabilities.js +5 -4
- package/dist/http/routes/fleet.d.ts +1 -0
- package/dist/http/routes/fleet.js +1 -1
- package/dist/http/routes/images.d.ts +1 -0
- package/dist/http/routes/images.js +1 -1
- package/dist/http/routes/leader.js +6 -0
- package/dist/http/routes/runs.d.ts +2 -0
- package/dist/http/routes/runs.js +5 -4
- package/dist/http/routes/sessions.d.ts +1 -0
- package/dist/http/routes/sessions.js +1 -1
- package/dist/http/routes/trace-usage.d.ts +1 -0
- package/dist/http/routes/trace-usage.js +1 -1
- package/dist/http/routes/workflows.d.ts +1 -0
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/send.d.ts +13 -0
- package/dist/http/send.js +9 -0
- package/dist/http/server.d.ts +3 -0
- package/dist/http/stored-body-replay.d.ts +27 -0
- package/dist/http/stored-body-replay.js +23 -0
- package/dist/leader/leader.d.ts +22 -1
- package/dist/leader/leader.js +4 -1
- package/dist/leader/repair-wire.d.ts +1 -1
- package/dist/leader/repair-wire.js +2 -2
- package/dist/leader/run-entry.d.ts +27 -0
- package/dist/leader/run-entry.js +29 -0
- package/dist/leader/wire.d.ts +10 -0
- package/dist/leader/wire.js +15 -10
- package/dist/model-route-url.d.ts +13 -20
- package/dist/model-route-url.js +28 -24
- package/dist/observability/fail-open.d.ts +23 -6
- package/dist/observability/fail-open.js +8 -4
- package/dist/plugins/approval-ask-store-file.d.ts +1 -0
- package/dist/plugins/approval-ask-store-file.js +3 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
- package/dist/plugins/approval-ask-store-memory.js +6 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
- package/dist/plugins/approval-ask-store-sql.js +4 -0
- package/dist/plugins/one-time-migrations.js +13 -0
- package/dist/plugins/park-commit-watch.d.ts +19 -5
- package/dist/plugins/park-commit-watch.js +24 -17
- package/dist/router/route-orchestration.d.ts +4 -2
- package/dist/router/route-orchestration.js +2 -1
- package/dist/run-local.js +3 -1
- package/dist/runtime-governance.d.ts +44 -32
- package/dist/runtime-governance.js +23 -9
- package/dist/sighup-idle.d.ts +7 -1
- package/dist/sighup-idle.js +4 -4
- package/dist/task-settings.d.ts +37 -14
- package/dist/task-settings.js +20 -20
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +13 -1
- package/dist/trace/redact.d.ts +3 -3
- package/dist/unattended-permission-mode.d.ts +17 -13
- package/dist/unattended-permission-mode.js +3 -1
- package/dist/url-shape.d.ts +31 -0
- package/dist/url-shape.js +41 -0
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +1 -1
package/dist/boot/reapers.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { TaskAttachmentStore } from "../plugins/task-attachment-store.js";
|
|
|
24
24
|
import type { FileHistoryErasureFace } from "../file-history-capture.js";
|
|
25
25
|
import type { ApprovalNonceStore, CheckpointStoreFull, CostQuotaStore, ImageBake, RateLimiterStore, ServiceWorkflowJournalStore, StoreBackend, ToolResultStoreFull } from "../plugins/store-backend.js";
|
|
26
26
|
import type { ToolApprovalCoordinator } from "../tool-approval.js";
|
|
27
|
+
import type { ParkCommitWatch } from "../plugins/park-commit-watch.js";
|
|
27
28
|
/** [ref] / [ref]§五(sema-comms `audits/failopen-governance-176.md` §7 "Z 类 1 条"):default consecutive-failure
|
|
28
29
|
* count before a reaper leg's swallowed error gets a `warn` line. The ~15 `.catch(() => undefined)` sites
|
|
29
30
|
* below are correct in DIRECTION (a hiccuping periodic sweep must never crash the whole tick loop — one bad
|
|
@@ -68,6 +69,8 @@ export interface ReapersCtx {
|
|
|
68
69
|
subRunner: Runner;
|
|
69
70
|
runStore: ReturnType<StoreBackend["run"]> | undefined;
|
|
70
71
|
checkpointStore: CheckpointStoreFull | undefined;
|
|
72
|
+
/** S-1280:checkpoint 店的 park 提交观察层(stage-06 与店同一处铸;在场 ⇔ 有 checkpoint 店)。对账收敛器订阅它,在停车行提交那一拍收口 PARKING。 */
|
|
73
|
+
parkCommits: ParkCommitWatch | undefined;
|
|
71
74
|
rateLimiter: RateLimiter | RateLimiterStore | undefined;
|
|
72
75
|
costQuota: CostQuota | CostQuotaStore | undefined;
|
|
73
76
|
toolResultStore: ToolResultStoreFull | undefined;
|
package/dist/boot/reapers.js
CHANGED
|
@@ -56,7 +56,7 @@ export function createPlacementAwareReapSessions(deps) {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
export function startReapers(ctx) {
|
|
59
|
-
const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, approvalNonceStore, getRunDenySweep, userInputLedger, } = ctx;
|
|
59
|
+
const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, parkCommits, rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, approvalNonceStore, getRunDenySweep, userInputLedger, } = ctx;
|
|
60
60
|
const leg = (p) => {
|
|
61
61
|
if (p !== undefined)
|
|
62
62
|
trackMaintenanceLeg(p);
|
|
@@ -122,6 +122,7 @@ export function startReapers(ctx) {
|
|
|
122
122
|
orphanTtlMs: config.streamApproval.orphanTtlMs,
|
|
123
123
|
unattendedPolicy: config.unattendedApprovalPolicy,
|
|
124
124
|
...(toolApproval ? { emitRevoke: (frame, target) => void toolApproval.emitRevoke(frame, target) } : {}),
|
|
125
|
+
parkCommits,
|
|
125
126
|
});
|
|
126
127
|
})();
|
|
127
128
|
const reapSessions = createPlacementAwareReapSessions({ host: backend?.session(), routed: subRunner.sessions });
|
|
@@ -26,7 +26,7 @@ import { resourceSuspendOptIn } from "../resource-suspend.js";
|
|
|
26
26
|
import { isIsolatedExecEnv, routeServiceTask, supPostureOverrides } from "../router/route-orchestration.js";
|
|
27
27
|
import { tightenGoverned } from "../governance-overrides.js";
|
|
28
28
|
import { gateExecutionLane } from "../runtime-caps-resolver.js";
|
|
29
|
-
import { applyRuntimeGovernance
|
|
29
|
+
import { applyRuntimeGovernance } from "../runtime-governance.js";
|
|
30
30
|
import { HttpError, createCodedHttpError, encodeCheckpointScope } from "../security.js";
|
|
31
31
|
import { memoryProjectKeyFor, memorySpecForRequest } from "../memory-scope.js";
|
|
32
32
|
import { inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
|
|
@@ -599,9 +599,6 @@ export function createResolveSpec(ctx) {
|
|
|
599
599
|
throw new HttpError(422, `request settings cannot be applied as written: ${e.message}`);
|
|
600
600
|
}
|
|
601
601
|
}
|
|
602
|
-
if (governed.handsReadOnly === true && Array.isArray(governed.tools)) {
|
|
603
|
-
governed = { ...governed, tools: stripDelegationTools(governed.tools) };
|
|
604
|
-
}
|
|
605
602
|
if (shouldDiscloseNoPersistentMemory({
|
|
606
603
|
memoryFaceMounted: memoryFaceMounted(governed.memory),
|
|
607
604
|
declaredCapable: effectiveMemoryPersistenceCapable(config),
|
package/dist/boot/shutdown.js
CHANGED
|
@@ -8,7 +8,7 @@ import { installProcessExitBoundary } from "./exit-boundary.js";
|
|
|
8
8
|
import { createEngineLeaseTracker } from "./engine-lease.js";
|
|
9
9
|
import { writeExitLast, readExitLast, clearExitLast } from "./exit-last.js";
|
|
10
10
|
import { isScriptRealmRejection, describeRejectionReason } from "../orchestration/hardened-vm-runner.js";
|
|
11
|
-
import { DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL, HARD_STOP_ACTION_ID } from "../fleet/drain-entry.js";
|
|
11
|
+
import { DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL, HARD_STOP_ACTION_ID, drainDeadlineBeyondBound } from "../fleet/drain-entry.js";
|
|
12
12
|
const HARD_STOP_GRACE_MS = 10_000;
|
|
13
13
|
const FINAL_EXIT_MS = 15_000;
|
|
14
14
|
export function backgroundGraceRemainingMs(a) {
|
|
@@ -140,6 +140,7 @@ export function installShutdownHandlers(ctx) {
|
|
|
140
140
|
});
|
|
141
141
|
const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: FINAL_EXIT_MS }); process.exit(0); }, FINAL_EXIT_MS);
|
|
142
142
|
finalExit.unref?.();
|
|
143
|
+
drainState.endSubscriptions?.();
|
|
143
144
|
server.closeIdleConnections?.();
|
|
144
145
|
const forceClose = setTimeout(() => server.closeAllConnections?.(), HARD_STOP_GRACE_MS);
|
|
145
146
|
forceClose.unref?.();
|
|
@@ -154,6 +155,8 @@ export function installShutdownHandlers(ctx) {
|
|
|
154
155
|
return { ...current, state: "already_draining" };
|
|
155
156
|
if (closing)
|
|
156
157
|
return { actionId: HARD_STOP_ACTION_ID, since: closingAt, deadlineAt: closingAt + FINAL_EXIT_MS, state: "already_draining" };
|
|
158
|
+
if (entry.deadlineBoundMs !== undefined && drainDeadlineBeyondBound(entry.deadlineAt, at, entry.deadlineBoundMs))
|
|
159
|
+
return { state: "deadline_beyond_bound", boundMs: entry.deadlineBoundMs };
|
|
157
160
|
draining = true;
|
|
158
161
|
drainStartedAt = performance.now();
|
|
159
162
|
firstStopSignalAt = bySignal ? drainStartedAt : undefined;
|
|
@@ -167,6 +170,7 @@ export function installShutdownHandlers(ctx) {
|
|
|
167
170
|
const deadlineMono = drainStartedAt + windowMs;
|
|
168
171
|
deviceHub?.beginDrain({ graceMs: () => windowMs });
|
|
169
172
|
drainState.yieldInflight?.();
|
|
173
|
+
drainState.endSubscriptions?.();
|
|
170
174
|
toolApproval?.beginDrain();
|
|
171
175
|
void fleetClient?.announceNow();
|
|
172
176
|
const inflight = drainState.inflight?.() ?? 0;
|
|
@@ -234,6 +238,7 @@ export function installShutdownHandlers(ctx) {
|
|
|
234
238
|
process.on("SIGINT", onSigint);
|
|
235
239
|
const onSighup = createSighupIdleHandler({
|
|
236
240
|
inflight: () => drainState.inflight?.() ?? 0,
|
|
241
|
+
attached: () => drainState.attachedStreams?.() ?? 0,
|
|
237
242
|
lastActivityAt: () => drainState.lastActivityAt?.() ?? 0,
|
|
238
243
|
isStopped: () => closing || draining,
|
|
239
244
|
escalate: () => drainOnCause("signal:SIGHUP"),
|
|
@@ -5,9 +5,9 @@ import { startRetentionLane, RETENTION_LEASE_TTL_FACTOR } from "./retention-lane
|
|
|
5
5
|
import { createLimitSync } from "./limit-sync.js";
|
|
6
6
|
import { runDenySweep } from "./late-binds.js";
|
|
7
7
|
export function runStage08(ctx) {
|
|
8
|
-
const { approvalNonceStore, backend, backgroundAgentStore, ceilingVerdict, checkpointStore, config, configCenter, consolidationSeatWatch, costQuota, degradeRouteWatch, fileHistoryErasure, handslessRunner, handslessSubRunner, hookAgentRunner, imageBakes, instanceId, localRoot, logger, mailboxStore, metrics, permissionRuleStores, pricing, rateLimiter, retentionExecutor, retentionLaneStore, rosterStore, runStore, runner, sqlWorkflowRunStore, subRunner, taskAttachmentStore, toolApproval, toolResultStore, userInputLedger, workflowJournalStore, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, worktreeReap } = ctx;
|
|
8
|
+
const { approvalNonceStore, backend, backgroundAgentStore, ceilingVerdict, checkpointStore, parkCommits, config, configCenter, consolidationSeatWatch, costQuota, degradeRouteWatch, fileHistoryErasure, handslessRunner, handslessSubRunner, hookAgentRunner, imageBakes, instanceId, localRoot, logger, mailboxStore, metrics, permissionRuleStores, pricing, rateLimiter, retentionExecutor, retentionLaneStore, rosterStore, runStore, runner, sqlWorkflowRunStore, subRunner, taskAttachmentStore, toolApproval, toolResultStore, userInputLedger, workflowJournalStore, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, worktreeReap } = ctx;
|
|
9
9
|
const reaper = startReapers({
|
|
10
|
-
config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore,
|
|
10
|
+
config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, parkCommits,
|
|
11
11
|
rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap,
|
|
12
12
|
workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore,
|
|
13
13
|
backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, approvalNonceStore, userInputLedger,
|
|
@@ -67,7 +67,7 @@ export interface MemoryFaceFacts {
|
|
|
67
67
|
/** 本请求命中场景的手带表态([ref] 判别位)。`none` = 该场景跑在不挂 `executionEnvFactory` 的 Runner 上,
|
|
68
68
|
* core 的手带 band(Bash/Edit/Write/…)整批不 mount。 */
|
|
69
69
|
hands: ScenarioHands;
|
|
70
|
-
/** 折完 governance+settings 后的最终 `handsReadOnly`(plan
|
|
70
|
+
/** 折完 governance+settings 后的最终 `handsReadOnly`(`AUTONOMY=read-only` 收窄;plan 模式不点亮它 —— plan 的手带照挂、调用时由引擎问)。手带只剩读腿。 */
|
|
71
71
|
handsReadOnly: boolean;
|
|
72
72
|
/** 折完之后的最终 `TaskSpec.excludeAllTools`(S-493;core 7.8.0 [ref])。`true` = **整只**工具面卸空
|
|
73
73
|
* (core `prepareDeferClassify` 对它直接 `roster.unmountWhere(() => true)`)⇒ 手带在场也一条落盘路都不剩。
|
package/dist/config-catalog.js
CHANGED
|
@@ -74,7 +74,7 @@ export const CONFIG_CATALOG = [
|
|
|
74
74
|
r("AUTH_BRIDGE_AUDIENCE", "auth", "string", "registry SSO-JWT 验签期望 aud", { danger: SEC }),
|
|
75
75
|
r("AUTH_BRIDGE_ISSUER", "auth", "url", "registry SSO-JWT issuer(auth bridge 的开关之一;S-671 / C-R100 ① 起**不是**托管证据 —— 多租户只认 REQUIRE_PRINCIPAL=true)", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 桥的装配处回退到 configCenter.baseUrl(配了 center 即默认开桥);显式值与回退值在托管判定上同判(都不算)", configKey: "authBridgeIssuer" }),
|
|
76
76
|
r("AUTH_BRIDGE_JWKS_URL", "auth", "url", "registry SSO-JWT 的 JWKS 地址(缺省从 issuer 派生)", { danger: SEC }),
|
|
77
|
-
r("AUTONOMY", "auth", "enum", "运维 autonomy
|
|
77
|
+
r("AUTONOMY", "auth", "enum", "运维 autonomy 模式(read-only ⇒ 只读手 + 写面 roots,每段续跑重折,子代同受、委派照挂;ask ⇒ 恒 shellGate,每段重折;plan ⇒ 提交入口的模式词地板:新提交的任务以 permissionMode:plan 起步,请求词钳到它之下,写与非只读调用在调用时问 / 拒,批准计划之后整条 run 按批准的档跑完);center 腿热且 non-sticky(plan 地板只作用于之后提交的任务)", { danger: SEC, enumValues: AUTONOMY_MODES, configKey: "autonomy", center: cGov("autonomy") }),
|
|
78
78
|
r("AUTO_MODE_CLASSIFIER_FALLBACK", "approval", "enum", "auto 分类器席答不出裁决时的梯子(main=主模型代判 / none=只留分类器那一级,整条拒)", { enumValues: ["main", "none"], danger: SEC, derivedDefaultNote: "未设/空串 ⇒ 不铸键,跟随引擎缺省 main(core 7.18.0 起;空串按未设,与其余词表旋钮同判);none = 7.77.0 及以前的行为逐字;词表外的值启动期拒启。权限轴放宽,升级须知见 DEPLOY-PREREQS", configKey: "autoModeClassifierFallback" }),
|
|
79
79
|
r("AUTO_MODE_CLASSIFIER_TIMEOUT_MS", "approval", "number", `auto 分类器单级判决上限(ms;缺席 = core 缺省;自托管慢网关按排队 + prefill 留足;一级到点没裁 ⇒ 走下一级,走完仍没裁 ⇒ 这次门控调用整条拒;须为 [${AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS.minMs}, ${AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS.maxMs}] 内十进制整数,0 / 小数 / 越界拒启)`, { derivedDefaultNote: "未设/空串 ⇒ 不铸键,跟随 core 缺省(本仓不复制上游数字;core 改缺省本旋钮照映不改)", configKey: "autoModeClassifierTimeoutMs" }),
|
|
80
80
|
r("BAKE_CACHE_BASE", "orchestration", "url", "烤镜像 fetch-once 缓存源 base(注入 --cache-base;绝不受调用方控制)"),
|
|
@@ -148,10 +148,10 @@ export const CONFIG_CATALOG = [
|
|
|
148
148
|
r("EXPERIMENTAL_OBSERVER_AGENTS", "orchestration", "boolean", "observer 开闸线(实验面 ship-dark;多租户只认 center caps,env 被忽略)", { staticDefault: "false", configKey: "experimentalObserverAgents" }),
|
|
149
149
|
r("FILE_HISTORY_CAPTURE", "store", "boolean", "文件历史**捕获**总开关(三条车道同义)。关 = 不再捕获任何改动文件的首次快照(落 core 的部署捕获位 `RunnerDeps.fileHistoryCapture: \"off\"`,能力词 `fileHistoryCapture` 翻 off);S-724(7.104.0)起**只关捕获**:历史店照接,已存历史照常 restoreFiles / rewindFilesTo / 2c session-sync。关掉 ≠ 删掉:已捕获的历史不被批量删除,仍按会话删除与留存规则清理 —— 要清存量走 DELETE 会话或留存旋钮;没有「整库历史不可用」的旋钮", { staticDefault: "true", danger: DATA, configKey: "fileHistoryCapture" }),
|
|
150
150
|
r("FILE_HISTORY_RETENTION_KEEP", "store", "number", "per-file rewind 历史的保留上限(与 core FileStorageBackend 同域键形,三条车道一套语义)", { danger: DATA, derivedDefaultNote: "缺席 ⇒ core 缺省 100" }),
|
|
151
|
-
r("FLEET_ADVERTISE_ADDRESS", "integrations", "url",
|
|
151
|
+
r("FLEET_ADVERTISE_ADDRESS", "integrations", "url", `fleet 面向 center 公告的本 worker 可达地址(中心车道配齐 + 本键 ⇒ 公告环;空串 = 未设;用量环不看本键)。中心往它后面拼路径,与中心基址三席同一条判据:${CENTER_BASE_URL_RULE}`, { configKey: "fleetAdvertiseAddress" }),
|
|
152
152
|
r("FLEET_GENERATION", "integrations", "string", "fleet 成员的代际(配置渲染器写;引擎回声到心跳与 /health 的 generation)。缺席 ⇔ 非 fleet 成员(不续服务租约、不受围栏);在场必须是规范十进制串(\"0\" / \"7\";空串、前导零、符号、空白拒启)", { danger: SEC, configKey: "fleetGeneration" }),
|
|
153
153
|
r("FLEET_LEASE_FAIL_CLOSED", "integrations", "boolean", "租约连接级失败(中心鉴权拒 / 凭证不可读 / 不可达 / 5xx / 超时 / 坏体)时计费入口拒 503 fleet.lease_unavailable(true)还是放行(false);409 not-lease 与耗尽 429 不受它管;词表外拒启", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 按部署姿态派生:REQUIRE_PRINCIPAL=true ⇒ true(多租:额度是安全轴);单用户 ⇒ false(AP 姿态)。显式值压过派生。生效值见 caps centerWiring.failMode", configKey: "fleetLeaseFailClosed" }),
|
|
154
|
-
r("FLEET_LEASE_OUTAGE_MAX_MS", "integrations", "number", "
|
|
154
|
+
r("FLEET_LEASE_OUTAGE_MAX_MS", "integrations", "number", "一台实例「活着而不服务」的最长时长 T_max(ms),两处读它:① 服务租约中心不可达的容忍上限 —— 租约过期满这么久 ⇒ 新计费提交 503 state.lease_expired(取消 / HITL 应答 / 续跑 / 读面照放行,在飞不动,/readyz 不翻),只在 fleet 成员 ∧ 中心宣告 servingLease 能力位时生效;② 排空口 POST /v1/admin/drain 的期限上界 —— deadlineAt 离此刻超过它 ⇒ 400 request.field_invalid,成员与非成员同读;< 60000(租约 TTL 地板)或非整数拒启", { danger: SEC, staticDefault: String(FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS), configKey: "fleetLeaseOutageMaxMs" }),
|
|
155
155
|
r("FLEET_SCOPE", "integrations", "string", "本引擎所在的租户 scope(配置渲染器为多租引擎写;唯一读者 = 排空口 POST /v1/admin/drain 的身份比对,请求不带 scope 也读成缺省 scope)。在场必须是 scope id 形(小写字母 / 数字 / 内部连字符,1–64;空串拒启);与 FLEET_GENERATION 不联动(成员缺席 ⇒ 缺省 scope)。续租体与心跳不带它(scope 由中心凭证绑定)", { danger: SEC, staticDefault: GLOBAL_SCOPE, configKey: "fleetScope" }),
|
|
156
156
|
r("FORK_ENABLED", "orchestration", "boolean", "CC /fork(core Fork 工具)装配开关", { derivedDefaultNote: "posture 三态:缺席 = 单用户 ⇒ on / 多租户 ⇒ off(per-principal entitlement 另判)", configKey: "forkEnabled" }),
|
|
157
157
|
r("GATEWAY_MAX_RETRIES", "modelPlane", "number", "openai 兼容腿每次调用重试上限", { derivedDefaultNote: "缺席 ⇒ 不传引擎,引擎默认当家" }),
|
|
@@ -73,16 +73,13 @@ export interface CenterRequestInit {
|
|
|
73
73
|
readonly fetchImpl?: typeof fetch;
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* `file://` / `data:`,配错的地址不许变成读本地文件)
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* 的短语(只回显 scheme,不回显值)。
|
|
76
|
+
* 「后面要拼路径的基址」的拒句 —— 中心基址三席(`SEMA_REGISTRY_URL` / `SEMA_REGISTRY_LIVE_URL` / `SEMA_REGISTRY_WEB_URL`)与本实例的
|
|
77
|
+
* 公告地址(`FLEET_ADVERTISE_ADDRESS`,中心往它后面拼 `/v1/...`)同读。判官是 `url-shape.ts` 那一只(层级式 http(s) URL;Node 的 fetch
|
|
78
|
+
* 认 `file://` / `data:`,配错的地址不许变成读本地文件),本函数只声明这一席禁哪几件({@link PATH_BASE_FORBIDS})。起服时 `config.ts` 判
|
|
79
|
+
* (拒启),请求期 {@link centerFetch} 对同一份基址再判一次(自拼 `ServiceConfig` 的嵌入方也吃同一条规则)。合格 ⇒ `undefined`;否则交回
|
|
80
|
+
* `<subject> <规则词>; got "<展示面安全形>"` —— 拒句里没有从值里解析出来的任何记号(scheme / userinfo / 主机名),捕获点照原样落日志即可。
|
|
82
81
|
*/
|
|
83
|
-
|
|
84
|
-
export declare const PATH_BASE_SCHEME_RULE = "scheme must be http or https";
|
|
85
|
-
export declare function pathBaseUrlFlaw(raw: string): string | undefined;
|
|
82
|
+
export declare function pathBaseUrlRefusal(subject: string, raw: string): string | undefined;
|
|
86
83
|
/**
|
|
87
84
|
* S-698 —— 凭证续签:`POST {issuer}/api/v1/auth/token/refresh`,form `refresh_token=…`(settings-schema `AUTH_V1_PATHS.tokenRefresh`)。
|
|
88
85
|
* · 以 `issuer` 为根(可带路径前缀;同源与否由续签器先判,本函数只装配);query / fragment 不带过去。
|
|
@@ -2,6 +2,7 @@ import settingsSchemaPackage from "@sema-agent/settings-schema/package.json" wit
|
|
|
2
2
|
import { AUTH_V1_PATHS } from "@sema-agent/settings-schema/api/auth";
|
|
3
3
|
import { FLEET_LIVE_PATHS, FLEET_PATHS } from "@sema-agent/settings-schema/fleet";
|
|
4
4
|
import { CenterCredentialUnavailableError } from "../center-credential.js";
|
|
5
|
+
import { buildHttpUrlRefusal, httpUrlFlaws } from "../url-shape.js";
|
|
5
6
|
export const SCHEMA_MAJOR_HEADER = "x-sema-schema-major";
|
|
6
7
|
export const CENTER_PULL_TIMEOUT_MS = 8000;
|
|
7
8
|
export const SETTINGS_SCHEMA_MAJOR = (() => {
|
|
@@ -55,38 +56,21 @@ function routePath(route, kind) {
|
|
|
55
56
|
return "/api/config/execution-keys";
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
-
export function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
parsed = new URL(raw);
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
parsed = undefined;
|
|
66
|
-
}
|
|
67
|
-
if (parsed === undefined)
|
|
68
|
-
return "is not a parseable URL (no scheme? it must look like http(s)://host[:port][/path])";
|
|
69
|
-
const broken = [
|
|
70
|
-
...(parsed.protocol !== "http:" && parsed.protocol !== "https:" ? [`${PATH_BASE_SCHEME_RULE} (got "${parsed.protocol.replace(/:$/, "")}")`] : []),
|
|
71
|
-
...(parsed.username !== "" || parsed.password !== "" ? ["must not carry userinfo (credentials never ride in a base URL: it is logged, reported and has paths appended)"] : []),
|
|
72
|
-
...(parsed.search !== "" || raw.includes("?") ? ["must not carry a query (request paths are appended to this base)"] : []),
|
|
73
|
-
...(parsed.hash !== "" || raw.includes("#") ? ["must not carry a fragment (request paths are appended to this base)"] : []),
|
|
74
|
-
];
|
|
75
|
-
return broken.length === 0 ? undefined : broken.join("; ");
|
|
59
|
+
const PATH_BASE_FORBIDS = ["userinfo", "query", "fragment"];
|
|
60
|
+
export function pathBaseUrlRefusal(subject, raw) {
|
|
61
|
+
const flaws = httpUrlFlaws(raw, PATH_BASE_FORBIDS);
|
|
62
|
+
return flaws.length === 0 ? undefined : buildHttpUrlRefusal(subject, raw, flaws);
|
|
76
63
|
}
|
|
77
64
|
function assertPathBaseUrl(raw, what) {
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
throw new Error(
|
|
81
|
-
}
|
|
82
|
-
function assertHttpScheme(url, what) {
|
|
83
|
-
const scheme = url.protocol;
|
|
84
|
-
if (scheme !== "http:" && scheme !== "https:")
|
|
85
|
-
throw new Error(`${what} must be http(s), got "${scheme}"`);
|
|
65
|
+
const refusal = pathBaseUrlRefusal(what, raw);
|
|
66
|
+
if (refusal !== undefined)
|
|
67
|
+
throw new Error(refusal);
|
|
86
68
|
}
|
|
87
69
|
export async function centerTokenRefresh(issuer, refreshToken, init) {
|
|
70
|
+
const flaws = httpUrlFlaws(issuer, []);
|
|
71
|
+
if (flaws.length > 0)
|
|
72
|
+
throw new Error(buildHttpUrlRefusal("center credential issuer", issuer, flaws));
|
|
88
73
|
const url = new URL(issuer);
|
|
89
|
-
assertHttpScheme(url, "center credential issuer");
|
|
90
74
|
url.pathname = `${url.pathname.replace(/\/+$/, "")}${AUTH_V1_PATHS.tokenRefresh}`;
|
|
91
75
|
url.search = "";
|
|
92
76
|
url.hash = "";
|
package/dist/config-types.d.ts
CHANGED
|
@@ -127,8 +127,8 @@ export declare const DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS = 250;
|
|
|
127
127
|
* 住在本叶(而不在 `fleet/drain-entry.ts`):`config.ts` 读它,而 drain-entry 值引 `plugins/store-contracts.ts`,config 不值引 plugins 层。
|
|
128
128
|
*/
|
|
129
129
|
export declare function fleetScopeOf(raw: string | undefined): string;
|
|
130
|
-
/** 中心基址三席(`SEMA_REGISTRY_URL` / `SEMA_REGISTRY_LIVE_URL` / `SEMA_REGISTRY_WEB_URL`)
|
|
131
|
-
export declare const CENTER_BASE_URL_RULE = "\u8BBE\u4E86\u5C31\u5FC5\u987B\u662F\u53EF\u89E3\u6790\u7684 http(s) URL,\u4E14\u4E0D\u5E26\u51ED\u636E / query / fragment(\u57FA\u5740\u540E\u9762\u8981\u62FC\u8DEF\u5F84);\u574F\u5F62\u62D2\u542F(\u6CA1\u63A5\u4E2D\u5FC3\u4E5F\u62D2)";
|
|
130
|
+
/** 中心基址三席(`SEMA_REGISTRY_URL` / `SEMA_REGISTRY_LIVE_URL` / `SEMA_REGISTRY_WEB_URL`)与公告地址的形句 —— 判官 `config-center/center-request.ts` `pathBaseUrlRefusal` 一只(形判 = `url-shape.ts`),配置目录四行同读这一句。 */
|
|
131
|
+
export declare const CENTER_BASE_URL_RULE = "\u8BBE\u4E86\u5C31\u5FC5\u987B\u662F\u53EF\u89E3\u6790\u7684 http(s) URL,\u4E14\u4E0D\u5E26\u51ED\u636E / query / fragment(\u57FA\u5740\u540E\u9762\u8981\u62FC\u8DEF\u5F84);\u574F\u5F62\u62D2\u542F(\u6CA1\u63A5\u4E2D\u5FC3\u4E5F\u62D2),\u62D2\u53E5\u53EA\u70B9\u540D\u89C4\u5219,\u4E0D\u56DE\u663E scheme";
|
|
132
132
|
/** 编排器片 2:`FLEET_LEASE_OUTAGE_MAX_MS` 的缺省(24 h)—— 中心 / 库故障、中心降级在今天都靠 LKG 无限期容忍,10 分钟级的窗会把它们变成数据面停服。 */
|
|
133
133
|
export declare const FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS = 86400000;
|
|
134
134
|
/**
|
|
@@ -1479,8 +1479,9 @@ export interface ServiceConfigFlat {
|
|
|
1479
1479
|
* `TaskSpec.enablePlanMode:true` but NOT `handsReadOnly` (writable-start) → core auto-mounts `enter_plan_mode`
|
|
1480
1480
|
* (+ `present_plan`) so the model can self-call `enter_plan_mode` at ANY time it judges a task needs planning
|
|
1481
1481
|
* (CC EnterPlanMode parity). 🆕 DEFAULT ON (clay: model-driven plan is resident — the model decides when to plan);
|
|
1482
|
-
* set `PLAN_MODE_ENABLED=false` to opt out. USER-driven plan (`body.permissionMode=plan
|
|
1483
|
-
*
|
|
1482
|
+
* set `PLAN_MODE_ENABLED=false` to opt out. A USER-driven plan (`body.permissionMode=plan`, bundle `defaultMode`, or
|
|
1483
|
+
* `AUTONOMY=plan`) is the same engine mode word, independent of this knob: the engine's mode table mounts the plan
|
|
1484
|
+
* tools for a declared `plan` itself, and no plan entry clamps the hands (only `AUTONOMY=read-only` does). */
|
|
1484
1485
|
planModeEnabled: boolean;
|
|
1485
1486
|
/**
|
|
1486
1487
|
* [ref]⑥(core 5.56 `Hooks.timeoutMs` 座)—— **每一只 hook 席位单次调用的时间上限**(ms)。
|
|
@@ -2134,10 +2135,12 @@ export interface ServiceConfigFlat {
|
|
|
2134
2135
|
* `loadConfig` **无条件**解析一次:未设 / 空串 ⇒ 缺席(公告环不装);在场必须是可解析的 http(s) URL,否则拒启。读者:fleet 公告环、
|
|
2135
2136
|
* 围栏判官 `fleetFenceLaneOf`、`/health.center.sessionsVisible`、启动日志 —— 都读这一席,不读 env。 */
|
|
2136
2137
|
fleetAdvertiseAddress?: string;
|
|
2137
|
-
/** 编排器片 2
|
|
2138
|
-
*
|
|
2139
|
-
*
|
|
2140
|
-
*
|
|
2138
|
+
/** 编排器片 2:一台实例「活着而不服务」的最长时长 `T_max`(ms;env `FLEET_LEASE_OUTAGE_MAX_MS`)。缺省 86 400 000(24 h);地板 = 服务租约
|
|
2139
|
+
* TTL 地板(settings-schema `SERVING_LEASE_TTL_FLOOR_MS`,60 s);不许 0 / 无穷,坏值拒启。`loadConfig` 无条件解析(成员与非成员同读)。两个读者:
|
|
2140
|
+
* ① 服务租约**中心不可达的容忍上限**(`fleet/serving-lease.ts`;只在 fleet 成员 ∧ 中心宣告了 `servingLease` 能力位时生效):租约过期(中心
|
|
2141
|
+
* 不可达 / 答 unknown)之后照常服务,直到过期满这么久 ⇒ 新计费提交 503 `state.lease_expired`(取消 / HITL 应答 / 续跑 / 读面照放行),在飞不动、
|
|
2142
|
+
* 不自停;中心恢复授予即恢复。② 排空口 `POST /v1/admin/drain` 的**期限准入上界**(`fleet/drain-entry.ts` 规则 2):`deadlineAt` 离此刻超过它 ⇒
|
|
2143
|
+
* 400 `request.field_invalid`。 */
|
|
2141
2144
|
fleetLeaseOutageMaxMs: number;
|
|
2142
2145
|
/** S-698(design-325 §2):中心凭证**续签提前量**(ms)—— `SEMA_REGISTRY_TOKEN_FILE` 指向人的登录会话信封
|
|
2143
2146
|
* (`kind:"user-session"`)时,访问令牌离到期 ≤ 本值即由引擎续签并原子改写同一文件(`center-credential-renewer.ts`)。
|
package/dist/config-types.js
CHANGED
|
@@ -12,7 +12,7 @@ export const DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS = 250;
|
|
|
12
12
|
export function fleetScopeOf(raw) {
|
|
13
13
|
return raw ?? GLOBAL_SCOPE;
|
|
14
14
|
}
|
|
15
|
-
export const CENTER_BASE_URL_RULE = "设了就必须是可解析的 http(s) URL,且不带凭据 / query / fragment(基址后面要拼路径);坏形拒启(没接中心也拒)";
|
|
15
|
+
export const CENTER_BASE_URL_RULE = "设了就必须是可解析的 http(s) URL,且不带凭据 / query / fragment(基址后面要拼路径);坏形拒启(没接中心也拒),拒句只点名规则,不回显 scheme";
|
|
16
16
|
export const FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS = 86_400_000;
|
|
17
17
|
export const FIRST_BYTE_TIMEOUT_BOUNDS = { minMs: 0, maxMs: MAX_TIMER_DELAY_MS };
|
|
18
18
|
export const RESILIENCE_DEFAULTS = {
|
package/dist/config.d.ts
CHANGED
|
@@ -300,8 +300,8 @@ export declare function isRosterGateArmed(configCenter: ServiceConfig["configCen
|
|
|
300
300
|
* 的判词 `unmintable_segment`,句子带它的 guidance 与 1:1 现名);本仓 7.116.0–7.117.x 那只自比 `mintNamespacePrefix` 的对照随之退役。
|
|
301
301
|
* 引擎判官放行、却一只工具都够不着的一形 —— **工具段为空**(`mcp__srv__`)—— 由本仓判(`namespaced-rule-name.ts`,会话规则写口两腿与 SQL 店写口同调)。
|
|
302
302
|
* ⚠️ 两段覆盖形 `mcp__<server>`(服务器段是自铸形)够得着(S-1047 收掉的分叉,见下)。
|
|
303
|
-
* - **core
|
|
304
|
-
*
|
|
303
|
+
* - **core 的文法编不出来的串**:判词取自 core 自己的 `validatePermissionRules`,射程与指引见 {@link uncompilableRuleGuidance}
|
|
304
|
+
* (名表消费者只递得来名字里带反斜杠、单条 >512 字符、空串三种;规则编译器消费者递来的每一条内容形都走这里)。[ref]①([ref])补齐 ——
|
|
305
305
|
* 此前只有 `ask` 腿(经 `createPermissionRulePolicy` 的 throw)拒得出这一族,`deny` 静默 no-op。
|
|
306
306
|
*
|
|
307
307
|
* 已接受的权衡(与退役表墓碑同一条):部署自有工具真叫 `figma__x` 的极端形会被误拒——boot 期无 roster 可
|
package/dist/config.js
CHANGED
|
@@ -13,7 +13,7 @@ import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
|
|
|
13
13
|
import { CONFIG_PROVIDER_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS, FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS, fleetScopeOf } from "./config-types.js";
|
|
14
14
|
import { FLEET_GENERATION_ENV, FLEET_SCOPE_ENV, SERVING_LEASE_TTL_FLOOR_MS, isCanonicalGeneration } from "@sema-agent/settings-schema/fleet";
|
|
15
15
|
import { ScopeId } from "@sema-agent/settings-schema/api/scopes";
|
|
16
|
-
import { centerLaneConfigured,
|
|
16
|
+
import { centerLaneConfigured, pathBaseUrlRefusal } from "./config-center/center-request.js";
|
|
17
17
|
import { resolveLocalDataRoot } from "./local-data-root.js";
|
|
18
18
|
import { buildWriteProtectionPosture } from "./write-protection.js";
|
|
19
19
|
import { normalizeSshFingerprint } from "./ssh-host-key.js";
|
|
@@ -33,6 +33,7 @@ import { createCenterCredentialSource, readCenterCredentialDeclaration } from ".
|
|
|
33
33
|
import { parseCenterCredentialRenewLeadMs } from "./center-credential-renewer.js";
|
|
34
34
|
import { modelProviderRefusal } from "./model-provider.js";
|
|
35
35
|
import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
36
|
+
import { buildHttpUrlRefusal, httpUrlFlaws } from "./url-shape.js";
|
|
36
37
|
import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
|
|
37
38
|
import { displayListEntry } from "./list-entry-display.js";
|
|
38
39
|
import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
|
|
@@ -1227,9 +1228,7 @@ function uncompilableRuleGuidance(name, behavior, pathBases) {
|
|
|
1227
1228
|
const issue = issues[0];
|
|
1228
1229
|
if (issue === undefined)
|
|
1229
1230
|
return undefined;
|
|
1230
|
-
return
|
|
1231
|
-
`Tool names carry no backslashes and are far shorter than the 512-character rule cap; check the spelling, and use ` +
|
|
1232
|
-
`mcp__<server>__* / a2a__<peer>__* for "every tool on that peer"`);
|
|
1231
|
+
return `${issue.code} — core's rule grammar cannot compile this entry: ${issue.message}`;
|
|
1233
1232
|
}
|
|
1234
1233
|
function findUnmatchableToolNames(names, opts) {
|
|
1235
1234
|
const out = [];
|
|
@@ -1564,16 +1563,11 @@ function parseMemoryEmbedder(ctx) {
|
|
|
1564
1563
|
if (endpoint === undefined || model === undefined || dimRaw === undefined) {
|
|
1565
1564
|
throw new Error(`MEMORY_EMBEDDER_* is half-configured — missing ${missing.join(", ")}. The memory vector plane needs all three of MEMORY_EMBEDDER_ENDPOINT, MEMORY_EMBEDDER_MODEL, MEMORY_EMBEDDER_DIM; refusing to start rather than silently running lexical-only retrieval while the deployment believes vectors are on (#228).`);
|
|
1566
1565
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
if (proto !== "http:" && proto !== "https:") {
|
|
1573
|
-
throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${buildDisplaySafeUrl(endpoint)}" must be http(s) (got "${proto}") — the embedder leg is an HTTP POST to an OpenAI-compatible /embeddings face`);
|
|
1574
|
-
}
|
|
1575
|
-
if (parsedEndpoint.username !== "" || parsedEndpoint.password !== "") {
|
|
1576
|
-
throw new Error("MEMORY_EMBEDDER_ENDPOINT must not carry credentials in the URL (user:password@host) — URL userinfo leaks into transport errors, logs and memory conflict reports. Use MEMORY_EMBEDDER_API_KEY (sent as a Bearer header) instead.");
|
|
1566
|
+
const endpointFlaws = httpUrlFlaws(endpoint, ["userinfo"]);
|
|
1567
|
+
if (endpointFlaws.length > 0) {
|
|
1568
|
+
throw new Error(`${buildHttpUrlRefusal("MEMORY_EMBEDDER_ENDPOINT", endpoint, endpointFlaws)} — the embedder leg is an HTTP POST to an OpenAI-compatible /embeddings face ` +
|
|
1569
|
+
`(expected e.g. https://embeddings.example.com/v1 — the OpenAI-compatible base, or the full .../embeddings endpoint); ` +
|
|
1570
|
+
`credentials in the URL leak into transport errors, logs and memory conflict reports, so use MEMORY_EMBEDDER_API_KEY (sent as a Bearer header) instead.`);
|
|
1577
1571
|
}
|
|
1578
1572
|
const dimensions = Number(dimRaw);
|
|
1579
1573
|
if (!Number.isSafeInteger(dimensions) || dimensions < 1) {
|
|
@@ -2249,10 +2243,10 @@ function parseFleetScope(raw) {
|
|
|
2249
2243
|
function parseFleetAdvertiseAddress(raw) {
|
|
2250
2244
|
if (raw === undefined || raw === "")
|
|
2251
2245
|
return {};
|
|
2252
|
-
const
|
|
2253
|
-
if (
|
|
2254
|
-
throw fleetEnvRefusal("advertise_address_invalid",
|
|
2255
|
-
`
|
|
2246
|
+
const refusal = pathBaseUrlRefusal("env FLEET_ADVERTISE_ADDRESS", raw);
|
|
2247
|
+
if (refusal !== undefined) {
|
|
2248
|
+
throw fleetEnvRefusal("advertise_address_invalid", `${refusal} — it must be an http(s) base URL such as "http://10.0.0.5:8090": the fleet center rejects any other shape on every announce. ` +
|
|
2249
|
+
`Unset it if this engine does not announce itself.`);
|
|
2256
2250
|
}
|
|
2257
2251
|
return { fleetAdvertiseAddress: raw };
|
|
2258
2252
|
}
|
|
@@ -2280,7 +2274,8 @@ function parseFleetLeaseOutageMaxMs(raw) {
|
|
|
2280
2274
|
const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
|
|
2281
2275
|
throw new Error(`env FLEET_LEASE_OUTAGE_MAX_MS=${JSON.stringify(shown)} must be a whole number of milliseconds ≥ ${SERVING_LEASE_TTL_FLOOR_MS} (the serving-lease TTL floor; ` +
|
|
2282
2276
|
`decimal digits only; unset = ${FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS}, 24 h). It bounds how long this engine keeps taking new submissions after its serving ` +
|
|
2283
|
-
`lease expired with the center unreachable — "0" and "infinite" are not values
|
|
2277
|
+
`lease expired with the center unreachable, and how far from now a drain request's deadlineAt may be — "0" and "infinite" are not values ` +
|
|
2278
|
+
`(the bound must be finite and non-trivial). Refusing to boot.`);
|
|
2284
2279
|
}
|
|
2285
2280
|
return n;
|
|
2286
2281
|
}
|
|
@@ -2293,9 +2288,9 @@ function parseFleetLeaseFailClosed(raw, requirePrincipal) {
|
|
|
2293
2288
|
function parseCenterBaseUrl(name, raw, role) {
|
|
2294
2289
|
if (raw === undefined || raw === "")
|
|
2295
2290
|
return undefined;
|
|
2296
|
-
const
|
|
2297
|
-
if (
|
|
2298
|
-
throw new Error(
|
|
2291
|
+
const refusal = pathBaseUrlRefusal(`env ${name}`, raw);
|
|
2292
|
+
if (refusal !== undefined)
|
|
2293
|
+
throw new Error(`${refusal}. ${role}`);
|
|
2299
2294
|
return raw;
|
|
2300
2295
|
}
|
|
2301
2296
|
function parseConfigCenter(baseUrl, requirePrincipal, liveBaseUrl, webBaseUrl) {
|
|
@@ -2322,20 +2317,13 @@ function parseA2aServe() {
|
|
|
2322
2317
|
`and that address is the operator's to declare (it is NOT derived from the request Host header: an anonymous discovery caller must not decide what our public card advertises). ` +
|
|
2323
2318
|
`Set A2A_SERVE_URL to this deployment's externally reachable /v1/a2a URL (e.g. "https://agent.example.com/v1/a2a").`);
|
|
2324
2319
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
parsed = undefined;
|
|
2331
|
-
}
|
|
2332
|
-
if (parsed === undefined || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
|
|
2333
|
-
throw new Error(`A2A_SERVE_URL must be a parseable http(s) URL (e.g. "https://agent.example.com/v1/a2a"); got ${JSON.stringify(buildDisplaySafeUrl(rawUrl))}`);
|
|
2334
|
-
}
|
|
2335
|
-
if (parsed.username !== "" || parsed.password !== "") {
|
|
2336
|
-
throw new Error(`A2A_SERVE_URL must not carry userinfo (credentials before "@") — it is published verbatim on the ANONYMOUSLY readable agent card, so those credentials would be handed to every discovery caller. ` +
|
|
2337
|
-
`Inbound authentication on this endpoint is header-shaped (a service credential), not URL-embedded; strip the userinfo and keep the secret in the caller's headers.`);
|
|
2320
|
+
const urlFlaws = httpUrlFlaws(rawUrl, ["userinfo"]);
|
|
2321
|
+
if (urlFlaws.length > 0) {
|
|
2322
|
+
throw new Error(`${buildHttpUrlRefusal("A2A_SERVE_URL", rawUrl, urlFlaws)} — it is published verbatim on the ANONYMOUSLY readable agent card as this deployment's JSON-RPC endpoint ` +
|
|
2323
|
+
`(e.g. "https://agent.example.com/v1/a2a"), so it must be a plain http(s) address and must not hand credentials to every discovery caller: ` +
|
|
2324
|
+
`inbound authentication on this endpoint is header-shaped (a service credential), not URL-embedded.`);
|
|
2338
2325
|
}
|
|
2326
|
+
const parsed = new URL(rawUrl);
|
|
2339
2327
|
const a2aCredentialScan = redactSecrets(parsed.href);
|
|
2340
2328
|
if (a2aCredentialScan !== parsed.href) {
|
|
2341
2329
|
throw new Error(`A2A_SERVE_URL carries something the credential-redaction law recognizes (${a2aCredentialScan}) — this address is published VERBATIM on the ANONYMOUSLY readable agent card (the card's url is a functional address: peers fetch it, so it is never rewritten for display), ` +
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* 非编排路径 = 停止信号(SIGTERM)/ SIGHUP 升级 / 附着租约自退,动作号由引擎自铸({@link ENGINE_DRAIN_CAUSES})。全部汇到同一只入口。
|
|
9
9
|
* 2. **期限在进入那一拍定死**:请求带的 `deadlineAt` 原样;引擎自起的排空 = 起点 + 此刻的排空窗(`drainGraceMs`)。之后改窗(中心热改
|
|
10
10
|
* `limits.drainGraceMs`)不动在跑的期限 —— 排空驱动读的是期限,不是窗。
|
|
11
|
+
* 请求带的期限有**准入上界** = 进入那一拍 + `config.fleetLeaseOutageMaxMs`(`T_max`,env `FLEET_LEASE_OUTAGE_MAX_MS`,`loadConfig` 无条件
|
|
12
|
+
* 解析、缺省 24 h,成员与非成员同读):一台实例「活着而不服务」最长多久本就由这一席定义(服务租约失联后最多再接单多久,
|
|
13
|
+
* `fleet/serving-lease.ts`),排空口不另立第二个数。上界由调用方随入参声明({@link DrainEntry.deadlineBoundMs};排空口声明它,引擎自起的
|
|
14
|
+
* 排空不声明 —— 那些期限由引擎自己的窗定,拒了就是不排空),**由入口判**:只有这一发会进入排空时才判(已在排空 / 硬停已在进行 ⇒ 期限
|
|
15
|
+
* 不会被采用,照规则 3 回执);越界 ⇒ 零状态变化,入口答 {@link DrainDeadlineRefusal}(排空口 400 `request.field_invalid`)。「会不会
|
|
16
|
+
* 采用这只期限」只有入口一处判 —— 调用方不另读 `draining` 去猜。已过的期限不越界(进入即到期:在飞为零当拍硬停,否则下一拍)。
|
|
11
17
|
* 3. **幂等**:已在排空 ⇒ 不重置,回执 `already_draining` 带**第一次**的动作 / 起点 / 期限(不论这次的动作号是否相同)。
|
|
12
18
|
* 4. **身份**:逐字段比,任一不符 ⇒ 409 `conflict.fleet_identity`,零状态变化。
|
|
13
19
|
* · `scope`:两侧都经 {@link fleetScopeOf} 读成值(缺席 = settings-schema 的缺省 scope,线上任何一处缺席的 scope 都读成它)再严格相等。
|
|
@@ -46,25 +52,39 @@ export declare function leaseDrainActionId(generation: string): LeaseDrainAction
|
|
|
46
52
|
export declare const DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL: {
|
|
47
53
|
readonly [C in EngineDrainCause]: boolean;
|
|
48
54
|
};
|
|
49
|
-
/** 入口的入参:动作号 + 期限(epoch ms)。 */
|
|
55
|
+
/** 入口的入参:动作号 + 期限(epoch ms)+ 调用方声明的期限准入上界(规则 2)。 */
|
|
50
56
|
export interface DrainEntry {
|
|
51
57
|
readonly actionId: string;
|
|
52
58
|
readonly deadlineAt: number;
|
|
59
|
+
/** 期限的准入上界(ms):这一发会进入排空而 `deadlineAt − 进入那一拍 > 它` ⇒ 入口零状态变化,答 {@link DrainDeadlineRefusal}。
|
|
60
|
+
* 排空口传 `config.fleetLeaseOutageMaxMs`;引擎自起的排空(停止信号 / 租约被拒 / 附着租约自退)不传。 */
|
|
61
|
+
readonly deadlineBoundMs?: number;
|
|
53
62
|
}
|
|
63
|
+
/** 入口拒了这只期限(规则 2):越过调用方声明的上界,什么都没变。 */
|
|
64
|
+
export interface DrainDeadlineRefusal {
|
|
65
|
+
readonly state: "deadline_beyond_bound";
|
|
66
|
+
readonly boundMs: number;
|
|
67
|
+
}
|
|
68
|
+
/** 入口的答:契约回执(进入 / 已在排空),或期限越界的拒(只在调用方声明了上界时出现)。按 `state` 穷举。 */
|
|
69
|
+
export type DrainEntryAnswer = DrainReceipt | DrainDeadlineRefusal;
|
|
54
70
|
/**
|
|
55
71
|
* 引擎与 HTTP 面共享的排空盒子(装配层建一只,`createHttpServer` 与收尾链各自往上装读口 / 入口)。
|
|
56
72
|
* · `draining` / `since` / `actionId` / `deadlineAt` —— **只由** {@link enter} 整组写入(排空事实 = 契约 `EngineDraining`);
|
|
57
73
|
* · `enter` —— 唯一入口(收尾链装;缺席 = 这个进程没有收尾链,如嵌入宿主 / 测试夹具);
|
|
58
74
|
* · `yieldInflight` —— 让出本副本在飞的可抢占腿(`createHttpServer` 装);
|
|
59
|
-
* ·
|
|
75
|
+
* · `endSubscriptions` —— 关掉本副本打开着的只读订阅流(发一帧 `draining` 终帧再关;`createHttpServer` 装,S-1281)。订阅流不是在飞的工作
|
|
76
|
+
* (不进 `inflight`),排空不等它;收尾链在排空进入那一拍与硬停起点各调一次(排空期间新开的订阅在硬停那一拍收);
|
|
77
|
+
* · 其余四只是既有的读口(在飞数 / 活性时钟 / 附着流),语义见 `http/server.ts`。`inflight` 只数**工作**(run 腿 / 计费提交 / HITL 应答 /
|
|
78
|
+
* admission 期的普通请求),只读订阅只进 `attachedStreams`(S-1281)。
|
|
60
79
|
*/
|
|
61
80
|
export interface DrainState {
|
|
62
81
|
draining: boolean;
|
|
63
82
|
since?: number;
|
|
64
83
|
actionId?: string;
|
|
65
84
|
deadlineAt?: number;
|
|
66
|
-
enter?: (entry: DrainEntry) =>
|
|
85
|
+
enter?: (entry: DrainEntry) => DrainEntryAnswer;
|
|
67
86
|
yieldInflight?: () => void;
|
|
87
|
+
endSubscriptions?: () => void;
|
|
68
88
|
inflight?: () => number;
|
|
69
89
|
lastActivityAt?: () => number;
|
|
70
90
|
attachedStreams?: () => number;
|
|
@@ -72,6 +92,11 @@ export interface DrainState {
|
|
|
72
92
|
}
|
|
73
93
|
/** 排空盒子里的排空事实(三键齐才是;入口整组写,所以「缺一」只出现在手搭的夹具上)。 */
|
|
74
94
|
export declare function drainingFactOf(s: Pick<DrainState, "draining" | "since" | "actionId" | "deadlineAt">): EngineDraining | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* 规则 2 的准入上界:期限离进入那一拍(本引擎的挂钟)超过 `boundMs` ⇔ 越界。恰好等于上界不越界;已过的期限不越界。入口判它
|
|
97
|
+
* (`boot/shutdown.ts` 的 enterDrain,在「已在排空 / 硬停已在进行」两臂之后)。
|
|
98
|
+
*/
|
|
99
|
+
export declare function drainDeadlineBeyondBound(deadlineAt: number, now: number, boundMs: number): boolean;
|
|
75
100
|
/** 本进程的身份。`scope` 已读成值(`config.fleetScope`);其余三项与 `/health` 上读到的同一份。 */
|
|
76
101
|
export interface EngineIdentity {
|
|
77
102
|
readonly scope: string;
|
|
@@ -13,6 +13,9 @@ export const DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL = {
|
|
|
13
13
|
export function drainingFactOf(s) {
|
|
14
14
|
return s.draining && s.actionId !== undefined && s.since !== undefined && s.deadlineAt !== undefined ? { actionId: s.actionId, since: s.since, deadlineAt: s.deadlineAt } : undefined;
|
|
15
15
|
}
|
|
16
|
+
export function drainDeadlineBeyondBound(deadlineAt, now, boundMs) {
|
|
17
|
+
return deadlineAt - now > boundMs;
|
|
18
|
+
}
|
|
16
19
|
export const DRAIN_IDENTITY_FIELDS = ["scope", "worker", "expectedGeneration", "instanceId"];
|
|
17
20
|
export function drainIdentityMismatches(req, self) {
|
|
18
21
|
return DRAIN_IDENTITY_FIELDS.filter((f) => {
|
|
@@ -39,8 +39,8 @@ export function createServingLeaseClient(o) {
|
|
|
39
39
|
const epoch = o.clock?.epoch ?? Date.now;
|
|
40
40
|
const f = o.fetchImpl ?? fetch;
|
|
41
41
|
const stickyPath = o.dataRoot === undefined ? undefined : join(o.dataRoot, SERVING_LEASE_DENIED_FILE);
|
|
42
|
-
let
|
|
43
|
-
|
|
42
|
+
let capability;
|
|
43
|
+
const enabled = () => capability?.announced === true;
|
|
44
44
|
let denied = false;
|
|
45
45
|
let stopped = false;
|
|
46
46
|
let lease;
|
|
@@ -55,7 +55,7 @@ export function createServingLeaseClient(o) {
|
|
|
55
55
|
};
|
|
56
56
|
const schedule = (ms) => {
|
|
57
57
|
clear();
|
|
58
|
-
if (stopped || denied || !enabled)
|
|
58
|
+
if (stopped || denied || !enabled())
|
|
59
59
|
return;
|
|
60
60
|
timer = setTimeout(() => void renewNow(), Math.max(RENEW_MIN_INTERVAL_MS, ms));
|
|
61
61
|
timer.unref?.();
|
|
@@ -97,7 +97,7 @@ export function createServingLeaseClient(o) {
|
|
|
97
97
|
o.drain({ actionId: leaseDrainActionId(o.generation), deadlineAt: epoch() + o.drainWindowMs() });
|
|
98
98
|
};
|
|
99
99
|
const renewOnce = async () => {
|
|
100
|
-
if (stopped || denied || !enabled)
|
|
100
|
+
if (stopped || denied || !enabled())
|
|
101
101
|
return;
|
|
102
102
|
const sendMono = mono();
|
|
103
103
|
const sendEpoch = epoch();
|
|
@@ -132,7 +132,7 @@ export function createServingLeaseClient(o) {
|
|
|
132
132
|
failStreak = 0;
|
|
133
133
|
switch (reading.verdict) {
|
|
134
134
|
case "granted":
|
|
135
|
-
if (denied
|
|
135
|
+
if (denied)
|
|
136
136
|
return;
|
|
137
137
|
lease = { expiresAtMono: sendMono + reading.ttlMs, leaseUntil: sendEpoch + reading.ttlMs };
|
|
138
138
|
nextInMs = reading.ttlMs / 3;
|
|
@@ -161,29 +161,27 @@ export function createServingLeaseClient(o) {
|
|
|
161
161
|
return;
|
|
162
162
|
const on = caps?.servingLease === true;
|
|
163
163
|
if (!on)
|
|
164
|
-
recordFailOpen("server.fleet.serving-lease-capability-absent", `worker=${o.center.worker} generation=${o.generation} was=${enabled ? "enabled" : "none"}`);
|
|
165
|
-
if (on && !enabled) {
|
|
166
|
-
|
|
167
|
-
enabledAtMono = mono();
|
|
164
|
+
recordFailOpen("server.fleet.serving-lease-capability-absent", `worker=${o.center.worker} generation=${o.generation} was=${enabled() ? "enabled" : "none"}`);
|
|
165
|
+
if (on && !enabled()) {
|
|
166
|
+
capability = { announced: true, firstSeenAtMono: capability?.firstSeenAtMono ?? mono() };
|
|
168
167
|
o.logger?.info("fleet_serving_lease_enabled", { generation: o.generation, worker: o.center.worker });
|
|
169
168
|
void renewNow();
|
|
170
169
|
}
|
|
171
|
-
else if (!on &&
|
|
172
|
-
|
|
173
|
-
lease = undefined;
|
|
170
|
+
else if (!on && capability?.announced === true) {
|
|
171
|
+
capability = { announced: false, firstSeenAtMono: capability.firstSeenAtMono };
|
|
174
172
|
clear();
|
|
175
|
-
o.logger?.warn("fleet_serving_lease_disabled", { note: "the fleet center no longer announces the servingLease capability — the lease is out of effect (leaseState none) until it does again" });
|
|
173
|
+
o.logger?.warn("fleet_serving_lease_disabled", { note: "the fleet center no longer announces the servingLease capability — the lease is out of effect (leaseState none) until it does again; its outage clock is not reset (when the capability returns, the last grant and this process's first sighting of the capability still count)" });
|
|
176
174
|
}
|
|
177
175
|
},
|
|
178
176
|
view() {
|
|
179
177
|
if (denied)
|
|
180
178
|
return { state: "denied" };
|
|
181
|
-
if (!
|
|
179
|
+
if (capability === undefined || !capability.announced)
|
|
182
180
|
return { state: "none" };
|
|
183
181
|
const now = mono();
|
|
184
182
|
if (lease !== undefined && now < lease.expiresAtMono)
|
|
185
183
|
return { state: "granted", leaseUntil: lease.leaseUntil };
|
|
186
|
-
const expiredAt = lease?.expiresAtMono ??
|
|
184
|
+
const expiredAt = lease?.expiresAtMono ?? capability.firstSeenAtMono;
|
|
187
185
|
return { state: "expired", pastOutageMax: now - expiredAt >= o.outageMaxMs };
|
|
188
186
|
},
|
|
189
187
|
resumeStickyDenial() {
|
|
@@ -7,13 +7,18 @@
|
|
|
7
7
|
* 塞 `allowedTools` 的调用点会静默改写请求的表。型上禁掉它(`allowedTools?: never`),写出来就是编译红 —— 比文本扫描的静态门(方括号写法、
|
|
8
8
|
* 展开形会漏)更硬。
|
|
9
9
|
*
|
|
10
|
+
* 🔴 入参型上同样**没有** `permissionMode`:一条 run 的模式词只有存储的提交体一个源(写者 = 每一个 spec 出生点的盖章 —— 新的持久化写都经 `http/checkpoint-ctx.ts` `buildCheckpointCtx` —— 与 `plan_review` 写回),
|
|
11
|
+
* 按段的收紧折叠写它就是第二个源 —— 一个被批准的计划会在下一次挂起→续跑被折回 plan。core 对模式词是钳(不会放宽),所以这里不另设运行期守卫:
|
|
12
|
+
* 型禁就是这条规则的全部(`permissionMode?: never`,写出来就是编译红)。
|
|
13
|
+
*
|
|
10
14
|
* 🔴 型禁有一个洞([ref] 合并复审 opus 腿):本仓没开 `exactOptionalPropertyTypes`,`{ allowedTools: undefined }` 过型检,而 core 的替换会把 base 上的表
|
|
11
15
|
* 抹成 `undefined`。所以折叠口再加一道**运行期**守卫:入参对象自带 `allowedTools` 键(任何值,含 `undefined`)⇒ 抛(fail-closed:宁多问一次)。
|
|
12
16
|
*/
|
|
13
17
|
import { type TaskSpec } from "@sema-agent/core";
|
|
14
|
-
/** 收紧折叠的入参:`TaskSpec` 的字段,`allowedTools` 除外(型上禁)。 */
|
|
15
|
-
export type GovernanceOverrides = Partial<Omit<TaskSpec, "allowedTools">> & {
|
|
18
|
+
/** 收紧折叠的入参:`TaskSpec` 的字段,`allowedTools` 与 `permissionMode` 除外(型上禁)。 */
|
|
19
|
+
export type GovernanceOverrides = Partial<Omit<TaskSpec, "allowedTools" | "permissionMode">> & {
|
|
16
20
|
readonly allowedTools?: never;
|
|
21
|
+
readonly permissionMode?: never;
|
|
17
22
|
};
|
|
18
23
|
/** 本仓唯一的收紧折叠口(deny-wins 合并;core 在放宽时抛)。 */
|
|
19
24
|
export declare function tightenGoverned(base: TaskSpec, overrides: GovernanceOverrides): TaskSpec;
|