import http from "node:http"; import type { IncomingMessage } from "node:http"; import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type EntryProvenanceAccount, type MemoryErasureAttestation, type MemoryEntryOrigin, type OriginClearanceRow, type SessionMemoryStatus, type ConsolidationRunReceipt, type ConsolidationDriverRunRow, type SideQuerySpec, type SideQueryResult, type Brain, type EngineNotice } from "@sema-agent/core"; import type { TaskRequestBody } from "./wire-types.js"; import type { ServiceConfig } from "../config-types.js"; import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js"; import { type OwnerAwareSessionStore } from "../security.js"; import type { RunStore, ImageIndex, ImageBake, CheckpointStoreFull, ResumeAnchorStore, ApprovalExemptionStore, ApprovalNonceStore, ServiceSessionPolicyStore, ServiceFileHistoryStore, ToolResultStoreFull, StoreBackend } from "../plugins/store-backend.js"; import type { RuleConsentLane } from "../rules-consent.js"; import { type MemorySyncRequest, type MemorySyncResponse } from "../memory-sync.js"; import type { TaskAttachmentStore } from "../plugins/task-attachment-store.js"; import type { LeaderEndpoint } from "../leader/endpoint.js"; import { type WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js"; import type { SubagentSteerRegistry } from "../orchestration/subagent-steer.js"; import { type WorkflowCompletionInbox } from "../orchestration/workflow-completion-inbox.js"; import type { FleetLeaseManager } from "../fleet-lease.js"; import { type FleetEventBus } from "../fleet/fleet-bus.js"; import type { ElicitationCoordinator } from "../elicitation.js"; import type { QuestionCoordinator } from "../question.js"; import { type ParkedAskRedeem, type ToolApprovalCoordinator } from "../tool-approval.js"; import type { ApprovalAskAuditReadFace, ApprovalLateSettleSink } from "../approval-ask-audit-store.js"; import type { PlanCacheProbe } from "../plan-cache-probe.js"; import type { Logger } from "../observability/logger.js"; import type { Metrics } from "../observability/metrics.js"; import { type RateGate } from "../observability/rate-limit.js"; import type { QuotaTracker } from "../observability/cost-quota.js"; import type { ModelUsageTracker, PromptManifestTracker } from "../budget.js"; import { scopedIdempotencyKey } from "./idempotency.js"; export { scopedIdempotencyKey }; import type { ConsolidationSeatLabel } from "../boot/memory-consolidation.js"; import { streamApprovals, checkQuestionAnswer } from "./routes/approvals-assistant.js"; export { streamApprovals, checkQuestionAnswer }; import { cascadeConfig } from "./run-meta.js"; export { cascadeConfig }; import { coarseStatusForState, errorCodeForExit } from "./routes/images.js"; export { coarseStatusForState, errorCodeForExit }; import { MAX_SKILL_CONTENT_CHARS, MAX_SYSTEM_PROMPT_CHARS, MAX_OUTPUT_SCHEMA_CHARS, validateUserSkills } from "./admission.js"; import { hasServiceAuth } from "./dispatch.js"; import { routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath } from "./route-table.js"; export { routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, hasServiceAuth }; export { MAX_SKILL_CONTENT_CHARS, MAX_SYSTEM_PROMPT_CHARS, MAX_OUTPUT_SCHEMA_CHARS, validateUserSkills }; import { explicitOperatorOk, isOperator } from "./principal-gate.js"; export { explicitOperatorOk, isOperator }; /** Per-request authorization context resolved before building the spec (identity, owned session). */ export interface RequestAuth { principal?: string; sessionId: string; memoryScope?: string; /** 142-S4:the SHAPE-VALIDATED **且已归一(小写)** 的 `body.projectId`(authorizer 的 core * `PROJECT_ID_REGEX` 门通过后折小写回传;[ref]/[ref] 宽读严写)。main.ts 拿它查 config.projects 的 * defaultScopes(memory.scopes 种子)。resume 腿(checkpoint 重建 auth)缺席 — 回落持久化的 * body.projectId(原始提交已验形),那条回落腿自己再折一次小写,两腿查同一个键。 */ resolvedProjectId?: string; } /** * [ref] A8 —— ServiceDeps 的**分组窄接口**。 * * 病:69 个平铺可选字段里 29 个只被单一路由域读、跨 ≥9 域的只有 4 个(config/logger/runStore/metrics)。 * 平铺形的直接后果 = main.ts 一个上千行的装配字面量,键与键之间没有任何「属于同一件事」的编译期表达, * 键落错家/从未接线只能靠源码钉逐个盯(1.300/1.301/1.304/1.307/1.310 五起真缺陷都是这个病族)。 * * 药:七个**窄**接口 + 顶层必填三件。装配点按组构造(每组一个带类型标注的中间 const ⇒ 受 TS 多余 * 属性检查保护),键放错组当场编译红。 * * 兼容形(本批**不动消费点**):`ServiceDeps` = 平铺视图 ∩ 组槽。旧的平铺构造(96 个测试文件 + * index.ts 的嵌入方)逐字不变仍编译通过;新的分组构造由 `flattenServiceDeps` 在 createHttpServer * 入口摊平回平铺视图,server.ts 内的 ~700 处 `deps.x` 消费点原样保留。消费点迁移是后批的事—— * 一步到位需要同批改 ~700 处 src + ~800 处 test 键位,与「先立形、后搬家」相比没有额外收益。 */ /** createHttpServer 的**必填四件**——引擎、部署配置、请求→TaskSpec 的映射、side-query 执行席。 * 四者恒在场,故留在顶层(不进任何可选组):没有它们 server 根本起不来,包一层可选组只会把编译期 * 错误推迟到运行期(本仓 E-HIGH-1 病族的成因就是「可选键从未接线、编译期无声」)。 */ export interface ServiceCoreDeps { runner: Runner; /** * [ref]:`POST /v1/side-query` 的 brain 出口。**不是** `runner.sideQuery` —— core 的 `SideQuerySpec` * 结构性没有 key 座位,经 Runner 那条路走的 side-query 恒用 brain 构造时的网关 key 打**模型自己的** * baseUrl(上游 401 + 凭据外泄形)。装配点 `boot/side-query-lane.ts` 给它一只 per-model key 注入 * 的 brain wrapper,models/roles 与 Runner 同源。 * 🔴 **必填**:可选形会让「装配点忘了接线」退化成静默走回旧路(安全轴上没有静默降级),必填让它编译红。 */ sideQuery: (spec: SideQuerySpec) => Promise; config: ServiceConfig; /** * Map an inbound request body + resolved auth to a full TaskSpec. The SERVICE/deployment owns model * + tools + mcp here (callers only supply content). `auth` carries the ownership-checked sessionId * and the principal-derived memory scope — prefer them over anything in the body. */ /** `opts.leg` (codex R4, appendSystemPrompt 案): "fresh" ⇒ a NEW submit — resolveSpec may fail-loud 400 on * combos that would silently discard caller payload (append rider / outputStyle on an append-less pack). * Absent/"resume" ⇒ a stored-body replay — the same combos drop+warn instead (a 4xx would brick the stored * task). The disposition lives INSIDE resolveSpec because only it derives the effective scenario (principal * ruling + defaultScenario) and holds the center-pack snapshot the provider pick uses — an HTTP-side probe * could disagree with the actual assembly (scenario mismatch / centerPrompts refresh TOCTOU). */ /** `opts.liveStream` ([ref] 件C):这条腿是不是 `POST /v1/tasks/stream` —— **唯一**挂 HITL 活流投递面 * (`question.runWithContext`)的腿。resolveSpec 拿它做逐腿 `interactionPosture` 判据(判据全文见 * `boot/resolve-spec.ts` 的 `declaresInteractivePosture`)。缺席/false ⇒ 无人腿,不声明 posture —— * bg(`POST /v1/runs`)、A2A、resume、cron/wake 一律走这一侧,行为字节不变。 * ⚠️ 它是**腿身份**不是能力表态:绝不从请求体读,只由路由装配点按 URL 定(客户端说了不算)。 */ /** `opts.onNotice` (L-167, core 7.9.1):装配期铸下的、**这条会话**的用户面通告(今天只有一族:请求腿 * MCP 注入被丢弃)。装配跑在通告腿开口**之前**(`prepareSpec` → 路由注册 `registerEngineNoticeLeg`), * 所以它不能在铸点直接 `route()` ——收下来交给调用腿,由腿在开口那一拍连同 `pending` 一起投。 * 缺席(桩)⇒ 只走结构化日志终点,行为逐字不变。 * ⚠️ 7.69.0(R-1)起 **resume 家族也递这一位** —— 四条 resume 腿收敛到 `resolveResumeSpec` 一只 resolver, * 通告口接一次、四腿同得(见 http/server.ts 的 `resolveResumeSpec` 顶注);此处旧注写作「resume 家族缺席」 * 已过期,按新行为改。 */ resolveSpec: (body: TaskRequestBody, req: IncomingMessage | undefined, auth?: RequestAuth, opts?: { leg?: "fresh" | "resume"; liveStream?: boolean; onNotice?: (notice: EngineNotice) => void; }) => TaskSpec | Promise; /** * [ref] 场景 hands lane 选路:按 `resolveSpec` 刚产出的 TaskSpec 取本任务的执行 Runner。 * 装配点(main.ts)把「场景判别位 → Runner 对」的映射整只闭包传进来 —— 本层不解释场景,只取用。 * 缺席 ⇒ 恒 {@link ServiceDeps.runner}(单 Runner 部署形 = [ref] 之前的行为,逐字不变;测试桩即此形)。 */ runnerFor?: (spec: TaskSpec) => Runner; } /** **durable 持久面**:每个键 present ⇔ 对应路由/能力位在场,absent ⇒ 501(诚实缺席), * 绝不静默降级。装配点 = main.ts 的 store backend 开箱段。 */ export interface ServiceStoreDeps { /** Durable async-run registry + event log (S1/S2). Enables /v1/runs when present. */ runStore?: RunStore; /** E18 resume-at: the eventId→entryId anchor map (tidb/pg/local). Present whenever a backend is — resolveSpec * resolves body.resumeAt(eventId)→entryId, the run lifecycle captures per-turn anchors, E21 purges them. */ resumeAnchorStore?: ResumeAnchorStore; /** Per-session approval exemption store — enables decide body `remember:"session"` + the * exemptions list/revoke surfaces on /v1/approvals. The ask-gate probe is wired separately (main.ts). */ approvalExemptionStore?: ApprovalExemptionStore; /** S-280:直连门审批证明的**一次性消费记录**(`plan_review` 的 nonce)。每条 store 车道都供,所以在任何 * `DB_BACKEND` 上都在场;缺席只出现在装配缝(桩 deps)—— 那时直连门的 plan_review **拒**(501,见 * `routes/approvals-assistant.ts` 的消费点),绝不静默受理一份结构上可重放的证明。BFF 门不读这只店。 */ approvalNonceStore?: ApprovalNonceStore; /** Durable-checkpoint store ([ref], core 1.67): durable F4 suspend/resume. When present, the resume * endpoint + suspended-run surfacing are active. Set only when DURABLE_APPROVAL + a TiDB pool. */ checkpointStore?: CheckpointStoreFull; /** E6 durable SessionPolicyStore (operator-tightened per-session tool rules). Enables PUT/GET /v1/sessions/:id/policy * when present; core reads the rules at prepare-time (subtract-only) via RunnerDeps.sessionPolicyStore (wired separately). */ sessionPolicyStore?: ServiceSessionPolicyStore; /** [ref] tool-results 读面([ref] 请求,core [ref] `buildToolResultRef` 到货):`GET /v1/tasks/:id/ * tool-results/:ref` 的数据源 —— 与 RunnerDeps 侧的 offload 店**同实例**(main.ts 的 `toolResultStore`, * 即「durable 后端在场」那一只;无 backend 部署里 Runner 用的内存兜底 `runnerOffloadStore` **不接**这里)。 * 🔴 缺席 ⇒ 路由回 404 而不是 501:501 会向一个无权 caller 暴露「这台机器配没配 durable 店」的部署形, * 与 unknown-ref / 归属不符两臂**同形** 404 才是防 oracle 的收口([ref] 的「另定」项按防 oracle 收紧)。 * 这是本组「absent ⇒ 501 诚实缺席」通例的**唯一具名例外**,例外理由即上一句。 */ toolResultStore?: ToolResultStoreFull; /** [ref] per-edited-file rewind history store. Present ⇒ the rewind capabilities are advertised; core uses it * via RunnerDeps.fileHistoryStore (first-touch tracking + per-turn boundaries + tracked-set restore, ANY * env). Also the 2c history-transfer store (exportHistory/importHistory + blob negotiation faces). */ fileHistoryStore?: ServiceFileHistoryStore; /** D-1 通用文件上传(clay 拍 2026-07-27:独立附件 store,不骑 snapshot blob 面):enables * POST/GET/DELETE /v1/attachments*。缺席 ⇒ 501(与其他可选 store 面同姿)。绑定/物化半场在 * main.ts(resolveSpec + env factory 包装);本面只管上传/取回/删除三动词 + server 权威上限。 */ taskAttachmentStore?: TaskAttachmentStore; /** [ref] 件3d(/decide parked 赎回腿)——与 RunnerDeps/scenarioDeps **同实例**:durable bg * 行店 + boot 期从生产装配(buildScenarios)取的裸 Agent ToolSpec。**这两件**与 checkpointStore * 齐备时,POST /v1/approvals/:sessionId/decide 对 parked 后台子代的 pending checkpoint 走 * claim→execute 赎回腿(src/parked-decide.ts);任一缺席=分支不存在(legacy 腿现状:getCtx * null → 409,诚实不误驱)。 */ backgroundAgentStore?: import("@sema-agent/core").BackgroundAgentStore; /** §0.5 session-ownership: the durable session abstraction (`SessionRepo`-style) the service OWNS — session * IDENTITY/enumeration (E16 list) + lifecycle (E17 fork, E21 history delete) come from HERE, not the * `task_run` runs ledger. When it carries `listSessions`, GET /v1/sessions sources sessions from the session * store (task_run only joins for run preview); `fork`/`deleteSession` enable the fork/delete routes. */ sessionStorage?: OwnerAwareSessionStore; /** * [ref] —— 跨会话车道的**发现席**(core `RunnerDeps.peerDirectory`),与 `RunnerDeps` 侧**同一个对象** * (`boot/stage-10-http-server.ts` 直接递 `runnerDeps.peerDirectory`)。 * * 🔴 HTTP 面**今天不读它的内容**:它在这里的唯一作用是让 `GET /v1/capabilities.peerLane` 那一位 * 「说 yes ⟺ 车道真的挂上了」成为**同一个值**的读取,而不是把那条三合取的装配谓词在路由里**重写一遍** * (重写的那一份必然漂 —— 本仓能力位族的既有病,见 `routes/capabilities.ts` 里 `agentRoster` 的同款注)。 * ⚠️ 精确说法是**值** iff,不是键 present-iff:那一位与同族的 `sessionList` 一样是 `Boolean(...)`, * 本席缺席时键仍在、值为 `false`(`test/capabilities-http.test.ts` 的桩部署那一格逐字钉着它)。 * 「键缺席」只发生在**老版本 worker** 上,那是版本事实,不是本部署的车道事实。 */ peerDirectory?: import("@sema-agent/core").PeerDirectory; /** S8 workflow-run store ([ref]/98): enables GET /v1/workflows (list) + /v1/workflows/:id (detail). * Set when SELF_ORCHESTRATION_ENABLED. Live progress (/v1/workflows/:id/stream) is in-process * (subscribeWorkflow) and works whenever self-orchestration is on, store or not. Runs are owner-gated by * `scope` (= the creating principal). Also drives `capabilities.workflowsList` (= "can durably LIST runs"). */ workflowRunStore?: WorkflowRunStore; /** S-185 车CM:workflow 出身 park 的**有界 join** 索引 —— 与 `workflowRunStore` **成对**才构成第三条 * 车道(索引只回答「去哪条 run 找」,行本体与赎回键仍以 run 的 blob 为准)。与 RunnerDeps 侧同实例 * (boot/workflow-orchestration.ts);任一缺席 = 那条车道不存在(workflow 出身的 park 落 legacy 腿, * 与本批之前逐字同形)。契约与单写者纪律的属主 = orchestration/workflow-agent-session-index.ts。 */ workflowAgentSessionIndex?: import("../orchestration/workflow-agent-session-index.js").WorkflowAgentSessionIndex; /** [ref] journal 读面(PAIR-REVIEW 修车批):GET /v1/workflows/:runId/journal 的数据源——与 runner 侧 * workflowJournalStore 同一实例(main.ts backend.workflowJournal());缺席=501(与 workflowRunStore 同门)。 */ workflowJournalStore?: import("@sema-agent/core").WorkflowJournalStore; /** Sandbox-image-pool index (IMAGE-API-DESIGN.md §2, pool design §8): enables /v1/images* (query + select). * Set when a TiDB pool is present. The bake/publish job API (P2/P3) layers on top later. */ imageIndex?: ImageIndex; /** Sandbox-image-pool BAKE store (IMAGE-API-DESIGN.md §P2): enables /v1/images/bakes* (operator-gated async * bake + resumable SSE + runner claim/ingest + status-monotonic auto-register). Set when a TiDB pool is * present AND `IMAGE_BAKES_ENABLED`. The auto-register path writes into `imageIndex` (so both are wired * together). 🔴 main.ts boot-asserts `operatorPrincipals` is non-empty when this is on (never a * unrestricted-write build-host exec door, §P2.4a). */ imageBakes?: ImageBake; /** P2 ([ref] §7.2): the outcome-fact sink, for the `GET /v1/outcomes` read aggregate. `summary` is * present on the SQL ledgers; a File sink (no query surface) → the endpoint 501s honestly. */ outcomeSink?: import("../plugins/file-outcome-sink.js").OutcomeSink; /** SendUserFile 账本(多租户治理面):scope↔object 映射的读面 GET /v1/sendfile-links。present ⇔ 签发面 * 已配 AND 有 store backend(main.ts 建;写点在两条 send lane 的 withLedgerRecording)。 */ sendFileLedger?: import("../plugins/send-file-ledger.js").SendFileLedger; /** 2c session-sync (P1d) — the WHOLE durable backend, so the `/v1/sessions/:id/sync/*` routes can fan out across * ALL four session subsystems (session entries + fileSnapshot blobs/manifests + sessionPolicy + resumeAnchor) * in the §8 fail-closed atomic order. * 🔴 2026-07-29 纠正:这里原写「…so the routes can call **exportSession/importSession**」——**不成立**。 * 两条路由早已是**流式**实现(PULL=exportSessionManifest + keyset 分页 NDJSON;PUSH=两阶段 staging, * §8 扇出**在路由内自己重写了一遍**,直接调各 store),`grep exportSession src/http/` 零调用点。 * 那两个整包编排器现为 **test-only**(见 src/session-sync.ts 顶注的判定与机器门)。留着这句错指路的 * 代价是:下一个人会以为整包路径还活着,并按它去推断内存/原子性行为(而 importSession 恰是 §1.5 的 * OOM 形)。理由仍成立、结论改了 —— backend 依旧要整个传,因为四个 store 必须同源。 * The individual stores above are wired for the per-feature routes; the sync routes need the backend * itself because they coordinate the four together (a route can't reassemble it from the parts — * `backend.session()`/`fileHistory()`/`sessionPolicy()`/`resumeAnchor()` must come from ONE backend). Present * whenever a durable backend (tidb/pg/local) is; absent on the env-only/no-backend worker (the routes 501). */ backend?: StoreBackend; /** [ref] 车二:**持久化权限规则的同意车道**(core [ref] 三步协议的宿主半场)。 * 在场 ⇔ 规则店真装配(`backend.permissionRule()` 有值)⇒ `POST /v1/rules/cc-import/{prepare,redeem}` * 可用、ask 帧才投 `ruleOffers`;缺席 ⇒ 两口 501、帧上零候选(发一格无处可兑的「不再询问」 * = wire 谎言,比缺席更坏)。 */ ruleConsent?: RuleConsentLane; } /** **进程内活体协调面**(本副本 only,重启即空):HITL 协调器、可 steer 句柄注册表、事件总线、 * 收件箱。跨副本一律不可见——每个消费点都按「不在本副本 ⇒ 409/降级」写,不假装全局。 */ export interface ServiceCoordinatorDeps { /** E23 (shell-host contract): inbound MCP elicitation coordinator (live-only HITL). Set when MCP_ELICITATION_ENABLED. * Wired onto the runner's onElicit seam AND consumed here by `POST /v1/elicitations/:id/respond` + the per-run * ALS context wraps (background/inline-stream/resume). Absent ⇒ the capability is false + no server can elicit. */ elicitation?: ElicitationCoordinator; /** §4④: AskUserQuestion LIVE coordinator. Set when ASK_QUESTION_ENABLED. Wired onto the runner's onQuestion * seam AND consumed here by `POST /v1/questions/:id/respond` + the per-run ALS wraps on the streaming legs. Absent ⇒ * the capability is false + the AskUserQuestion tool headless-defaults (never a live human). */ question?: QuestionCoordinator; /** [ref]/[ref]②: live tool-approval coordinator (core onAsk → CC three-choice card). Set when * TOOL_APPROVAL_ENABLED. Wired onto the runner's onAsk seam AND consumed here by * `POST /v1/tool-approvals/:id/respond` + the per-run ALS wrap on the SYNC streaming leg (live-only by design — * every other leg keeps core's fail-closed headless auto-deny, [ref]⑤/[ref]④). */ toolApproval?: ToolApprovalCoordinator; /** [ref] 裁 (c) 审计半场:local 车道的 File ask **审计**读面(`GET /v1/approvals` 的 additive 键 * `crashConverged` 数据源)。在场 ⇔ `DB_BACKEND=local` ∧ 审批面开着(装配 boot/coordinators.ts); * SQL 车道恒缺席(那里有真 ask 店,[ref])—— 键缺席 = 本部署无此面,`[]` = 有面且无崩溃残账, * 两义与 `livePending` 同律可判别。 */ approvalAskAudit?: ApprovalAskAuditReadFace; /** [ref] 迟到人工结算的**写面**(与 {@link approvalAskAudit} 同一个店实例,两把窄口:读面喂 * `GET /v1/approvals` 的 `crashConverged`,写面只有 `recordLateSettle` 一个动词 —— 铸造/决议/腿闭 * 三点的属主是协调器,路由层拿到它们只会长出第二个写者)。在场判据与读面逐字同源(local 车道 ∧ * 审批面开着);缺席 ⇒ 两条 decide 腿**不写、不抛**,行为与修前逐字相同(SQL 车道恒缺席)。 */ approvalLateSettle?: ApprovalLateSettleSink; /** SVC-5 ([ref] CORE-5 #6): the process-local registry of STEERABLE workflow-agent handles * (`ctx.agentStream`) live on THIS replica. Enables POST /v1/workflows/:id/agents/:label/steer. See * workflow-agent-steer.ts for the handle-visibility seam status (the registry is real + ready; core does not * yet hand the deployment a handle to register, so the route returns steering.not_running until it does). */ workflowAgentRegistry?: WorkflowAgentRegistry; /** C2 (core 1.219): the process-local registry of STEERABLE Task-subagent handles live on THIS * replica (populated by the per-run-leg `onSubagentSpawn` internals sink). Enables * POST /v1/runs/:runId/subagents/:target/steer (`target` = the child's parentToolCallId, or its agentName * when unambiguous). Sync delegations only (core F3 scope: background children have no steer handle). */ subagentSteerRegistry?: SubagentSteerRegistry; /** P1 ①②: the async-workflow COMPLETION INBOX. A finished background workflow's completion * is enqueued here (in main.ts's `deliverWorkflowCompletion`) keyed by the originating session; when that * session next opens a stream (`POST /v1/runs` or a resume), we `drain` it and emit a `workflow_complete` * out-of-band SSE frame the shell turns into a `` (deduped by runId). Present when * self-orchestration is on. */ workflowCompletionInbox?: WorkflowCompletionInbox; /** MF-Fleet (shell-host data contract) — the in-process fleet aggregation bus backing `GET /v1/fleet/stream` * (the live multi-row task/workflow fleet view: status/elapsed/tokens/doneCount transitions pushed over time). * Present when wired in main.ts; the run + workflow lifecycle publish their per-row deltas to it. */ fleetBus?: FleetEventBus; /** [ref] 病理1:boot recover(判死前世 running 行并回灌 fleet)完成屏障——`/v1/fleet/stream` 快照组装 * 对它做**有界**等待(3s cap,routes/fleet.ts),SIGKILL 后壳重连不再与 recover 赛跑。缺席或已 * resolve=立即组装(memory 后端、无 notify gate 的部署);main.ts 铸(recover 收官即落,失败也落)。 */ fleetRecoveryDone?: Promise; /** [ref] session-watch registry(SSE 订阅共享探针面)。present ⇔ sessionStorage 有 getLeafId/ownerOf * (main.ts 建);absent ⇒ /v1/sessions/:id/events 501(capabilities.sessionEvents 同谓词,永不漂)。 */ sessionWatch?: import("../session-watch.js").SessionWatchRegistry; /** asyncRewake wake 管道 holder(main.ts 建壳,server 启动装 deliver=session→本副本活流 steer)。 */ hookWakeBus?: { deliver?: (sessionId: string, text: string) => Promise; }; /** SendUserFile(切片2):per-run file_link 帧发射 ALS;undefined ⇒ 工具未建(非 host lane / 未配签发面)。 */ sendUserFile?: import("../capabilities/send-user-file-tool.js").SendUserFileEmitter; /** v2 leader endpoint ([ref]): enables POST /v1/leader (async) + GET /v1/leader/:id. Set when * LEADER_ENABLED + the leader wiring (e2b workers + git push) is configured. */ leaderEndpoint?: LeaderEndpoint; /** Fire-and-forget session auto-titler (first-submit hook; absent = feature dark). */ sessionTitler?: { maybeTitle(sessionId: string, objective: string, model?: string): void; }; } /** **函数/构件缝**:main.ts 把跨多个子系统的动作编译成一个可调用面交给路由(路由不该自己 * 重组零件——见 backend 的同款论证)。parked-revive 三件是同族的构件缝(boot 期从生产装配取的裸 * ToolSpec + 成员集 + 父约束重建器),与 backgroundAgentStore 齐备才构成赎回腿。 */ export interface ServiceSeamDeps { /** * device lane 的 WS 汇聚端(design/device-executor-lane-v2 §5,车A-3)—— 本仓**首个** WS 服务端。 * 给了就把 `GET /v1/device/ws` 的 upgrade 处理器挂到这台 `http.Server` 上;缺席 = 本部署没有 device * 车道 ⇒ 该路径连 upgrade 都不认(与 `REMOTE_EXEC=device` 的拒启门同一条诚实线)。 * * ⚠️ 排空联动由**装配层**驱动:`drainState.draining` 翻真时,main.ts 须同时调 `deviceHub.beginDrain()` * ——两者刻意不在这里自动耦合,因为 §8-R7 的顺序(新提交 503 → 新 upgrade 拒 → 停发新指令 → * 在途结果照收 → 才断连)是**部署编排**的语义,不是 HTTP 层能自作主张的。 * * 🔴 **当前生产装配恒缺席,这是有意的**(`parkedKnownAgentTypes` 同族形):车A-3 只交汇聚端与这条 * 挂载缝;`main.ts` 侧的真装配(建店 → 建 hub → 传这个键 → 与 `drainState` 联动 → 与 * `REMOTE_EXEC=device` 拒启门对齐)属**车A-4/端点车**。在那之前本键只有测试口消费,不是死枝。 */ deviceHub?: import("../device-ws-hub.js").DeviceWsHub; /** * O4/S-4([ref]):device lane 四表店 —— `/v1/devices/*` 管理面(routes/devices.ts:列表投影 / * 绑定读面 / 显式换绑动词)的持久真源。main.ts 与 {@link deviceHub} 同源装配 * (`deviceLane?.store`,`REMOTE_EXEC=device` 时在场);缺席 ⇒ 该族路由 501 * `capability.device_lane_required`(管理面在场性由路由自身答 —— capabilities 刻意不加新位, * v2 换绑稿 §7)。 */ deviceStore?: import("../device-store.js").DeviceStore; /** * S-470(车C 余件):device lane 的**准入链**(`createDeviceEnrollment`)—— 管理面三个写动词 * (签发 / 注册 / 吊销)与首绑写协议(`commitPlacement`)的**唯一**判据属主。 * * 🔴 为什么路由拿的是它而不是 {@link deviceStore}:enrollment 那一层持着两件路由造不出来的东西 —— * ①跨副本限速座(`DeviceEnrollRateLimiter`,§7-T5 ⑤:端点车**必须**传一个,而它由 boot 从既有 * `RateGate` 适配);②反枚举拒绝的**单一铸造点**(四种 enrollment 失败理由在 wire 上逐字节同形)。 * 路由若绕过它直接打店,那两件就会在路由里被手抄第二份 —— 而手抄的那份迟早漏一臂。 * * 与 {@link deviceStore} / {@link deviceHub} **同源装配**(`boot/device-lane.ts` 一处建齐三件), * 所以三者的在场性是同一个事实;族内 501 谓词因此可以是「三件齐」这一句话。 */ deviceEnrollment?: import("../device-enrollment.js").DeviceEnrollment; /** Audit回溯: current context (+ summary) for a session, plus its owner. Enables GET /v1/sessions/:id. */ sessionAudit?: (sessionId: string) => Promise<({ owner: string | null; } & Record) | undefined>; /** E21 session delete coordinator: purge a session's history (sessionStorage) PLUS the service-owned * run-ledger/checkpoint/tool-result rows, owner-guarded, in one call. Returns `{ deleted }` (idempotent) — or * `{ active }` when the runs-ledger TX re-asserted a still-live run (a concurrent createRun raced the route's * pre-check), which the route maps to 409. Enables DELETE /v1/sessions/:id. Wired by main.ts (all the stores). */ purgeSession?: (sessionId: string, owner: string | null) => Promise<{ deleted: boolean; } | { active: string; }>; /** [ref]③(b): read a BACKGROUND agent-handle child's final report / current status out of the core * TaskRegistry (the same face the TaskOutput tool reads — agent handles are NEVER in the run store). * Wired in main.ts to `defaultTaskRegistry.pollTask` (non-blocking). Access is the REGISTRATION tuple: * owner = the parent run's taskId (resume legs register children under the sessionId — the registry's * sessionScoped arm matches via `access.sessionId`), scope = the run's principal ?? "default". */ subagentTaskOutput?: (handle: string, access: { owner: string; scope: string; sessionId?: string; }) => Promise<{ content: string; details: unknown; }>; /** [ref] CC TaskOutput 人侧对位 — the GENERIC task-handle read (background_bash cursor-consuming stdout / * monitor batches / background_agent final report; `workflow` refused at the seam — journal face owns it). * Same registration-tuple access derivation as subagentTaskOutput. Wired in main.ts to the seam over * defaultTaskRegistry.pollTask. */ taskHandleOutput?: (handle: string, access: { owner: string; scope: string; sessionId?: string; }) => Promise<{ content: string; details: unknown; }>; /** [ref] CC TaskStop 人侧对位 — stop a background task handle (bash kill / monitor stop / agent abort), * attributed "user". Same kind gate + access derivation as taskHandleOutput. */ taskHandleStop?: (handle: string, access: { owner: string; scope: string; sessionId?: string; }) => Promise<{ content: string; details: unknown; }>; /** 142-S5 §1.4: per-scope memory export reader (main.ts wires `exportMemoryScope` over the DB memory * backend). Enables GET /v1/memory/export — present ONLY on a DB memory plane (MEMORY_ENGINE_BACKEND= * pg|tidb); absent / file posture ⇒ 501 (single-user file memory exports by copying the memory dir). */ memoryExport?: (scope: string) => Promise; /** * [ref](core 5.57.0 `routePairingStatus`)—— **key↔URL 配对读面**:`GET /v1/models` 每行的 * `routePairing` 标注就是拿这一只判的。main.ts 接的是**装配好的那只 brain 本身**(不是摘出来的 * 函数引用:[ref] 那条「裸摘引用丢接收者」的教训同款,且 core 的装饰器族是按 `stream` 的同一条 * 路再分派 `adjudicateRoute` 的,整只传才不会跳过路由/快切层)。 * * 🔴 缺席 ⇒ 该面每行标 `"unknown"`,**绝不猜**:一只没有 `adjudicateRoute` 面的自定义 brain 是 * core 明令「callers must treat that as 'no judgment', never as OK or as broken」的形。测试夹具 * 不接这一件时读面字节逐字不变(标注仍在,值恒 `"unknown"`)。 */ routeJudgeBrain?: Pick; /** 142-S2.5(设计 §2.5;裁定 plan 随数据走,core 零运行时半场):one full sync round for one * (scope, peer) — main.ts wires `performMemorySync` over the DB memory backend + the sync-cursor * store. Enables POST /v1/memory/sync/:scope — present ONLY on a DB memory plane * (MEMORY_ENGINE_BACKEND=pg|tidb); absent / file posture ⇒ 501 honestly (a single-user file plane * IS the TOC side of a sync — it has no central-authority half to serve). */ memorySync?: (scope: string, syncReq: MemorySyncRequest) => Promise; /** [ref] v2-c(core 5.40.0 `MemoryEngine.exportMemoryScopes`):治理完备包的**携出**面。main.ts 把它接到 * 装配好的 memory engine 上。启用 `POST /v1/memory/export`(operator-only)。 * 🔴 缺席 = 这个部署**没有记忆引擎**(`MEMORY_ENGINE=off` / 多租 file 底座等暗态)⇒ 501 * `capability.memory_engine_required`。注意与 {@link memoryExport} 的**方向相反**:那条要 DB 记忆 * 平面,本条只要引擎在场 —— 而 bundle 复合面(`exportSnapshotOf`)今天只有 core 自带的 File 后端 * 实装,SQL 孪生上引擎会自己响亮拒(`memory.export_incomplete` 原样上 wire)。理由全文见 * routes/memory-bundle.ts 的「能力面的诚实形」。 */ memoryBundleExport?: (scopes: readonly string[]) => Promise; /** [ref] v2-c(core 5.40.0 `MemoryEngine.importMemoryBundle`):治理完备包的**导入**面(治理写面)。 * 启用 `POST /v1/memory/import`(operator-only)。缺席语义与 {@link memoryBundleExport} 逐字相同 —— * 两口共用一个能力判据,因为它们是同一个引擎面的两半(一个部署不可能只有其中一半)。 */ memoryBundleImport?: (bundle: unknown, opts?: { expectedScopes?: readonly string[]; }) => Promise; /** [ref] 件③(core 5.57.0 `MemoryEngine.provenanceOf` / `eraseMemoryEntries`):出处 / 抹除合规面。 * 启用 `GET /v1/memory/entries/:entryId/provenance` 与 `POST /v1/memory/erase`(两口 operator-only)。 * main.ts 把它接到 `createMemoryComplianceFaces` 的产物上。 * 🔴 缺席有两个成因,与 {@link memoryBundleExport} **同码不同判据**(同码是因为消费端分支相同:换部署 * 形态;判据不同是因为要查的旋钮不同):①记忆引擎没接线;②引擎接了但后端**不自带控制面归属** * (`controlPlaneRoot`)——本仓两只 SQL 记忆孪生即此形,core 那时会把控制面落到副本本地盘上,与 * stateless replicas 正面冲突。②在**挂载期**判掉(工厂返 undefined),理由全文见 * src/memory-operator-faces.ts 头注。 * 🔴 `erase` 的入参是 `unknown` 而不是 `EraseMemoryEntriesInput`:HTTP 层拿到的是一个**未校验**的对象, * 三选一选择子的整体判决是 core 的单一属主面(`erasureRequestInvalid`)。写成具体类型就得在路由里断言 * 一次「它是合法输入」——那句断言是假的,而且正好把类型检查关灯。 */ memoryCompliance?: { provenanceOf: (entryId: string) => Promise; erase: (input: unknown, opts?: { allowedScopes?: readonly string[]; }) => Promise; }; /** [ref] seam①([ref] §S-7,core 7.0.2 [ref] 件1):**会话记忆状态读面**。启用 * `GET /v1/sessions/:id/memory-status`(owner 门,tenant 自助读 —— 不是 operator 面)。 * main.ts 把它接到 `createSessionMemoryStatusFace` 的产物上(单实例引擎,SQL capture 店同座喂入)。 * 🔴 缺席的两个成因与 {@link memoryCompliance} **同码**(`capability.memory_engine_required`,消费端分支 * 相同:换部署形态):①记忆引擎没接线;②引擎接了但后端**不自带控制面归属**(`controlPlaneRoot`)—— * committedCount/foldedCount/lastCaptureAt 读的是引擎控制面的 lineage 台账,控制面落在副本本地盘的 * 部署上,一个「可读的空目录」会把三键铸成**说谎的 0**(可读零必须是真零),所以②在挂载期判掉 * (工厂返 undefined),与 compliance/origin 两族同一条 F-9 裁。 * 🔴 face 的五键**全可选**(core `SessionMemoryStatus`):absent = 该事实源不可读,路由逐键透传缺席, * 恒不铸 false/0 替身;`optOutSource:"fault"` = capture 态 indeterminate(383 片2/3 轴直投)。 */ sessionMemoryStatus?: { status: (sessionId: string) => Promise; }; /** * S-403(core 7.22.0 [ref] W1)—— 本**部署**的自动整理武装事实,`GET /v1/sessions/:id/memory-status` * 的 `autoConsolidationArmed` 格。 * * 🔴 **唯一读法**:值必须由 core 的 `autoRunOnRecommendationEffective(RunnerDeps)` 组 * (契约 `memory.auto_consolidation.arming_has_one_resolver`),在 **boot 期算一次** * (`boot/stage-10-http-server.ts` 单点)。`RunnerDeps.autoRunOnRecommendation` 的**缺席不是「关」** * (core 7.21.1 起具名缺省是开,且只在 `memoryConsolidation` ∧ 可解整理模型席都接线时才真武装)—— * 所以「按席在不在场判」与「按席值判」**两个方向都错**,本仓不自铸第二套派生。 * 🔴 **必填,不是可选**:它是一个纯函数在 boot 期的答案,任何装配点都算得出;做成可选就等于在 * wire 上多出一个「答不出」的第三态,而这一格读错的方向是**把一台正在外流记忆的部署报成没在外流**。 * 装配漏接 ⇒ tsc 红(本行就是通知);测试桩在 `test/helpers/boot.ts` 显式给 false。 */ memoryAutoConsolidationArmed: boolean; /** [ref] 件②(core 5.59.0 `MemoryEngine.listExternalOriginEntries` / `listOriginClearances` / * `clearEntryOrigin`):**外源标记人面**。启用 `GET /v1/memory/origin/external`、 * `GET /v1/memory/origin/clearances`、`POST /v1/memory/origin/entries/:entryId/clear`(三口 operator-only)。 * main.ts 把它接到 `createMemoryOriginFaces` 的产物上。 * 🔴 缺席的两个成因与 {@link memoryCompliance} **逐字同一条**(同码 `capability.memory_engine_required`, * 消费端分支相同:换部署形态):①记忆引擎没接线;②引擎接了但后端**不自带控制面归属** * (`controlPlaneRoot`)。②在**挂载期**判掉(工厂返 undefined)。这一族的理由比合规两口更硬:清标的 * 写前托管行(`OriginClearanceRow.entryText`)在墓碑已落、重录未成的崩溃窗内是那条记忆**唯一的一份** * ——落在副本本地盘上 = pod 重建即永久丢失。全文见 src/memory-operator-faces.ts 头注。 * 🔴 `listClearances` 是**同步**的(core 的读面就是一次严格 sidecar 读),且账本损坏时**抛** * `ControlPlaneCorruptError` —— 那是 fail-closed 信号,不是空数组。 */ memoryOriginFace?: { listExternal: (scopes: readonly string[]) => Promise>; listClearances: () => OriginClearanceRow[]; clear: (entryId: string, input: { requestId: string; reason: string; }) => Promise<{ entryId: string; clearanceId: string; origin: MemoryEntryOrigin; landedSlug: string; }>; }; /** [ref](core 5.58.0 `runMemoryConsolidationDriver`):记忆 **consolidation 阀门**的操作面。 * 启用 `POST /v1/admin/memory/consolidation/run` 与 `GET /v1/admin/memory/consolidation`(两口 operator-only)。 * main.ts 把它接到 `createMemoryConsolidationFaces` 的产物上。 * 🔴 缺席的**唯一**成因是「阀门没开」(`MEMORY_CONSOLIDATION_DRIVER` 不是 `on` —— 出厂缺省): * 阀门开着却结构上跑不了的三种形(引擎没接线 / 后端没有控制面归属 / `MEMORY_PROVENANCE=off`)在 * **启动期**就被拒了(`boot/memory-consolidation.ts`),那些机器根本起不来。⇒ 缺席形是 **404 族** * (整域不挂),刻意**不是** 501:501 说「换部署形态」,这里的动作是「把旋钮打开」。全文见 * `routes/memory-consolidation.ts` 头注。 * 🔴 `seat` 的词表单一真源 = `boot/memory-consolidation.ts` 的 {@link ConsolidationSeatLabel} * (type-only import,tsc 擦除、非装载边)——在这里再写一遍字面量联合就是第二份会漂的词表。 */ memoryConsolidation?: { readonly seat: ConsolidationSeatLabel; readonly model: string; readonly scopes: readonly string[]; run(scope: string): Promise; lastRun(scope: string): ConsolidationDriverRunRow | undefined; }; /** [ref] §3.1 / [ref] §1.3([ref] S-2):per-principal memory-capture opt-out 授权表的管理面 * (`/v1/admin/memory-optout` 四口,operator-only)—— `backend.memoryOptOutGrant()` 的同一只(与 * `RuntimeCaps.allowMemoryOptOut` 的 resolver 腿读同一张表,零缓存直读 ⇒ 写后下一次 prepare 即生效)。 * 缺席 = 本部署没有 SQL 后端(local 车道刻意无源)⇒ 四口 501 `capability.memory_optout_grant_required` * (「换部署形态」族;与 consolidation 阀门的 404 分家:这里不是旋钮没开,是店在本部署上不存在)。 */ memoryOptOutGrant?: import("../plugins/memory-optout-grant-store-sql.js").MemoryOptOutGrantStore; /** [ref] 件A §7 —— org 记忆授权目录,`boot/org-memory.ts` 装配的**同一个实例**(准入 seam 与 * 本面共享它的 TTL 缓存/退避窗/gen 高水位)。memory-policy 面的 `org:` 属主门用它回答成员性: * 读面要该 scope 在授权表里、写面另要条目 `write === true`。缺席=本部署没有目录源 ⇒ `org:` 键 * 逐字保持 operator-only(收编前行为),绝不因目录不接线而放宽。 */ orgMemoryDirectory?: import("../org-memory-admission.js").OrgMemoryDirectory; /** [ref] —— org 共享记忆库的供给面(与 core `RunnerDeps.sharedMemoryStores` **同一个实例**: * HTTP 只读面看到的库集合按定义等于模型面看到的那一份,两面不可能分歧)。 * 🔴 缺席 ⇒ `/v1/shared-memory/*` 整域不挂载(诚实缺席);挂载还另要 {@link orgMemoryDirectory} * 在场 —— 没有成员性判据的读面只能全放或全拒,两个都比「这个面不存在」差。 */ sharedMemoryStore?: import("@sema-agent/core").SharedMemoryStoreProvider; /** WEB-SETUP-RECON 附B — org sessionMirror 治理态的 SERVICE 半场 = **观测面(audit),非执法面**。 * 裁定 (c):「该镜像而未镜像」发生在数据产生端(壳)——本 server 是镜像**目标**,只看得见到达的 push; * 没到达的会话对 server 不存在(无客户端会话清单、无心跳契约),required 锁的真执法只能在壳(CC * managed-settings 心智;registry-core 域头注同判)。server 能诚实观测的最小面 = 镜像 PUSH 到达时该 * principal 的治理态(required + 指定 engineUrl)入 audit 日志/metrics,供运营关联「治理生效 vs 到达流」。 * per-principal 查询走 caps client 的 executionRuling 同车(零额外 RTT);absent = 无 center(单用户/ * dry-run)⇒ 观测面不接线,零行为差。fail-open:观测失败绝不影响 sync 面本身。 */ sessionMirrorRuling?: (principal: string | undefined) => Promise; /** Degenerate-output a/b instrument: on errorCode=output.degenerate, inspect the session's turns to * classify where the salvageable answer was (counter + log). Fire-and-forget; no-op otherwise. */ instrumentDegenerate?: (result: TaskResult) => void; /** [ref] side-query 记账 seam(budget.ts createSideQueryAccountant——与 createTracer 同一装配点、 * 同一组 sink 实例:costQuota/model_cost 指标/fleetUsage/fleetLease,四路同语义)。缺席=零记账 * (手搭测试 deps;生产 main.ts 恒传)。 */ sideQueryAccounting?: (principal: string | undefined, r: { model: string; family?: "input-includes-cached" | "input-excludes-cached"; usage?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; cost?: { total?: number; }; }; }) => void; parkedReviveTool?: import("@sema-agent/core").ToolSpec; /** 可选第三件:revive args 的 `subagent_type` roster 成员集([ref] Q1 成员性规则)。当前生产装配 * **恒缺席**(boot 内建 default 无自定义 agents roster,per-request 的 spec.agents 不进 boot 工具) * = 恒省略 subagent_type,typed 子代按 general-purpose 形复活(保真度 caveat 已报 core,[ref] * 批复审 G-F1;center roster 接线是候件,字段先留给它与测试口)。 */ parkedKnownAgentTypes?: ReadonlySet; /** [ref]/[ref] 跨进程父约束重供:decide 腿按部署配置重建 host 任务的同语义父策略链(exempt 锚= * row.rootSessionId),经 core 1.396 的 `parkedResume.inheritedGate` 席位透传——缺席时重启后带 * requiresParentConstraint 的 parked checkpoint 恒被 core pre-CAS 门诚实拒(不可赎回)。 * ⚠️ **async**([ref]):重建要按本行 principal 现解 entitlement——理由在 parked-decide.ts 的 * `rebuildInheritedGate` 头注。 */ parkedReviveInheritedGate?: (row: import("@sema-agent/core").BackgroundAgentRecord, identity: { principal: string | undefined; }) => Promise; } /** **可观测面**:日志/指标/累加器。全体 fail-open——观测失败绝不改变业务面行为。 */ export interface ServiceObservabilityDeps { /** Structured logger (access log + errors). */ logger?: Logger; /** Metrics registry; when present, exposed at GET /metrics (token-gated). */ metrics?: Metrics; /** E8 (shell-host contract): per-task × per-model usage accumulator fed by the cost tracer. Shared with `createTracer` * so the durable run lifecycle drains it into `model_usage` events → the `TaskStats.modelUsage` echo. */ modelUsage?: ModelUsageTracker; /** [ref]②: the shared prompt-manifest accumulator (tracer records at prepare; the durable legs drain it * into `prompt_assembled` events for turns/stream). Same register-fence lifecycle as modelUsage. */ promptManifests?: PromptManifestTracker; /** Plan-cache recurrence probe (core [ref]): per-scope objective recurrence rate, dumped at * GET /metrics/plan-cache. Pure in-memory; recorded per finished task on both result paths. */ planCacheProbe?: PlanCacheProbe; } /** **准入/治理面**:身份解析与配额/限流/租约。每一件都是**拒绝**的权力(400/403/429), * 缺席 = 该道门不存在(不是「默认放行的门」——两者在事故复盘里语义完全不同)。 */ export interface ServiceGovernanceDeps { /** * Resolve identity + the owned session BEFORE the spec is built (S6). Throw `HttpError` to reject. * Optional: when omitted, no principal/ownership enforcement (the body's sessionId is used as-is). */ authorize?: (ctx: { req: IncomingMessage; body: TaskRequestBody; }) => Promise; /** 鉴权桥(registry-core api/auth-bridge):registry 用户 JWT → principal。缺席=分支不存在 * (bearer 只认 SERVICE_AUTH_TOKEN(S),现状不变)。 */ registryJwtVerifier?: { verify: (bearer: string) => Promise; }; /** Per-principal rate limiter for task/run endpoints. In-memory (single replica) or TiDB-backed * (cross-replica soft limit) — both satisfy RateGate. */ rateLimiter?: RateGate; /** Per-principal cumulative cost quota (refuses a tenant's next task once over for the window). * In-memory (single replica) or TiDB-backed (cross-replica) — both satisfy QuotaTracker. */ costQuota?: QuotaTracker; /** D4 AP:worker 侧 quota-lease admission(协商式;center lane 未配 = 缺席 = 零行为)。 */ fleetLease?: FleetLeaseManager; /** [ref]-T1:治理窗账本店(USAGE_WINDOWS 配置时在场,与 config.usageWindows 成对)——提交面 * pre-admission 读窗用(主力 /v1/runs 是 202 异步,core 进场拒发生在 202 之后永远变不成 429; * 这里是异步车道唯一的 429 出路)。enforcement 真源在 core(RunnerDeps 同店),此门是快速座。 */ usageWindowStore?: import("@sema-agent/core").UsageWindowStore; /** [ref] ④([ref]/[ref],core [ref] 合修):auto 模式自查读面的三件装配事实(席位在不在 / 合成后的 * per-principal caps resolver / 分类器配置路由)。`/v1/capabilities.permissionModeAuto.{armed,reason,model}` * 只消费它,不自算 —— 席位与 resolver 都必须是递给 `RunnerDeps` 的**同一只**(main.ts 装配),否则又是 * 「诊断说亮、位说暗」。缺席(桩/一次性 CLI)⇒ 读面如实答 `deployment_incapable`。 */ autoModeFace?: import("../auto-mode-face.js").AutoModeFaceDeps; } /** **部署自述面**:只被 /health、/v1/capabilities 与提交前置门读的部署事实(多为 live getter, * 不是快照——/health 必须回答「此刻」)。 */ export interface ServiceDeploymentDeps { /** Stable per-process id recorded on runs (audit / stale recovery). */ instanceId?: string; /** Deployment-level capability facts merged into GET /v1/capabilities: e.g. * scenarios list, version, pricingConfigured. The store-derived booleans (asyncRuns/artifacts/approvals/ * leader) are computed here from the deps — this bag is for what only main.ts knows. Open set. */ capabilities?: Record; /** 场景详情只读面(GET /v1/capabilities/scenarios/:name 数据源):main.ts 在 boot 组装 * (内建 details+center 条目同判定源盖同名)。absent = 404(与 capabilities 缺席同姿势)。 */ scenarioDetails?: Record; /** Honest /health `sessionBackend` label — the wiring coerces the config enum to "tidb" * (meaning "durable, generically"), which on a pg deployment reads as SQL-TiDB and misleads triage. * main.ts passes the effective store label (`durable(pg)` / `durable(mysql)` / `durable(local)` / "memory", * same derivation as the boot `listening` log). Absent → fall back to the raw config enum. */ sessionStoreLabel?: string; /** S5 (SILENT-FALLBACK P0-b): true when the boot auto-probe degraded the store backend to in-memory. */ storeDegraded?: boolean; /** * Graceful drain — shared mutable state between main.ts (flips `draining` on SIGTERM) and this server: * • while `draining`, NEW model-running submissions (the billable submit paths) get 503 + Retry-After, so an * orchestrator/shell retries against the replacement instance; read paths + in-flight verbs (steer/cancel/ * respond/subagent) stay open so the runs we are waiting on can still finish/interact. * • `inflight` is ASSIGNED BY createServer (a live union of this replica's in-flight legs: durable bg/resume * `inflightRuns` + live sync/resume streams `steerableRuns`) — main.ts polls it to know when drain is done. * • /health carries `draining:true` (k8s readiness 摘流信号) + `version` (build self-description). * • [ref]:`attachedStreams` 同样 ASSIGNED BY createServer —— 本副本当前打开着的 SSE 流条数 * (= 引擎附着租约集的大小)。消费方是 `boot/engine-lease.ts` 的自退判据;与 `inflight` 同族, * 同一个共享盒子,不另开第二条 main↔server 的通道。 */ drainState?: { draining: boolean; since?: number; reason?: string; inflight?: () => number; lastActivityAt?: () => number; attachedStreams?: () => number; lastAttachEndedAt?: () => number; }; /** boot ready 门(b):false = registry 部署无显式 env 模型且首次 effective pull 尚未落 roster * (worker 只有占位模型)。计费提交 503 + /health 加性 `ready:false`。absent = 恒 ready(env 模型在/非 * registry 部署),现有部署零影响。main.ts 在 applyEffective 落模型后翻 true(one-way latch)。 */ modelReady?: () => boolean; /** Sema-registry restart signal: the refresh loop sets this when a pull carries restart-to-apply * changes (skills/mcp/scenarios/runtime-gates baked at boot) that DIFFER from this process's boot config. * Surfaced at GET /health as `restartRequired` + `restart{reasons,version,since}` so the orchestrator can * auto rolling-restart the worker. Change-detected (not presence) — absent when nothing baked-at-boot * changed, so polling it never causes a restart loop. */ restartState?: () => RestartSignal | undefined; /** [ref] 世代账([ref] §2):本副本的配置收敛状态 —— `/health` 取三键 * (configTargetVersion / configAppliedVersion / configApplyStaleMs),`GET /v1/diagnostics/wiring` 取整份 * (按组的 appliedVersion / deferredKeys+铸因 / lastRejected 消毒形)。缺席 = 这个进程没有配置管道 * (纯 env 部署 / 测试夹具)⇒ 两个面的形状逐字不变(additive)。 */ configApplyState?: () => import("../config-center/apply-ledger.js").ConfigApplyLedgerView | undefined; /** [ref] 追加件:`POST /v1/admin/config/refresh` 的实现面 —— 触发**一次既有**的 refresh 拍(在飞则 join)。 * 缺席 / 回 `"not-wired"` = 本部署没有配置管道 ⇒ 该口诚实 4xx(绝不吞成 200)。 */ configRefreshNow?: () => Promise<"triggered" | "not-wired" | "failed">; /** DESIGN-278 §5 S2([ref]):center **当前在管**的 ServiceConfig 键集 —— `GET /v1/config/catalog` * 判 `effectiveLane`(env 占位/center 在管/默认)的 center 半场。缺席或返回 undefined = 本部署 * 没有配置管道(纯 env worker / 测试夹具)⇒ 目录把每条 center 腿如实标「不在管」(additive)。 */ configCenterManagedKeys?: () => ReadonlySet | undefined; /** [ref](core 5.50 [ref]):mid-turn MCP 撤销台账读口 —— GET /v1/sessions/:id/mcp 的展示面与 * 引擎执行面同一权威(被撤服务器的工具不再进面板)。缺席 = 无中心撤销面(pre-338 展示语义)。 */ mcpRevocations?: { isRevoked(serverName: string): boolean; }; /** [ref] 诊断投影:当前被撤销的 MCP 服务器名单(`GET /v1/diagnostics/wiring` 的 mcpRevocations 段)。 * 缺席/返回 undefined = 无配置管道 ⇒ 读面报 null(additive,不铸空壳)。 */ mcpRevocationState?: () => { revoked: string[]; } | undefined; /** codex R18 (models-tiers deferral): the persistent "deferred WITHOUT a next-boot handoff" state — set when a * changed model plane could not hot-apply (tier-frozen Runner / tiered candidate) AND no restart-surviving LKG * exists to justify forcing a restart. Surfaced on /health (`modelPlaneDeferred`) so the stuck-degraded state * outlives the one-shot warn (the etag advances → later ticks 304 and never re-enter the defer branch). */ planeDeferredState?: () => { version: number; since: number; blocked?: string[]; } | undefined; /** B1(鲁棒性批3 设计件):后台**缓存**探针的读座——/health 绝不逐请求打 DB(center/k8s 高频面), * main.ts 只在 SQL 后端(backend.kind !== "local")挂环(createStoreLiveProbe,STORE_PROBE_INTERVAL_MS)。 * 缺席(local/memory/探针关)= /health 形状不变;接线 = `storeProbe{live,ageMs,error?}` 恒在 * (「探过且活」与「没接线」机读可分),顶层告警键 `storeLive:false` 只在死时出现。status 恒 "ok" * ——liveness≠readiness,DB 死不是进程死;摘流语义留给读键的编排器(与 durable/ready 同姿势, * 披露不代裁)。钉:test/health-store-live.test.ts。 * 🔴 [ref]:座席给的 `error` 是**驱动原始 message**(DSN/主机/账号常在其中),而 /health 在鉴权门 * 之前 ⇒ 路由把它过 `publicStoreProbeError` 换成闭集词才上线;原文只走日志轴。 */ storeLiveState?: () => { live: boolean; ageMs: number; error?: string; } | undefined; /** [ref] 件三:core `describeStaticWiring(deps, specTemplate)` 的 boot 产物 —— `GET /v1/diagnostics/wiring` * 的 `static` 半场,**与启动期拒启自检读的是同一份**(诊断页与拒启判据结构性同源)。 * operator-only 面,故整份原样下发(含 `governance` 段:本面就是那个段的受众)。 * 缺席 = 这个进程不是由 composition root 装起来的(测试夹具形)⇒ 该路由诚实地不存在(404)。 */ staticWiring?: import("@sema-agent/core").WiringManifest; /** [ref] 件2/件3:记忆面姿态(boot/stores.ts 的 `buildMemoryPosture` 产物 —— 与启动日志同一份)。 * 两个读面共用:`GET /v1/diagnostics/wiring` 的 `memoryPosture` 段(operator-only,全量)与 * `GET /v1/capabilities` 的 `memoryEngine` 位(窄投影)。缺席 = 这个进程不是由 composition root 装 * 起来的(测试夹具形)⇒ 诊断面报 null、能力位取诚实下限 false。 */ memoryPosture?: import("../memory-posture.js").MemoryPosture; /** S-131:SQL 引擎姿态的 **live getter**(`sql-engine-posture.ts` 的单一推导点 —— boot 期借一条池连接 * 触发驱动的连接初始化,读的是 `SET` 之后**回读复核**过的真值)。两个读面共用: * `GET /v1/diagnostics/wiring` 的 `sqlEngine` 段(operator-only,全量含版本)与 `GET /v1/capabilities` * 的 `sql` 位(窄投影)。getter 而不是快照:池重连后驱动会重新初始化并刷新事实,快照会说谎。 * 缺席 / 返回 null = 本部署没有 SQL 后端(env-only worker、local 文件后端)—— 诚实缺席,不铸空壳。 */ sqlEngineFacts?: () => import("../plugins/sql-driver.js").SqlEngineFacts | null; /** S-138:**写保护名表**的姿态(`write-protection.ts` 的 boot 期单一推导点 —— 与递给每只 Runner 的 * `writeProtectedPaths` 座同一份 config 决定)。两个读面共用:`GET /v1/diagnostics/wiring` 的 * `writeProtection` 段(operator-only,**逐行**表 + 来源 + 被丢的缺省行名)与 `GET /v1/capabilities` * 的 `writeProtection` 位(窄投影 `{armed, rows, replaced}`,**不含行内容**)。 * boot 期算一次的**存活对象**(纯 config 派生,运行期不变;core 座没有热改腿)。 * 缺席 = 这个进程不是由 composition root 装起来的(测试夹具形)⇒ 诊断面报 null、能力位报 null。 */ writeProtection?: import("../write-protection.js").WriteProtectionPosture; /** S-382:**部署侧 WebSearch 后端**(`WEB_SEARCH_PROVIDER`)—— `GET /v1/capabilities` 的 `webSearch` 位的 * 唯一席位。值**逐字**取自 boot 期真正拿去造后端的那份 `webSearchConfigFromEnv()` 产物 * (`boot/stage-07-capability-layer.ts`),所以「能力位说 brave、装配里装的是 tavily」结构上不可能。 * 缺席 = 本部署没有搜索后端(未设 / 拼错 / 装配没喂席位)⇒ 窄投影报 `{backend:"none"}`; * 折词的属主与全部判据在 `plugins/web-search.ts` 的 `projectWebSearchCapability` 头注。 * ⚠️ 席位只承载**部署默认**:单用户车道上每请求的 `settings.webSearch` 压过它,但那条腿**不经过本席** * (它在场景装配里每请求现算),所以本位不会随调用方漂。 */ webSearchProvider?: import("../plugins/web-search.js").WebSearchProvider; /** ① core ruling — SPLIT: `capabilities.workflows` = the ENGINE-CAN axis, boot-computed from core's own * `workflowsCapability(deps)` (hardened script runner ∧ governance), NOT the `Boolean(workflowRunStore)` store * proxy. Orthogonal to `workflowsList` (the durable-list axis = `workflowRunStore`). Falls back to the store * proxy when absent (older callers / tests that don't pass it). */ workflowsCapable?: boolean; /** [ref]③ + codex R1-[medium]:`capabilities.workflowModels` 的**引擎快照席**。main.ts 在 boot 期用 * `workflowModelAllowlistFor(config)` 算一次(与 createRunnerDeps 喂给 core `workflowGovernanceBaseline. * workflowModelAllowlist` 的同一只函数、同一 config 代际),多租户 fail-closed 的 undefined 在装配点 * 折 `[]` ⇒ 生产装配下本席**恒在场**。为什么不让能力面每请求活派生:core 5.49 起 models plane 经 * `Runner.swapDeps` 永久热,而 governance allowlist 是 Runner 构造期快照 —— 活派生会在热加目录键后 * 长期广告一个 core 必拒的词(教学面超前执法面,直到重启)。广告**引擎真快照**让「能力面教的」与 * 「core 收到的 allowlist」结构性恒等。缺席(遗留 stub 装配)⇒ 能力面回落活 config 派生(测试便利, * 生产路径到不了)。 */ workflowModelAllowlist?: readonly string[]; } /** **数值旋钮**:部署可调的上限/拍距。缺省值写在消费点(`?? 256` 之类),此处只承载覆写。 */ export interface ServiceKnobDeps { /** (a)(clay 拍 a+c,2026-07-27)当快照 blob 走 SQL 默认店时(逃生口形/覆写形),该店的 per-blob * 承载帽——PUT /sync/blobs 面据此**前置 413**(typed `blob_too_large_for_sql`)。不设=不前置(MinIO * 形无帽)。为什么在 HTTP 面重复店里的检查:店的 never-throw 结果形把一切写失败折成 502,而 502 * 语义=可重试,对端会对一个永远过不去的 blob 无限重试——413 才是「别再送了」的诚实形。 */ snapshotBlobSqlCapBytes?: number; /** [ref] SSE 连接帽(缺省 256/副本;超限 503+Retry-After=消费方回落 /head 轮询)。 */ sessionEventsMaxConnections?: number; /** [ref] SSE 心跳/owner 复核拍距 ms(缺省 25s;测试注入用)。 */ sessionEventsHeartbeatMs?: number; /** [ref] 背压阻塞断流阈 ms(缺省 60s;write 持续 false 超过即断——僵消费者不无界积压)。 */ sessionEventsStallMs?: number; } /** 组槽本身。每组可选:一个只跑 env-only 的 worker 可以一组都不给。 */ export interface ServiceDepGroups { stores?: ServiceStoreDeps; coordinators?: ServiceCoordinatorDeps; seams?: ServiceSeamDeps; observability?: ServiceObservabilityDeps; governance?: ServiceGovernanceDeps; deployment?: ServiceDeploymentDeps; knobs?: ServiceKnobDeps; } /** 平铺视图 = 必填三件 + 其余七组的全可选并集。**派生**自组接口,故与分组形永不漂。 */ export type FlatServiceDeps = ServiceCoreDeps & Partial; /** createHttpServer 的入参:两形皆可(平铺=存量,分组=推荐)。同名键两形都给时,**顶层显式键赢** * (含显式 `undefined`)——一处装配点只该用一形,混用时以肉眼最近的那个为准。 */ export type ServiceDeps = FlatServiceDeps & ServiceDepGroups; /** 分组入参 → 平铺视图(createHttpServer 的第一件事)。一组都没给 ⇒ 原样返回(存量调用零开销、零形变)。 */ export declare function flattenServiceDeps(deps: ServiceDeps): FlatServiceDeps; /** The TRUSTED end-user principal for an owner-gated / per-tenant request. On a direct-door worker the * trusted-header path is OFF (x-agent-principal is client-self-asserted), so identity comes from the crypto-VERIFIED * principal JWT (`x-approval-principal-token`, principal-only proof — no cnf.bnd action binding); otherwise the * BFF-minted trusted header. undefined when absent/invalid (fail-closed → callers map to 401 / scope "__none__"). * 🔴 Use this for ANY endpoint that reads or gates PER-TENANT data — NOT `principalFrom` directly, which trusts * the self-asserted header on a direct door (/v1/usage + the trace owner-gate * had drifted to a bare principalFrom, unlike the leader/images/approvals identity paths). One source of truth * so a future per-tenant endpoint can't re-introduce the bypass. The action-BOUND decide/answer paths keep their * own verifyDirectDoorProof (cnf.bnd) — this is the identity-only twin. */ /** * Managed-Agents-style HTTP/SSE service surface. * GET /health * POST /v1/tasks → run to completion, TaskResult JSON (synchronous) * POST /v1/tasks/stream → SSE of TaskEvent, live tokens (synchronous, same instance) * POST /v1/runs → async: 202 {taskId, sessionId, status}, runs in background (S1) * GET /v1/runs/:id → run status + result (poll) * GET /v1/runs/:id/events → SSE replay from Last-Event-ID, then tail to terminal (S2) * POST /v1/sessions/:id/notify → inject an out-of-session event ([ref] §2): live stream ⇒ core * `TaskStream.notify()`; idle ⇒ parked in the session inbox, drained * as a `task_notification` on the next stream open * POST /v1/sessions/:id/wake → wake a `task_done` pure park with a message and resume it * ([ref] §3). NOT a gate decision — a pending gate is refused * (`wake.gate_pending`). Goes through `resumeWake`, which reuses the * resume family's lease/CAS/run-log leg, so it BURNS MODEL TOKENS and * is subject to the billable-submit gates (`SESSION_WAKE_RE`). * (both live in `http/routes/notify-wake.ts` — [ref] A9 domain split) */ export declare function createHttpServer(rawDeps: ServiceDeps): http.Server & { denyExpiredApprovals: (now: number) => Promise; isRunLiveOnThisReplica: (taskId: string) => boolean; /** [ref]:live 回决腿的 PARKED 赎回席(装配层把它接进协调器的晚绑座,见 {@link redeemParkedAsk})。 */ redeemParkedAsk: ParkedAskRedeem; }; /** SSE: replay durable events after Last-Event-ID, then tail until the run is terminal (or stale). */ /** * The differences a concrete log (task_run | image_bake) feeds the ONE resumable SSE reader (P2.8). Everything * the reader does — Last-Event-ID/`?from=` resume, the 416 retention boundary, the per-poll concurrent * status+events read, the terminal re-fetch (the terminal event lands in the gap before the status flips), the * stale fallback, the 15-min cap, the 15s idle heartbeat — is provider-agnostic and lives in `streamSseLog`. * 🔴 The task_run provider MUST keep the existing wire bytes EXACTLY (center's relay + 730+ tests depend on it). */ //# sourceMappingURL=server.d.ts.map