/** * TaskRequest.settings wire — project a client's per-request `SemaSettings` stamp onto the per-task `TaskSpec`, * TIGHTEN-ONLY (deny-wins), at the SAME seam as runtime-governance (after the approval baseline + operator * governance). Backs the remote-mode settings path: sema runs over HTTP→engine with no local Runner, so the * resolved settings can't be applied by a local `tightenTaskSpec` — they ride on `TaskRequest.settings` and the * SERVICE projects them into the engine here. * * Design: `TaskRequest.settings` is a * PER-REQUEST ephemeral stamp = the v2-design TASK layer (a single-run override, NOT durable). It folds into THIS * spec via {@link tightenTaskSpec} and that is all — it is NEVER written to the durable `SessionPolicyStore` (the * operator/principal axis, [ref] §E6, a different trust axis + lifecycle). Re-applying on a resume leg is * idempotent (the same tighten), so no special resume handling is needed (mirrors runtime-governance). * * 🔒 SECURITY INVARIANTS (SDK settings.ts §3.3): * - permissions are TIGHTEN-ONLY — folded via `tightenTaskSpec` (deny-wins `combinePolicies`); it THROWS on any * override that would LOOSEN a stricter base safety field. The client can only ADD denies/asks/handsReadOnly on * top of the deployment + operator baseline, never widen it. * - permissionMode(轴B #2 注释保鲜,1.254——[ref]/[ref] 后的真实现):`defaultMode` 收**五**模式 * (default | acceptEdits | plan | bypassPermissions | auto),bypass/auto 被 HONOR 为 gate-shape 选择 * ——但深层不变量未变:bypass=「不加 ask 门」,恒不越部署基线(tightenTaskSpec deny-wins,基线 deny/ * ask 照压);精化语义见 :2xx 行内注释。旧头注的「NEVER honored/三模式白名单」是 pre-[ref] 拍照。 * * 🔴 SYNC WITH sema-cli(壳仓)的 `src/sema/settings/seam.ts`: the permissions→toolPolicy + §6.4 permissionMode derive MIRRORS * the shell's LOCAL-Runner resolver — both compose the SAME core primitives (`createAllowDenyPolicy` / * `combinePolicies` / `tightenTaskSpec`), so the deny-wins ENFORCEMENT is core-single-source. Only the COMPOSITION * is duplicated for the remote path; a proposed follow-up extracts it to `@sema-agent/sdk` for true * single-source. Because the core backstops (deny-wins + tighten-throws-on-loosen + this bypass-reject) hold either * way, a composition drift would be a local/remote CONSISTENCY gap, never a privilege-scope widening. * * v1 scope: `permissions` + `permissions.defaultMode` + `model` + `outputStyle` + `env` (shipped 1.26.0). DEFERRED * (need a seam, flagged to the team + advertised in capabilities so a caller never silently loses a setting): * - (`env` is NO LONGER deferred — systematic-audit doc fix: core kept no untrusted `TaskSpec.env` seam (types §7 * Q4), so the SERVICE applies it on the TRUSTED host lane: `settings.env` → `cleanEnvMap` → `shellEnv` on the * executionEnvFactory, single-user-gated. See `deriveSettings` below + main.ts.) * - (`hooks` NO LONGER deferred either — 轴B #2 注释保鲜:applied 形已接线(本文件 hooks 分片 + * main.ts 单用户闸),仅 malformed 才报 deferred;旧「需要 remote hook-runner」是接线前拍照。) */ import { type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PromptProvider } from "@sema-agent/core"; import type { HooksConfig } from "@sema-agent/settings-schema/hooks"; import { type KeyClosureIssue } from "./request-key-closure.js"; /** The permission MODE a client may request (SDK `SettingsPermissions.defaultMode`), the [ref]/[ref] five-word * table. ⚠️ This is a DELIBERATE deployment set, NOT a copy of any CC set ([ref] P2-1 corpus check): CC's own * full enum is SIX words (= ours + `dontAsk`), CC's managed/remote form accepts FOUR (= ours − `bypassPermissions`, * which it warns-and-ignores). Ours = {CC-remote four} ∪ {`bypassPermissions`} — registered divergence, not parity. * `bypassPermissions` and `auto` joined the set with the [ref] fs-write gate: since the gate exists, every * mode is a choice of HOW MUCH mode-derived gating the interpretation layer ADDS on top of the deployment baseline — * `bypassPermissions` adds none (= the pre-[ref] behavior of every mode: "不加门" not "开门", [ref] table row 4; the * deployment approval/governance baseline is composed OUTSIDE this derive and is untouchable from a settings stamp), * and `auto` adds the same ask gate as `default` with core's auto-mode classifier screening the asks upstream * (entitlement-gated in core, [ref]②). */ export declare const SETTINGS_PERMISSION_MODES: readonly ["default", "acceptEdits", "plan", "bypassPermissions", "auto"]; /** 五模式闭集的型形 —— **从值表派生**(S-496 / [ref]):改前本仓有 **4 份**同一张五词表(本处的手写联合 / * 下面 `coercePermissionModeStrict` 的运行期字面量链 / `http/admission.ts` 顶层 `permissionMode` 的比较链 / * 同文件 `settings.permissions.defaultMode` 的比较链),外加两句错误文案里各抄一遍词。现在值表一份、 * 型与文案都从它派生,比较链改调 {@link isSettingsPermissionMode}。 */ export type SettingsPermissionMode = (typeof SETTINGS_PERMISSION_MODES)[number]; /** 五词表的 wire 文案形(`"a" | "b" | …`)—— 两条 400 的句子从这里取词,不再各抄一份。 */ export declare const SETTINGS_PERMISSION_MODE_WORDS: string; /** 闭集成员判定(`unknown` 入口用)—— 词表的**唯一**运行期读者:wire 门与 coerce 两族同调。 */ export declare function isSettingsPermissionMode(raw: unknown): raw is SettingsPermissionMode; /** * R4: coerce a raw top-level `body.permissionMode` (the LIGHT per-turn wire — the shell sends the * CC permission mode RAW, axis-agnostic; the service interprets) to a service-honored mode, or undefined. * 🔴 TIGHTEN-ONLY vs the DEPLOYMENT baseline still holds for every value: the mode only selects the CLIENT-derived * gate ({@link deriveSettingsPolicy}); it can never subtract from the deployment/operator policy (applyTaskSettings * folds via tightenTaskSpec, deny-wins). Post-[ref] the honored set is the five-word table above (NOT "CC verbatim" — * see the {@link SettingsPermissionMode} header for the three-set contrast; the earlier "five CC modes" wording here * was [ref] P2-1's finding: no CC set has five members). `acceptEdits`, * `bypassPermissions` and `auto` are HONORED as gate-shape choices (they no longer coerce to `default`, which * would FORCE the manual ask gate onto a caller that explicitly asked for less asking — with the gate live, the old * coercion would have been a behavior change for them, incl. a headless-deny regression for bypass callers). An * UNKNOWN string still coerces to `default` (fail-safe: the most-asking mode). * [ref]-②: fresh submits can no longer REACH the unknown-word arm — the HTTP gate 400s a non-five-mode value * (server.ts, sibling of promptProfile) and the wire type is a closed enum. This lenient fold stays for the one * population the gate deliberately does not cover: RESUME replay of a persisted pre-gate body (outputStyle/model * two-layer posture — bricking a resume over a value that was legal at submit time is worse than the quiet fold). */ export declare function coercePermissionMode(raw: unknown): SettingsPermissionMode | undefined; /** 五模式闭集的**严格**读法([ref] ④:`/v1/capabilities?permissionMode=` 查询串用它 —— 读面对词表外的值 400, * 不走上面那条给 resume 重放留的 `default` 宽臂)。上面的宽读只是本函数外再包一层折叠。 * S-496:词表单点已上移到 {@link SETTINGS_PERMISSION_MODES},本函数只是它的 `undefined` 形读者。 */ export declare function coercePermissionModeStrict(raw: unknown): SettingsPermissionMode | undefined; /** * S-433 —— 一份**被批准的计划**之后,这条会话继续用的 permission mode(`plan_review` 决裁体的 * `permissionModeAfter`)。core 的契约句是 `present-plan-tool.js`:「If the plan is approved, the run * continues (and any read-only restriction is lifted)」—— 而 core 顶层 resume 把宿主的续跑配置**整体** * 展开成新一段 spec、不继承停靠点的 `handsReadOnly` / `enablePlanMode`(`runtask.ts`),所以「松开」这 * 件事的属主在**本仓装配续跑配置的那一处**。批准者要说的正是「松到哪一档」。 * * 🔴 **闭集为什么只有这两个词**:它是 CC `ExitPlanMode` 三选卡上那两张 Yes 卡(「Yes, and auto-accept * edits」/「Yes, and manually approve edits」)的对位;`plan` 自己(= 不松开)由 `edit`/`reject` 两个 * 决裁表达,`bypassPermissions` / `auto` 不在这张卡上 —— 一个批准动作不该顺带把部署的 ask 面整个关掉。 * * 🔴 **不手抄词表**:`satisfies readonly SettingsPermissionMode[]` 让「本表的成员必须仍是本仓受理的 * 模式词」成为**编译期**事实 —— 五词表某天改名/删词,这里就是红的,而不是静默留一个没人认的词。 */ export declare const PLAN_APPROVE_PERMISSION_MODES_AFTER: readonly ["default", "acceptEdits"]; /** {@link PLAN_APPROVE_PERMISSION_MODES_AFTER} 的类型形(值表单点,类型从值派生 —— 两处各写一份就会漂)。 */ export type PlanApprovePermissionModeAfter = (typeof PLAN_APPROVE_PERMISSION_MODES_AFTER)[number]; /** S-433 值闭集判据(wire 门用)。**宽臂一个都没有**:词表外的值 400 响亮拒 —— 这一位选的是批准之后 * 模型有没有手,把一个拼错的词折成任何一档都是安全轴上的静默降级([ref]「无静默 fail-open」)。 */ export declare function isPlanApprovePermissionModeAfter(raw: unknown): raw is PlanApprovePermissionModeAfter; /** * S-433 —— **一条已持久化的提交体的生效 permission mode**,单点。 * * 它只是把 {@link effectivePermissionMode} 与「拿哪一份 settings 去问」这**一对**绑在一起:后者是 * `parseTaskSettings(body.settings)`(resume 重放共用的宽容解析层)。两处各写一遍这一对,就会在 * 「某天改成用别的 settings 去算生效模式」的那一拍静默分家 —— 而分家在这条轴上的形态是「wire 门说 * 这条任务不是只读起步、装配腿却按只读起步重建」,两个 4xx/行为各说各话,tsc 一声不响。 * * ⚠️ 射程:**只管持久化体这一路**。首跑那一路的生效模式在 `boot/resolve-spec.ts` 单点算(它吃的是 * 场景折叠后的 settings),本函数刻意不去冒充那一处的属主。 */ export declare function storedBodyPermissionMode(body: { permissionMode?: unknown; settings?: unknown; }): SettingsPermissionMode | undefined; /** L2 ultracode ([ref]): the effective `thinking` level given the explicit `reasoningEffort` and the ultracode * preset. ultracode FLOORS at the "ultra" tier — core's single source `resolveReasoningProfile("ultra").thinking` * (= "max"), never a hardcoded literal — so an explicit higher tier still wins (none exists above max today; * future-proof). Awareness rides for free: core composes ORCHESTRATION_AWARENESS from thinking∈{xhigh,max}, no field. */ export declare function effectiveThinking(reasoningEffort: unknown, ultracode: boolean): ThinkingLevel | undefined; /** R4: fold a top-level `body.permissionMode` onto the parsed settings as `permissions.defaultMode` (the top-level * field is the explicit per-turn intent → it WINS over a bundle defaultMode). Creates a minimal settings object when * no `body.settings` bundle was sent. The result flows through {@link applyTaskSettings} (tighten-only). */ export declare function withPermissionMode(settings: ParsedTaskSettings | undefined, mode: SettingsPermissionMode): ParsedTaskSettings; /** * [ref] §6-1 —— 本请求的**生效** permission mode,单点。优先序逐字同 {@link withPermissionMode}: * 顶层 `body.permissionMode`(本轮显式表态)赢过 bundle 的 `settings.permissions.defaultMode`;两处 * 都没有 ⇒ `undefined` = **无表态**(调用点据此不写键,而不是替调用方选一个默认值)。 * * 🔴 为什么必须是一只函数:同一个「生效模式」此前在 resolve-spec 里被算了两次(hooks 腿的 * `permission_mode` 载荷、settings 折叠腿的 `withPermissionMode`),三审同点判定翻译表**不得**成为 * 第三个算点 —— 三处各算各的,任何一次优先序修订都会让三面分家,而分家在这条轴上的形态是 * 「壳发的 bundle bypass 在 A 面生效、在 B 面没生效」这种最难被外部发现的静默偏差。 */ export declare function effectivePermissionMode(body: { permissionMode?: unknown; }, settings: ParsedTaskSettings | undefined): SettingsPermissionMode | undefined; /** * [ref] §2 —— 显式 permission mode → `TaskSpec.shellGate` 档位的**翻译表(单一真源)**。 * * · `bypassPermissions` ⇒ `"off"` —— CC `--dangerously-skip-permissions` 的对位裁定 * (与 fs-write 面的 bypass 臂两面归一,见 {@link deriveSettingsPolicy})。 * · `auto` ⇒ `"classify"` —— **[ref] §8 开口按施工期新披露亲裁改译**(原裁定字面是 off, * §8 保留「core auto 分类器覆盖面有新披露时可独立再裁,表驱动一行」——新披露见下段): * * ⚠️ **`auto` 这一行有一条待裁的开口([ref] §8 明列「core auto 分类器覆盖面有新披露时可独立再裁, * 表驱动一行」)。施工期交叉复审给出了那条新披露,已亲读安装包核实**: * · core 只在**已经产生 `ask`** 之后才咨询分类器(`dist/core/hooks.js`:`if (input.autoMode && * decision.action === "ask" …)`); * · 而 `shellGate:"off"` 下 core **不铸任何 shell 面的门**(`dist/core/runner/prepare-task.js` 的 * `shellGate === "off"` 臂反而发一条 `classification:"shell-gate-off"` 的 onError:「真可写 Bash 挂着 * 却没有 shell 安全轴折叠」)。 * 两条合起来:`auto` × off ⇒ Bash 既不产 ask、分类器也就永不被咨询 —— off 档下「筛选权交给分类器」 * 不成立。故 auto 归 classify 组:分类器坐在 classify 产的 ask 之上,语义才真是「交给分类器」 * (良性只读命令由分类器自动放行,其余 ask;这正是 auto 模式的本义)。发车帖向 [ref] 裁定链披露此 * 一行偏离;core auto 分类器若来日在 off 档下也有咨询点,可再裁回(表驱动一行)。 * · `default` / `acceptEdits` / `plan` ⇒ `"classify"` —— 这一档原先由壳无条件注入 `MANUAL_MODE_SHELL_GATE` * 供给(车道缺省走了 operator 通道),现在归位到表态轴。`plan` 本身 handsReadOnly(core 明写此档下 * shellGate 无效),给它 classify 纯为一致性。 * * 🔴 **入参非可选**:缺席(无表态)不是这张表的一行 —— 它的语义是「不写这个键」,只能在调用点判。 * 把它折进来会逼出一个 `undefined` 返回值,而那正是「写 off」与「不写」被混同的入口(core 的 * 缺席默认是 off,但**写**一个 off 会成为 governance 的 base,与缺席不是同一件事)。 * * 🔴 **闭集穷举表,无 default 臂**:五个模式词是封闭词表,新增一个模式而不更新本表 * 是**编译错误**([ref] 的安全轴纪律:词表的未知项不许有静默兜底臂)。 */ export declare function shellGateForMode(effMode: SettingsPermissionMode): "off" | "classify"; /** * [ref] —— 生效模式 → `TaskSpec.writeFace`(**写**面的 roots 容纳判断)。`"open"` = 工作区根之外也能写; * `undefined` = **不写这个键**(core 缺省 `"roots"`,与 7.77.0 及以前逐字节相同)。表逐字在 * {@link MODE_ENGINE_FACES},裁定与硬边界写在 `resolve-spec.ts` 的写点上。 * * 🔴 与 {@link shellGateForMode} 同律:入参非可选 —— 「表态缺席」不是这张表的一行(那是调用点判的 * 「整个键都不写」),把它折进来就会逼出第二个 `undefined` 语义。 */ export declare function writeFaceForMode(effMode: SettingsPermissionMode): "open" | undefined; /** * [ref] ① —— 生效模式 → `TaskSpec.autoModeRequested`(auto **意图**座,CC 250「用户开」半场)。 * `true` 只有 auto 一行;`undefined` = 不写键。表逐字在 {@link MODE_ENGINE_FACES}。 */ export declare function autoModeRequestedForMode(effMode: SettingsPermissionMode): true | undefined; /** * S-346 —— [ref] 技能清单预算两旋钮的**值域谓词**(一对具名单铸点)。 * * 两层同问这一只:FRESH 提交面的 400 门(`http/server.ts`)问「合不合法」,RESUME 宽容层 * ({@link parseTaskSettings})问「收不收下」—— 这两句话在这两个键上是**同一个问题的正反面** * (400 的条件恰是受理条件的否定)。修前它们各写一遍字面比较,而承重的两件事 * (`Number.isFinite` 挡 `NaN`/`Infinity`、`Number.isSafeInteger` 与引擎受理域**同字**) * 靠两份注释各自记着 —— 任一处漏一个判据,同一个坏值就会在一条腿上 400、在另一条腿上被收下。 * * 值域**抄 CC settings schema**,不自创;server 侧零加工(不夹紧、不换算、不复刻 core 的默认值), * 判据全文见两个调用点的顶注。 */ export declare function skillListingBudgetFractionOk(v: unknown): v is number; /** {@link skillListingBudgetFractionOk} 的孪生(`int().positive()`;`isSafeInteger` 而不是 `isInteger` 的 * 理由见那里)。 */ export declare function skillListingMaxDescOk(v: unknown): v is number; /** The validated, service-trusted subset of `SemaSettings` we project onto the spec. `env` is parsed only to REPORT * it as received-but-deferred when off the host lane (never silently dropped); a MALFORMED `hooks` likewise reports * deferred (submit 路径另有 400 fail-loud),valid `hooks` 进 applied shape(hook-runner 阶段一)。 */ export interface ParsedTaskSettings { permissions?: { allow?: string[]; deny?: string[]; /** Carried for completeness; v1 folds `ask` into the gate via the deny-wins base (see {@link applyTaskSettings}). */ ask?: string[]; defaultMode?: SettingsPermissionMode; /** [ref] 件⑥([ref] §3.4,[ref];PM 裁 [ref]):CC settings 的 kill-switch `permissions.disableAutoMode`,cli 投影 * (settingsRulesWire.ts toWireSettings)而此前本白名单丢弃 ⇒ 用户/managed 层关 auto 到不了引擎。语义 = 只能关 * 不能开(tighten-only):把生效模式 auto 折 default(座不写 ⇒ 不武装)。折叠单点 = {@link effectivePermissionMode}。 * `disableBypassPermissionsMode` 本批不做落点([ref] 另立)。 * * 🔴 **受理集 = `"disable" | true | false`**([ref] 放宽一档,原为「只认字面 `"disable"`」)。两套拼写**都是已发布 * 契约**:CC 250 的 `Ct` 严极词 `"disable"`,与 `@sema-agent/sdk` 8.1.0 `SettingsPermissions.disableAutoMode?: boolean` * (`dist/settings.d.ts`)。按已发布 SDK 类型写 `true`/`false` 的客户端此前一律 400 —— [ref] 的「只认 disable」是 * 「拒非法**拼错**」,boolean 不是拼错。归一化**不做**:三形逐字过 parse(拒体/日志里看得见调用方真发的是哪一形), * 语义归一只在折叠单点做(`"disable"` ≡ `true`)。受理集之外(别的词/数字/对象)= fresh 提交 HTTP 门 400, * resume 重放宽臂丢弃(两层姿势同 defaultMode)。 */ disableAutoMode?: "disable" | boolean; }; model?: string; outputStyle?: string; /** `settings.env` → per-task shell env. core deliberately keeps NO * `TaskSpec.env` (an untrusted caller must not swap the sandbox — [ref] §7 Q4); per-session shell env rides * the TRUSTED `executionEnvFactory` seam (`shellEnv`) instead. [ref] "env = capability axis": HONORED only on * the single-user host lane (`cwdHonored`), IGNORED on any cloud/multi-tenant lane. Parsed here; gated at apply. */ shellEnv?: Record; /** L2 ultracode preset ([ref]): a CC-parity `settings.ultracode` boolean that EXPANDS to the two existing * orthogonal axes — `thinking:"max"` (the reasoning axis) + `selfOrchestration:true` (the orchestration axis, * through its existing fail-closed gate). NOT a new mechanism; just a preset that fans out to existing spec fields. */ ultracode?: boolean; /** hooks hook-runner(阶段一):registry-core 0.1.51 契约校验过的 hooks 配置。像 shellEnv 一样 * 这里只 PARSE;应用点(main.ts resolveSpec)按单用户闸(`requirePrincipal !== true`)决定翻不翻成 * core `TaskSpec.hooks` 回调——多租户 lane 收到只警告忽略(hook 命令跑在 worker host = class ② 能力授予)。 */ hooks?: HooksConfig; /** [ref](7.77.0)—— CC settings 同名键:上下文窗口里**留给技能清单**的比例((0,1];CC 默认 0.01 = 1%)。 * 超预算时引擎把描述裁短、再不够退成 name-only(`cli259.js` 的清单渲染算法),**从不**拒掉一条技能 —— * 这正是本版把技能条数/描述两条帽整条退役后的补偿面:per-turn 成本的旋钮在**渲染侧**,不在受理侧。 * server 零加工透传到 `TaskSpec`,默认值属主是引擎(缺席 ⇒ 整键不写)。 */ skillListingBudgetFraction?: number; /** 同族第二键:每条技能描述在清单里的字符帽(正整数;CC 默认 1536)。超出由引擎**截断**,不是拒。 */ skillListingMaxDescChars?: number; } /** The result of parsing an untrusted `body.settings`: the applied subset + the names of present-but-deferred * fields (so the caller can log/advertise them — no silent drop). `null` settings ⇒ `{ settings: undefined }`. */ export interface ParseTaskSettingsResult { settings?: ParsedTaskSettings; /** Sub-fields the caller SENT but this version does not wire(`env`;`hooks` 仅在 MALFORMED 时入列——valid hooks * 已是 applied 字段)。Empty when none. */ deferred: string[]; } /** Cap on `outputStyle` length — it lands in the system prompt, so it shares the `MAX_SYSTEM_PROMPT_CHARS` (16384) * posture (an uncapped prompt body is a per-turn token-cost hole; cross review). Exported so the HTTP layer * (prepareSpec) can 400 fail-loud on submit with the SAME bound this defensive parse enforces on every path. */ export declare const MAX_SETTINGS_OUTPUT_STYLE_CHARS = 16384; /** Cap on the element count of each permission list (allow/deny/ask) — bounds a pathological rule blast. */ export declare const MAX_SETTINGS_PERMISSION_RULES = 1000; /** Caps on `settings.env` — bound a pathological env blast (count + per-value length; values land in the shell env). */ export declare const MAX_SETTINGS_ENV_VARS = 256; export declare const MAX_SETTINGS_ENV_VALUE_CHARS = 32768; export declare const MAX_SETTINGS_ENV_KEY_CHARS = 256; /** 本 service 真正**消费**的 `settings` 顶层键 —— 闭集。每一个都有消费点: * `env`/`hooks`/`model`/`outputStyle`/`permissions`/`ultracode` 与两个 `skillListing*` 在 * {@link parseTaskSettings};`webSearch` 在 `capabilities/scenarios.ts`(`webSearchConfigFromSettings`,单用户闸)。 * 加键必须同时加消费点,否则就是往闭集里塞一个新的静默丢。 * * 🔴 两个 `skillListing*`([ref],7.77.0)= **CC settings 同名键**,值域抄 CC 的 schema 声明 * (`skillListingBudgetFraction` 是 `gt(0).lte(1)`、`skillListingMaxDescChars` 是 `int().positive()`); * 消费点 = 本文件的 parse + `boot/resolve-spec.ts` 的 `TaskSpec` 透传。语义(预算怎么花、描述怎么裁、 * 溢出怎么退成 name-only)整个归引擎,本仓一个字节都不渲染,也**不复刻默认值**——缺席即整键不写。 */ export declare const TASK_SETTINGS_KEYS: readonly ["env", "hooks", "model", "outputStyle", "permissions", "skillListingBudgetFraction", "skillListingMaxDescChars", "ultracode", "webSearch"]; /** 本 service 消费的 `settings.permissions` 子键 —— 闭集(值形校验另有既有门,本门只判键名)。 */ export declare const TASK_SETTINGS_PERMISSION_KEYS: readonly ["allow", "ask", "defaultMode", "deny", "disableAutoMode"]; /** CC/SDK 合约里合法、本 service **不接线**的顶层键。词表外的 CC 键落 `unknown` —— 两列都是 400,标签只影响 * 文案,不影响判决。前三个的出处 = `@sema-agent/sdk` `SemaSettings` 尾注逐字点名的 deferred CC 键。 * * 🔴 `crossSessionInbound` / `dialogExpiry`(S-111)为什么在**这一列**而不是受理列:它们是**每会话**的 * 保护型设置(`refuse` = 本会话退出入站跨会话车道),而引擎只给了**部署级**座位(一个 Runner 单例, * getter 无会话上下文)。受理它们只能做到「解析 + 记一行运维日志」——调用方拿到 202、以为自己的 * opt-out 生效了,而实际没有任何 per-session 生效点。**在保护型配置上,「受理但不执行」比「响亮拒」 * 危险得多**:前者是静默的假安全,后者是调用方当场看得见的事实(400 体的 `unsupportedKeys` 逐字列名, * 机器可读)。所以本版按 `statusLine` 族同姿势明拒。 * 部署层的同名治理**照常可用**,走部署旋钮(`cross-session-settings.ts` 的 managed 层);等引擎开出 * 每任务座、或本仓 per-run 上下文覆盖到每一条 run 腿,这两个键**移到上面的受理列**即可(一行), * 那一刻它们才真的有生效点。 */ export declare const TASK_SETTINGS_UNSUPPORTED_KEYS: readonly ["apiKeyHelper", "crossSessionInbound", "dialogExpiry", "sandbox", "statusLine"]; /** 同上,`settings.permissions` 子层(出处 = SDK `SettingsPermissions` 的三个声明键)。 * `additionalDirectories` 有**同请求替代面**:顶层 `body.additionalDirectories`([ref],resolve-spec 真消费)。 */ export declare const TASK_SETTINGS_UNSUPPORTED_PERMISSION_KEYS: readonly ["additionalDirectories", "disableBypassPermissionsMode"]; /** * FRESH 提交面的 `settings` 键闭集裁决:顶层或 `permissions` 子层出现受理集之外的键 ⇒ 返回拒体(调用方 400 * `request.body_shape`);全部受理 ⇒ `null`。 * * 判据边界(有意窄): * · 只判**键名**。值形校验(outputStyle 上限 / permissions 数组上限 / env 形 / hooks 契约 / defaultMode 闭集) * 仍归各自既有门,本门零改动、零抢话。 * · `settings` 本身非对象、或 `permissions` 非对象(数组/标量/null)⇒ 本门**不判**,留给既有的 * 「settings must be an object」/「settings.permissions must be an object」两道门,免得同一个体被两句话抢答。 * · `permissions: null` 是既有钉住的「容器缺席」形,照旧不咬。 */ export declare function taskSettingsKeyIssue(raw: unknown): KeyClosureIssue | null; /** * Validate an untrusted `body.settings` (raw `unknown` off the wire) into the service-trusted {@link * ParsedTaskSettings}. NEVER trusts the wire type — every field is shape-checked. `defaultMode` accepts the FIVE * modes (post-[ref]; unknown values coerce to `default` — see the header note). Returns the * present-but-deferred field names so the caller can warn/advertise rather than silently dropping `env`/`hooks`. * * 🔴 未知键在**本层**照旧忽略,而那**不是** wire 的现行为([ref] 件①,PM 裁「拒」[ref]):本层是 RESUME * 重放也走的宽容层,一个 4xx 在这里会把存量 parked 任务永久砖死;FRESH 提交面由 {@link taskSettingsKeyIssue} * 在 `server.ts` prepareSpec 里 400 响亮拒 + 逐字列名。双层姿势与 outputStyle / permissionMode 两处逐字相同。 */ export declare function parseTaskSettings(raw: unknown): ParseTaskSettingsResult; /** Defensive acceptance of the `appendSystemPrompt` rider ([ref] R2 + codex F2): the HTTP 400 gate (prepareSpec, * MAX_SYSTEM_PROMPT_CHARS) only covers FRESH submits — every resume family re-enters resolveSpec on a STORED body, * and a pre-1.243 store preserved unknown request keys verbatim (the field was INERT then), so an over-cap legacy * value must not become active prompt content after rollout (context exhaustion / spend hole). Over-cap ⇒ dropped + * warned — the rider was inert pre-upgrade, so dropping preserves the stored task's prior behavior; a 4xx here would * permanently brick that task's resume instead (same defensive posture as parseTaskSettings' outputStyle drop). * Cap value = the submit gate's (16384; MAX_SETTINGS_OUTPUT_STYLE_CHARS — both land in the same spec field). */ /** TRUE when `text` is an ALREADY-ASSEMBLED system prompt — core's migration guard detects the same three * constitution anchors and takes the pass-through assembler arm, whose section subset (OPAQUE_KEPT, dist-read) * has NO `core/role.append`: any append rider (top-level or settings.outputStyle) would be SILENTLY discarded * there. Shared predicate for the submit 400 gate, the resolveSpec resume mirror, and the center-declaration * probe — one definition so the three gates can't drift. */ export declare function hasConstitutionAnchors(text: unknown): boolean; /** codex R9 (provider-aware append-less detection): mirrors core's ACTUAL pass-through triggers instead of a raw * body-text heuristic — the assembler's migration guard probes (a) `stableBlocks` identity declarations and * (b) `stableSystem` OUTPUT for the three anchors; a plain `userSystemPrompt` under the DEFAULT provider (no * scenario provider) never triggers it and `core/role.append` is retained, so an anchored systemPrompt there * must NOT block/strip a rider (R9 false-positive: unbranded default / scan rejected valid combos). Probing * calls the provider hooks with a minimal ctx — server providers (and centerPromptProvider) are pure functions * of ctx, and the same hooks run again inside core's assembly; a hook throw = conservative false (core will * fail the task loudly on its own). `replaceAll` owns the whole prompt(legacy free-form `system()` 腿已随 core 2.0.0 删除) * (REPLACE_ALL_EXCLUDED strips role.append) → append-less true. */ export declare function providerDropsAppend(provider: PromptProvider | undefined, userSystemPrompt: string | undefined): boolean; export declare function acceptAppendSystemPrompt(v: unknown, warn?: (detail: string) => void, packDropsAppend?: boolean): string | undefined; /** [ref]②/codex F2 — what the workflow ask leg needs from the deployment, on EVERY lane. It is name-keyed and * fs-independent, so it never had the lane split {@link FsWriteGateWiring} carried (that split is itself gone * since [ref] — the sandbox lanes wire a gate of their own shape instead of no gate). */ export interface WorkflowGateWiring { /** The session "don't ask again" probe (approvalExemptionStore.has, canonical toolName key) — same store and * key space as the fs-write gate's probe; one remember="session" grant serves both. */ isExempt?: (toolName: string) => boolean | Promise; } /** [ref]/[ref]① — everything the mode-derived fs-write ask gate needs from the deployment, provided by * `resolve-spec.ts`. Absent ⇒ no gate is derived (the pre-[ref] behavior). * * WHY the `env` matters: core's `createFsWriteGatePolicy` canonicalizes every target/dir through the given `env` * (real `exists`/`canonicalPath`/`readLink` fs access — read in dist, fs-write-gate-policy.js), so on a * HOST-SEMANTICS lane (`REMOTE_EXEC` unset or "host") the env MUST be the fs the hand tools write. * * [ref]/[ref] — the sandbox lanes (e2b/k8s/ssh/adb/local-docker) wire a gate TOO, but a different-shaped one: the * tools run OFF this box and the per-task sandbox env is minted inside core (executionEnvFactory) AFTER spec * build, so a worker-local env would adjudicate against the WRONG fs (a wrong ALLOW is worse than no gate — that * is why [ref] left those lanes gate-less). They supply a `DeferredSandboxPathEnv` * (`src/boot/deferred-sandbox-path-env.ts`) instead: a proxy that forwards the fs READ primitives to the task's * REAL sandbox env at adjudication time (a per-session slot the factory decorator fills), keeping only * `absolutePath` lexical so no relative path is resolved against the wrong base. Empty slot ⇒ fail-closed. */ export interface FsWriteGateWiring { /** The env the gate canonicalizes against — the worker host env on the host lane, the * `DeferredSandboxPathEnv` proxy over the task's real sandbox env on the sandbox lanes ([ref]). */ env: ExecutionEnv; /** The task's working directory: the factory's `rootPath` (relative-path base) AND the acceptEdits accept domain. * ABSENT on the sandbox lanes ([ref]/[ref]): the sandbox cwd is the ENGINE's tracked cwd, not the env's own, so * neither spec time nor the env proxy can supply the right base — inventing one would mint a bogus auto-allow * domain. With it absent the gate gets no `rootPath` and no `acceptDirs`, so relative paths fail to resolve * (⇒ ask) and `acceptEdits` degrades to the `default` arm — the same fail-safe direction as the host lane's * never-created sentinel dir (resolve-spec 修5). */ cwd?: string; /** * 🔴 S-177 / codex r1 [high](验真后修)—— **权限规则**的路径基,与上面那个 `cwd` **刻意分成两个字段**。 * * 两者在 host 腿上常常同值,但它们回答的不是同一个问题,而混用一个字段会让其中一个静默失效: * · `cwd` 是**写门**的基。它在「有 remoteExec 但这条 session 还没登记工作目录」时取的是一个 * **永不创建的哨兵目录**(`resolve-spec` 的 `fs-write-gate-unrooted`)。对写门那是 fail-SAFE 的: * 相对目标在哨兵下解析不出真身 ⇒ 落 ask。 * · `taskRoot` 是**规则模式**的基(core `PathRuleBases.root`)。规则编译器按**词法**匹配,不问目录 * 存不存在 ⇒ 把哨兵递给它,`deny Edit(src/**)` 会编译成功、指向一个谁也不会写的目录,对真实工作区 * 的 `<工作区>/src/secret.ts` 这类目标判 allow —— 一条运维亲手写下的收紧规则**静默失效**,方向与写门那条正相反。 * * ⇒ 本字段**只在真任务根已知时**在场。缺席 ⇒ 规则编译器拿不到 `pathBases.root` ⇒ 需要该基的拼法 * (`Edit(src/**)` / `Edit(/etc/**)`)被 core 以 `unsupported.path_base` **整表拒**(→ 422,fail-loud), * 而不是编成一条够不着任何东西的规则。绝对形(`//abs`)与 `~/` 形不需要基,两种部署形都照常编得出。 */ taskRoot?: string; /** * 🔴 S-177 / codex r2 [high](验真后修)—— **执行环境的 home**(core `PathRuleBases.home`,`~/…` 形的基)。 * * 🔴 **S-213④ 起本字段在远端车道上也可以有值**:四只远端 adapter 此前一个都不声明 `ExecutionEnv.homeDir`, * 而本字段那时只在 `remoteExec` 整个缺席时才铸 —— 于是**每一条远端部署的 `~/` 规则都恒 422**(接入审计 * M13)。修法不是放松下面那道门,而是让部署**真的知道** home:`execution-lane-caps.ts` 的 * `executionLaneHomeDir` 是唯一属主,`boot/execution-env.ts` 把它递给 adapter 的 `homeDir` 座、 * `boot/resolve-spec.ts` / `boot/parked-revive-gate.ts` 把**同一个值**递成本字段。两处同源是承重的: * 各算一份就会出现「422 门放行而引擎判 unreadable」或反过来。 * * 🔴 **S-213⑦ 改真话**(亲读 core 7.12.0 `dist/core/permission-rule-model.js` / * 的 `resolvePathPattern`,推翻本注此前的两句): * · core 对缺席的 `home` **不**回落 `os.homedir()` —— `resolvePathPattern` 走 * `lexicalNormalAbsolutePathOf(baseValueOf("home", bases) ?? "")`,base 缺席 ⇒ `undefined` ⇒ 直接回 * `{missingBase:"home"}`,该调用的射程判 `unreadable`(fail-closed 的 ask,人来清),从不猜一个目录。 * 换言之引擎侧本来就没有「拿 server 的 home 去匹配远端目标」这条静默腿(那句旧注是本仓自己的推断)。 * · core **有**「这条规则需要哪些 base」的查询口 —— 根导出的 `ruleBasesNeeded(rule)` * (`//abs` ⇒ 空、`~/` ⇒ `["home"]`、`/x` ⇒ `["root"]`、其余 ⇒ `["cwd"]`),`resolvePathPattern` * 自己也读它。本仓的判据腿今天读的是 core 解析出的规范 command 前缀,与该查询口**同一分类**、 * 不是第二套文法;两者哪天分叉由这条注负责被人看见。 * * ⇒ 本字段在**这条部署的执行车道 home 已知**时在场(`executionLaneHomeDir`,见 `execution-lane-caps.ts`)。 * 缺席时 `~/` 形规则由 {@link rejectUnmatchableSettingsNames} **响亮拒**(422),而不是让它编译成一条 * core 只会判 `unreadable` 的规则 —— 两道门同向(都不猜目录),422 只是把同一个拒挪到更早、更响的地方: * 运维在**下单那一刻**就知道这条规则这条车道读不了,而不是等到跑起来每次都弹一张 ask。 */ taskHome?: string; /** The session scratchpad dir (envFacts.scratchpadDir, [ref]③) — writes there are always auto-allowed. */ scratchpadDir?: string; /** [ref]① / core 1.294 exemption seam: the session "don't ask again" probe (approvalExemptionStore.has), * called by core as `(canonicalToolName, canonicalPath)`. The host arm ignores the second argument (the key * comes from a real-fs canonicalize and the grant is deliberately name-keyed); the sandbox arm ([ref]) uses it * to confine the exemption to POSIX-absolute canonical keys — core's `canonicalizeTarget` short-circuits * backslash-UNC forms to `ok:true, key=raw` WITHOUT consulting the env, and an unconfined name-keyed grant * would turn those unresolvable forms into an allow (codex review 2026-08-05). * 🔴 **坐标刷新(7.74.0 / core 7.17.0 [ref];结论不变,前提收窄了)**:那条短路现在**只在 `win` 家族上** * (`dist/tools/fs/safety.js` 的 `canonicalizeTarget`:`family === "win" && isUncSpelling(...)` 才折 UNC * 并直接返回);在 `posix` 家族上,同一个反斜杠 UNC 拼法在更早一步的 `win32NamespaceScreen` 上就被 * **拒**(`unc_on_posix_tree`),压根到不了短路。⇒ 今日部署形(posix 树)这条腿的暴露面比当年更小, * 而本臂的 POSIX-absolute 收敛**继续是对的**:它挡的是「未经 env 解析的 key 拿到 name-keyed 放行」, * 在两个家族上都成立,且方向仍是 fail-closed。不改实现。 * The probe is consulted by the gate RIGHT BEFORE it would ask (after the exempt/accept dir layering — dist 亲读:true ⇒ * allow with decisionReason "rule", a THROWN probe ⇒ not exempt = fail-closed to ask). Same canonical * toolName key space as the ask-policy layer's exemption probe — ONE grant serves both layers; the gate-level * check is what makes "allow all session" bite SAME-TURN for the parent and inherited child tasks (the ask * path is never re-entered). (codex M1 的边界注记:sensitive-path DENY 腿与本探针的「豁免越不过 deny」 * 保证仍成立——但那条腿已随 [ref] 搬到 governance 层,deny-wins 是 tightenTaskSpec 折叠给的,不再依赖 * 本 wiring 的组合点。) */ isExempt?: (toolName: string, canonicalPath: string) => boolean | Promise; } export declare function deriveSettingsPolicy(settings: ParsedTaskSettings, gate?: FsWriteGateWiring, workflowGate?: WorkflowGateWiring): { toolPolicy?: ToolPolicy; handsReadOnly?: boolean; enablePlanMode?: boolean; }; /** * Apply a client's parsed per-request settings onto a base `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link * tightenTaskSpec} pass (so the permission override composes deny-wins onto the baseline — TRAP #1 — and * `handsReadOnly`/`onAsk` can only narrow). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any override would * LOOSEN a stricter base — surfaced by the caller as a 4xx (a client settings stamp that tries to widen is a * client error, never a silent weakening). `model`/`outputStyle` are applied directly (model is the caller's * choice WITHIN the catalog — the caller gates it before calling; outputStyle appends to the system prompt). * * Returns `base` untouched when the parsed settings project nothing onto the spec. */ export declare function applyTaskSettings(base: TaskSpec, settings: ParsedTaskSettings, gate?: FsWriteGateWiring, workflowGate?: WorkflowGateWiring): TaskSpec; //# sourceMappingURL=task-settings.d.ts.map