@sema-agent/server 7.78.1 → 7.79.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.
Files changed (55) hide show
  1. package/README.md +6 -0
  2. package/USAGE.md +4 -0
  3. package/dist/approval-card.d.ts +14 -4
  4. package/dist/approval-card.js +9 -2
  5. package/dist/auto-mode-face.d.ts +23 -11
  6. package/dist/auto-mode-face.js +5 -2
  7. package/dist/boot/deferred-sandbox-path-env.d.ts +47 -0
  8. package/dist/boot/deferred-sandbox-path-env.js +1 -5
  9. package/dist/boot/resolve-spec.js +25 -6
  10. package/dist/capabilities/code-review-council.js +2 -1
  11. package/dist/config-catalog.d.ts +18 -1
  12. package/dist/config-catalog.js +3 -3
  13. package/dist/config.js +4 -3
  14. package/dist/env-facts.d.ts +61 -0
  15. package/dist/env-facts.js +32 -6
  16. package/dist/execution-lane-caps.d.ts +12 -0
  17. package/dist/execution-lane-caps.js +8 -0
  18. package/dist/governance-http-status.d.ts +27 -0
  19. package/dist/governance-http-status.js +11 -0
  20. package/dist/http/routes/approvals-assistant.d.ts +13 -0
  21. package/dist/http/routes/approvals-assistant.js +35 -0
  22. package/dist/http/server.js +7 -13
  23. package/dist/main.js +4 -3
  24. package/dist/model-select.d.ts +4 -0
  25. package/dist/model-select.js +2 -1
  26. package/dist/observability/fail-open.d.ts +4 -0
  27. package/dist/observability/fail-open.js +4 -0
  28. package/dist/parked-decide.d.ts +7 -2
  29. package/dist/plugins/host-workspace-registry.d.ts +27 -0
  30. package/dist/plugins/host-workspace-registry.js +27 -0
  31. package/dist/plugins/one-time-migrations.d.ts +3 -0
  32. package/dist/plugins/one-time-migrations.js +18 -11
  33. package/dist/plugins/remote-env-adb.d.ts +11 -0
  34. package/dist/plugins/remote-env-adb.js +1 -1
  35. package/dist/plugins/remote-env-device.d.ts +11 -0
  36. package/dist/plugins/remote-env-device.js +1 -1
  37. package/dist/plugins/remote-env-host.d.ts +26 -0
  38. package/dist/plugins/remote-env-host.js +42 -2
  39. package/dist/plugins/remote-env-k8s.d.ts +11 -0
  40. package/dist/plugins/remote-env-k8s.js +1 -1
  41. package/dist/plugins/remote-env-ssh.d.ts +11 -0
  42. package/dist/plugins/remote-env-ssh.js +1 -1
  43. package/dist/run-local.js +2 -1
  44. package/dist/runs.d.ts +51 -0
  45. package/dist/runs.js +1 -10
  46. package/dist/spec-fields.d.ts +2 -2
  47. package/dist/task-a2a.js +8 -6
  48. package/dist/task-cwd.d.ts +24 -0
  49. package/dist/task-cwd.js +9 -0
  50. package/dist/task-mcp.js +5 -3
  51. package/dist/task-settings.d.ts +31 -1
  52. package/dist/task-settings.js +22 -11
  53. package/dist/tool-axis-words.d.ts +30 -0
  54. package/dist/tool-axis-words.js +9 -0
  55. package/package.json +2 -2
@@ -78,7 +78,7 @@ export class RemoteK8sExecutionEnv {
78
78
  this.cwd = this.cfg.mountPath;
79
79
  this.homeDir = config.homeDir;
80
80
  this.deps = deps ?? {};
81
- this.capabilities = { isolation: true, suspendable: !!this.cfg.s3Snapshot };
81
+ this.capabilities = { isolation: true, suspendable: !!this.cfg.s3Snapshot, canonicalPathAuthoritative: true };
82
82
  const bgMax = Math.max(1, Math.min(K8S_BG_MAX_TIMEOUT_SEC, Math.floor(this.cfg.timeoutMs / 1000)));
83
83
  this.backgroundCapabilities = {
84
84
  supported: true,
@@ -61,9 +61,20 @@ export interface SshEnvDeps {
61
61
  connectOnce?: (signal?: AbortSignal) => Promise<ConnectResult>;
62
62
  }
63
63
  export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
64
+ /**
65
+ * 🔴 **S-354 / core 7.20.0([ref])`canonicalPathAuthoritative: true`** —— 本适配器的 `canonicalPath`
66
+ * 就是**命令真正跑的那块文件系统**的权威答案(`posix-shell-fs` 的 `canonicalPath` 原语,在真机上解到落点)。声明它 = 告诉引擎:整链答不出来是**解析器出了事**
67
+ * (RPC 断、传输掉),不是「一个它看不见的命名空间」⇒ 读边界的执行期复核**拒**,而不是让词法判决站着。
68
+ * 缺席是今天每个适配器的读法(整链沉默 ⇒ 词法判决成立、命令照跑),所以这一声明是**收紧**方向。
69
+ * 严格读法 `=== true`(core `canonicalPathIsAuthoritative`);`isSuspendable` 一族同样严格,对本仓的
70
+ * 布尔字面量零影响(三键都写死字面 `true`/`false`,不存在「非布尔值被读成缺席」那一形 —— 那一形由
71
+ * core 的 `config.execution_env_capability_invalid` 普查响亮播报,本仓的受众表已登记)。
72
+ * 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
73
+ */
64
74
  readonly capabilities: {
65
75
  isolation: boolean;
66
76
  suspendable: boolean;
77
+ canonicalPathAuthoritative: boolean;
67
78
  };
68
79
  /** Working directory; relative paths resolve against it (ExecutionEnv contract). Starts at mountPath. */
69
80
  cwd: string;
@@ -16,7 +16,7 @@ function exitCodeForSignalKill(signal) {
16
16
  return 128 + (signalNumber(`SIG${signal}`) ?? 9);
17
17
  }
18
18
  export class RemoteSshExecutionEnv {
19
- capabilities = { isolation: false, suspendable: false };
19
+ capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true };
20
20
  cwd;
21
21
  conn;
22
22
  sftpPromise;
package/dist/run-local.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { DEFAULT_MODEL_REF } from "./model-select.js";
2
3
  import { terminalProjection } from "@sema-agent/core";
3
4
  import { homedir } from "node:os";
4
5
  import { join } from "node:path";
@@ -429,7 +430,7 @@ export async function runLocal(argv, deps = {}) {
429
430
  const webSearch = webSearchCfg ? createWebSearchBackend(webSearchCfg) : undefined;
430
431
  const taskListLane = createTaskListLane({ logger });
431
432
  const scenarioDeps = {
432
- runner, subRunner, handslessSubRunner, model: "default", skills, repoClient, metrics, logger,
433
+ runner, subRunner, handslessSubRunner, model: DEFAULT_MODEL_REF, skills, repoClient, metrics, logger,
433
434
  ...(webSearch ? { webSearch } : {}),
434
435
  backgroundAgentStore: fileBackend.backgroundAgentStore,
435
436
  taskListStoreFor: taskListLane.storeFor,
package/dist/runs.d.ts CHANGED
@@ -97,6 +97,57 @@ export declare function markChildrenStoppedByUserOnAbort(signal: AbortSignal, ta
97
97
  export declare function isSessionConflictResult(result: {
98
98
  terminal: TerminalCause;
99
99
  }): boolean;
100
+ /**
101
+ * E18 resume-at CALLER error → an HTTP 4xx. A bad/stale resume-at target (the resolved entryId is gone or is not a
102
+ * settled message boundary) surfaces as a FAILED TaskResult carrying a `resume_at.*` errorCode (core swallows the
103
+ * prepare-throw into the result, exactly like a session conflict — runtask catch → status:"failed"+errorCode). The
104
+ * sync path otherwise 200s a failed result; map these to 4xx so a caller mistake reads as one. `not_found` (the
105
+ * entry is genuinely absent) → 404; everything else (not_a_message / conflicts_resume / no_session) → 422.
106
+ *
107
+ * [ref] Phase3: `requireExistingSession` on a genuinely-missing/purged session ALSO surfaces as a failed result
108
+ * (core prepare-task maps the store's `not_found` → `resume.session_not_found`); map it to 404 (the session is gone),
109
+ * the same "caller mistake reads as a 4xx, not a silent 200 fresh run" discipline.
110
+ *
111
+ * [ref] rewind exclusive mode (core 1.292 resumeAtMode:"before"): its two edge rejections ride the same `resume_at.*`
112
+ * prefix (before_target_not_user / before_root_unsupported → 422 via the existing branch, errorCode passed through
113
+ * UNCHANGED for the shell). `rewind_snapshot.unresolvable` (rewindFiles + "before": no snapshot at/above the branch
114
+ * point) is core's third rejection with a DIFFERENT prefix — same caller-mistake shape (prepare-throw, nothing
115
+ * billed), map it to 422 explicitly so it doesn't fall through to a 200-with-failed body.
116
+ *
117
+ * [ref] 件A([ref] 件4): core 5.13.0's org-memory admission rides the SAME prepare-throw pipe
118
+ * (GOVERNANCE_CODES). `memory.admission_denied` is TERMINAL — the principal has no grant on that tenant
119
+ * plane → 403 (a retry cannot change the verdict). `memory.admission_required` is TRANSIENT fail-closed —
120
+ * the directory is unreachable / no resolver is wired → 503 (retry later; the sync leg forwards the
121
+ * result's `retryAfterMs` as body `retryAfterSec`, same family shape as usage.window_exhausted). Exact
122
+ * codes only, no memory.* family grab — an unknown sibling stays a 200-with-failed-body until cataloged.
123
+ *
124
+ * [ref] (core 7.0.0 片2/3, [ref] S-3): the session capture opt-out's four prepare-throw refusals ride the
125
+ * same pipe. `memory.capture_optout_denied` is TERMINAL (the principal's `allowMemoryOptOut` verdict is an
126
+ * explicit `false`, or the posture is `capture-required` / `governed`-with-absent-verdict — a retry cannot
127
+ * change it) → 403, the `memory.admission_denied` twin. `memory.capture_optout_unpersisted` is the
128
+ * declaration arm's "control-plane record could not be written" refusal — a store outage, retry may succeed
129
+ * → 503 (transient fail-closed, the `memory.admission_required` twin; no retryAfterMs on this one — core
130
+ * mints none, so the sync leg forwards no hint rather than inventing one). `config.memory_capture_spelling`
131
+ * is a TaskSpec authoring mistake (the only legal spelling is the string "off") → 400, the
132
+ * `config.memory_erasure_request` family shape. `config.memory_capture_unsupported` is the DEPLOYMENT SHAPE
133
+ * refusing the declaration (remote/per-run execution with no durable capture-record carrier: the record
134
+ * would not survive to a resume replica) → 409 — the request is well-formed, the deployment's state
135
+ * conflicts with it, the `config.refresh_unavailable` family shape ("change the deployment, not the request").
136
+ * Status choices are pinned by the `config.`/`memory.` family status sets in error-code-catalog-live.test.ts.
137
+ *
138
+ * S-125 件⑥(core 7.4.0 [ref]):`rewind.child_scope_unsupported` —— 这条腿是一个**委派子代**,它的文件编辑
139
+ * 记在**根 session** 的文件历史里,所以「把文件还原到某个 entry」是根 session 的动作、不是子代的;core 在
140
+ * prepare 期响亮拒(零计费,文件一个字节没动)。映射 **422**:请求形式合法、语义上找错了执行者 —— 与
141
+ * `resume_at.*` 的「调用方指错了锚」同一族同一档,而不是 404(目标在,只是不归你)也不是 409(部署状态没冲突)。
142
+ * ⚠️ **前缀不吞**:本码前缀是 `rewind.`,与下面那条 `resume_at.` **开集前缀臂**不同族 —— 它不会被那一臂顺手
143
+ * 兜走(兜走会把一条「找错执行者」的拒答成「锚解析不了」),也不是 `rewind_snapshot.`(那是「快照找不到」)。
144
+ * 三个前缀各自成族,逐码对表见 `test/resume-anchor.test.ts` 的 S-125 `rewind.` 族格。
145
+ * 🔴 **可达性如实**:本仓的 HTTP 面**今天铸不出**这条腿 —— 子代由 core 内部的 Agent 工具派生,
146
+ * `fileHistoryLineage` 是 core prepare 的 internals(不在 `TaskSpec` 上),server 从不直接 prepare 一条带
147
+ * lineage 的子代腿。本臂是**分诊面的完备性**(core 说 X ⇒ wire 形是 Y),不是一条今天在跑的路;将来的
148
+ * 委派入口接线那天零改动即生效。同族其余五码今天**仍落 200-with-failed-body**,那是本批**之前**就存在的
149
+ * 缺口(不是本批回归),已在上述对表格里逐码显式登记为债务,并由那道门看住不许陈腐。
150
+ */
100
151
  export declare function resumeAtHttpStatus(result: {
101
152
  terminal: TerminalCause;
102
153
  }): number | undefined;
package/dist/runs.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { runWithVerification, runCascade, defaultTaskRegistry, terminalProjection, DURABLE_AGENT_HANDLE_RE } from "@sema-agent/core";
2
2
  import { failureCodeOf, stripResumeToken } from "./terminal.js";
3
+ import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
3
4
  import { redactErrorMessage } from "./observability/run-terminal-log.js";
4
5
  import { withPrincipal } from "./observability/principal-context.js";
5
6
  import { redactSecrets } from "./trace/redact.js";
@@ -66,16 +67,6 @@ export function isSessionConflictResult(result) {
66
67
  return result.terminal.code === "conflict";
67
68
  return /stale write|concurrent write|conflict/i.test(result.terminal.message ?? "");
68
69
  }
69
- const GOVERNANCE_HTTP_STATUS = {
70
- "memory.admission_denied": 403,
71
- "memory.admission_required": 503,
72
- "memory.capture_optout_denied": 403,
73
- "config.tool_mount_denied": 400,
74
- "config.locked_key": 400,
75
- "usage.window_exhausted": null,
76
- "config.compliance_required": null,
77
- "config.compliance_denied": null,
78
- };
79
70
  export function resumeAtHttpStatus(result) {
80
71
  const code = failureCodeOf(result.terminal);
81
72
  if (code === undefined)
@@ -37,8 +37,8 @@ export declare function normalizeAttachments(v: unknown): TaskSpec["attachments"
37
37
  */
38
38
  export declare function normalizeResumeAtMode(v: unknown, resumeAtResolved: boolean): "at" | "before" | undefined;
39
39
  /** [ref] 防御上限:一份请求最多带多少个 agent 定义。core 侧无上限;这里封一个防御值 —— 每个定义都进
40
- * 系统提示(whenToUse/skills/systemPrompt 全是 prompt 载荷),不设界就是 per-turn token 成本洞(与
41
- * 上限)。32 足够覆盖 CC 式 `.claude/agents/` 全家桶还有余量。⚠️ 这条帽守的是 **agent 定义**的条数
40
+ * 系统提示(whenToUse/skills/systemPrompt 全是 prompt 载荷),不设界就是 per-turn token 成本洞。
41
+ * 32 足够覆盖 CC 式 `.claude/agents/` 全家桶还有余量。⚠️ 这条帽守的是 **agent 定义**的条数
42
42
  * (每个定义的 whenToUse/systemPrompt 都是无条件进系统提示的 prompt 载荷),与 [ref] 退役的
43
43
  * **技能**条数帽不是同一轴:技能清单由引擎按预算渲染(超了裁描述),agent 定义没有那个预算面。 */
44
44
  export declare const MAX_TASK_AGENTS = 32;
package/dist/task-a2a.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { resolveComplianceDenies } from "@sema-agent/core";
2
2
  import { isUsableWireName } from "./wire-name-bound.js";
3
3
  import { HttpError } from "./security.js";
4
+ import { isToolAxisIrreversibility, isToolEffect } from "./tool-axis-words.js";
5
+ import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
4
6
  export const MAX_REQUEST_A2A_PEERS = 32;
5
7
  export function a2aInjectionHonored(config) {
6
8
  if (config.lockedConfigKeys?.includes("a2a") === true)
@@ -49,16 +51,16 @@ function normalizeA2aPeer(e) {
49
51
  if (!ax || typeof ax !== "object" || Array.isArray(ax))
50
52
  return null;
51
53
  const a = ax;
52
- if (a.effect !== undefined && a.effect !== "read" && a.effect !== "write" && a.effect !== "idempotent")
54
+ if (a.effect !== undefined && !isToolEffect(a.effect))
53
55
  return null;
54
56
  if (a.egress !== undefined && typeof a.egress !== "boolean")
55
57
  return null;
56
- if (a.irreversibility !== undefined && a.irreversibility !== "always" && a.irreversibility !== "never")
58
+ if (a.irreversibility !== undefined && !isToolAxisIrreversibility(a.irreversibility))
57
59
  return null;
58
60
  const entry = {
59
- ...(a.effect !== undefined ? { effect: a.effect } : {}),
60
- ...(a.egress !== undefined ? { egress: a.egress } : {}),
61
- ...(a.irreversibility !== undefined ? { irreversibility: a.irreversibility } : {}),
61
+ ...(isToolEffect(a.effect) ? { effect: a.effect } : {}),
62
+ ...(typeof a.egress === "boolean" ? { egress: a.egress } : {}),
63
+ ...(isToolAxisIrreversibility(a.irreversibility) ? { irreversibility: a.irreversibility } : {}),
62
64
  };
63
65
  if (Object.keys(entry).length > 0)
64
66
  axes[skill] = entry;
@@ -91,7 +93,7 @@ export function assertRequestA2aUnlocked(bodyA2a, lockedKeys) {
91
93
  return;
92
94
  if (bodyA2a === undefined || bodyA2a === null)
93
95
  return;
94
- throw new HttpError(400, "`a2aPeers` is administratively locked by this deployment (locked key \"a2a\") — task-supplied A2A peers are refused; locks are config-time and there is no request-time unlock", {
96
+ throw new HttpError(GOVERNANCE_HTTP_STATUS["config.locked_key"], "`a2aPeers` is administratively locked by this deployment (locked key \"a2a\") — task-supplied A2A peers are refused; locks are config-time and there is no request-time unlock", {
95
97
  code: "config.locked_key",
96
98
  });
97
99
  }
@@ -18,6 +18,30 @@ export declare const MAX_CWD_CHARS = 4096;
18
18
  * 先改这里**:把判据换成 core 的 `isAbsoluteForFamily`,并把家族从这条 cwd 自己的拼法取。
19
19
  * 钉见 `test/task-cwd.test.ts` 的「[ref] 路径家族接缝」格(实测读数,不是推断)。 */
20
20
  export declare function isValidCwd(cwd: unknown): cwd is string;
21
+ /**
22
+ * 词法归一:纯字符串运算,折 `.` / `..` / 重复分隔符,**不碰 fs**。
23
+ * `posix.normalize` 把 `..` 在根部截断(`/../x` → `/x`),与「根之上没有东西」的语义一致;尾部分隔符
24
+ * 统一剥掉(根 `/` 除外),让同一目标只有一个 key —— 前缀判域(见 {@link dirCoversPath})靠的就是
25
+ * key 的唯一性。
26
+ *
27
+ * 🔴 **单点**(S-351 起):此前它的一份私有副本住在 `boot/deferred-sandbox-path-env.ts`(沙箱代理的
28
+ * `absolutePath` 词法臂),而写门豁免域与任务根的判域现在读的是同一条律 —— 两份各自归一就是两个 key
29
+ * 空间,而「豁免域盖没盖住任务根」这种判断在两个 key 空间里可以同时答是和否。
30
+ * (core 内部有同形的 `normalizeAbsPathLexically`,但未从包根导出;此处是 Node 标准库的等价运算,
31
+ * 不是它的抄本——若日后 core 导出,这里应改为直接复用。)
32
+ */
33
+ export declare function normalizeAbsolutePathLexically(p: string): string;
34
+ /**
35
+ * **词法**判域:`dir` 是不是 `target` 本身或它的祖先(= `target` ⊆ `dir`,「等于 ∪ 后代」)。
36
+ *
37
+ * 这是 core `assertExemptDirsDoNotCoverRoot` 那条律的本仓同形(亲读安装树
38
+ * `dist/core/fs-write-gate-policy.js:11-28` → `swallowedFenceRoot` → `isWithin(root, p)`:
39
+ * 先各自词法归一,再判 `p === root` 或 `p` 以 `root + "/"` 起头)。**两端都只做词法**,不 realpath ——
40
+ * 写门的构造期本来就没有 I/O(core 同理),承诺 canonical 是承诺一件此刻做不到的事。
41
+ *
42
+ * 段感知由「归一后 + 分隔符前缀」保证:`/a/bc` **不在** `/a/b` 域内。
43
+ */
44
+ export declare function dirCoversPath(dir: string, target: string): boolean;
21
45
  /**
22
46
  * The deployment-level gate: does THIS deployment honor a caller-supplied `cwd`? TRUE only for the single-user
23
47
  * `host` lane (see module header). All cloud/container lanes + any multi-tenant deployment → FALSE (cwd ignored).
package/dist/task-cwd.js CHANGED
@@ -8,6 +8,15 @@ export function isValidCwd(cwd) {
8
8
  path.isAbsolute(cwd) &&
9
9
  !cwd.split(/[/\\]/).includes(".."));
10
10
  }
11
+ export function normalizeAbsolutePathLexically(p) {
12
+ const collapsed = path.posix.normalize(p.replace(/^\/+/, "/"));
13
+ return collapsed.length > 1 ? collapsed.replace(/\/+$/, "") : collapsed;
14
+ }
15
+ export function dirCoversPath(dir, target) {
16
+ const root = normalizeAbsolutePathLexically(dir);
17
+ const p = normalizeAbsolutePathLexically(target);
18
+ return p === root || p.startsWith(root.endsWith("/") ? root : `${root}/`);
19
+ }
11
20
  export function cwdHonored(config) {
12
21
  return config.remoteExec?.provider === "host" && config.requirePrincipal !== true;
13
22
  }
package/dist/task-mcp.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { mcpInjectionDroppedNotice, resolveComplianceDenies } from "@sema-agent/core";
2
2
  import { isUsableWireName } from "./wire-name-bound.js";
3
3
  import { HttpError } from "./security.js";
4
+ import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
4
5
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "./mcp-content-origin.js";
5
6
  import { readMcpSourceLabel } from "./mcp-source-label.js";
6
7
  import { readMcpToolFaces } from "./mcp-tool-faces.js";
8
+ import { isToolAxisIrreversibility, isToolEffect } from "./tool-axis-words.js";
7
9
  export const MAX_REQUEST_MCP_SERVERS = 32;
8
10
  export const MAX_REQUEST_MCP_DROP_NAMES = MAX_REQUEST_MCP_SERVERS;
9
11
  export function mcpInjectionHonored(config) {
@@ -72,9 +74,9 @@ function normalizeServer(e, noteBadOrigin) {
72
74
  continue;
73
75
  const a = ax;
74
76
  const entry = {
75
- ...(a.effect === "read" || a.effect === "write" || a.effect === "idempotent" ? { effect: a.effect } : {}),
77
+ ...(isToolEffect(a.effect) ? { effect: a.effect } : {}),
76
78
  ...(typeof a.egress === "boolean" ? { egress: a.egress } : {}),
77
- ...(a.irreversibility === "always" || a.irreversibility === "never" ? { irreversibility: a.irreversibility } : {}),
79
+ ...(isToolAxisIrreversibility(a.irreversibility) ? { irreversibility: a.irreversibility } : {}),
78
80
  };
79
81
  if (Object.keys(entry).length > 0)
80
82
  axes[tool] = entry;
@@ -123,7 +125,7 @@ export function assertRequestMcpUnlocked(bodyMcp, lockedKeys) {
123
125
  return;
124
126
  if (bodyMcp === undefined || bodyMcp === null)
125
127
  return;
126
- throw new HttpError(400, "`mcpServers` is administratively locked by this deployment (locked key \"mcp\") — task-supplied MCP servers are refused; locks are config-time and there is no request-time unlock", {
128
+ throw new HttpError(GOVERNANCE_HTTP_STATUS["config.locked_key"], "`mcpServers` is administratively locked by this deployment (locked key \"mcp\") — task-supplied MCP servers are refused; locks are config-time and there is no request-time unlock", {
127
129
  code: "config.locked_key",
128
130
  });
129
131
  }
@@ -119,10 +119,40 @@ export declare function effectivePermissionMode(body: {
119
119
  * 把它折进来会逼出一个 `undefined` 返回值,而那正是「写 off」与「不写」被混同的入口(core 的
120
120
  * 缺席默认是 off,但**写**一个 off 会成为 governance 的 base,与缺席不是同一件事)。
121
121
  *
122
- * 🔴 **闭集 exhaustive switch,无 default 臂**:五个模式词是封闭词表,新增一个模式而不更新本表
122
+ * 🔴 **闭集穷举表,无 default 臂**:五个模式词是封闭词表,新增一个模式而不更新本表
123
123
  * 是**编译错误**([ref] 的安全轴纪律:词表的未知项不许有静默兜底臂)。
124
124
  */
125
125
  export declare function shellGateForMode(effMode: SettingsPermissionMode): "off" | "classify";
126
+ /**
127
+ * [ref] —— 生效模式 → `TaskSpec.writeFace`(**写**面的 roots 容纳判断)。`"open"` = 工作区根之外也能写;
128
+ * `undefined` = **不写这个键**(core 缺省 `"roots"`,与 7.77.0 及以前逐字节相同)。表逐字在
129
+ * {@link MODE_ENGINE_FACES},裁定与硬边界写在 `resolve-spec.ts` 的写点上。
130
+ *
131
+ * 🔴 与 {@link shellGateForMode} 同律:入参非可选 —— 「表态缺席」不是这张表的一行(那是调用点判的
132
+ * 「整个键都不写」),把它折进来就会逼出第二个 `undefined` 语义。
133
+ */
134
+ export declare function writeFaceForMode(effMode: SettingsPermissionMode): "open" | undefined;
135
+ /**
136
+ * [ref] ① —— 生效模式 → `TaskSpec.autoModeRequested`(auto **意图**座,CC 250「用户开」半场)。
137
+ * `true` 只有 auto 一行;`undefined` = 不写键。表逐字在 {@link MODE_ENGINE_FACES}。
138
+ */
139
+ export declare function autoModeRequestedForMode(effMode: SettingsPermissionMode): true | undefined;
140
+ /**
141
+ * S-346 —— [ref] 技能清单预算两旋钮的**值域谓词**(一对具名单铸点)。
142
+ *
143
+ * 两层同问这一只:FRESH 提交面的 400 门(`http/server.ts`)问「合不合法」,RESUME 宽容层
144
+ * ({@link parseTaskSettings})问「收不收下」—— 这两句话在这两个键上是**同一个问题的正反面**
145
+ * (400 的条件恰是受理条件的否定)。修前它们各写一遍字面比较,而承重的两件事
146
+ * (`Number.isFinite` 挡 `NaN`/`Infinity`、`Number.isSafeInteger` 与引擎受理域**同字**)
147
+ * 靠两份注释各自记着 —— 任一处漏一个判据,同一个坏值就会在一条腿上 400、在另一条腿上被收下。
148
+ *
149
+ * 值域**抄 CC settings schema**,不自创;server 侧零加工(不夹紧、不换算、不复刻 core 的默认值),
150
+ * 判据全文见两个调用点的顶注。
151
+ */
152
+ export declare function skillListingBudgetFractionOk(v: unknown): v is number;
153
+ /** {@link skillListingBudgetFractionOk} 的孪生(`int().positive()`;`isSafeInteger` 而不是 `isInteger` 的
154
+ * 理由见那里)。 */
155
+ export declare function skillListingMaxDescOk(v: unknown): v is number;
126
156
  /** The validated, service-trusted subset of `SemaSettings` we project onto the spec. `env` is parsed only to REPORT
127
157
  * it as received-but-deferred when off the host lane (never silently dropped); a MALFORMED `hooks` likewise reports
128
158
  * deferred (submit 路径另有 400 fail-loud),valid `hooks` 进 applied shape(hook-runner 阶段一)。 */
@@ -35,15 +35,26 @@ export function effectivePermissionMode(body, settings) {
35
35
  return requested;
36
36
  }
37
37
  export function shellGateForMode(effMode) {
38
- switch (effMode) {
39
- case "bypassPermissions":
40
- return "off";
41
- case "auto":
42
- case "default":
43
- case "acceptEdits":
44
- case "plan":
45
- return "classify";
46
- }
38
+ return MODE_ENGINE_FACES[effMode].shellGate;
39
+ }
40
+ const MODE_ENGINE_FACES = {
41
+ bypassPermissions: { shellGate: "off", writeFace: "open", autoModeRequested: undefined },
42
+ auto: { shellGate: "classify", writeFace: undefined, autoModeRequested: true },
43
+ default: { shellGate: "classify", writeFace: undefined, autoModeRequested: undefined },
44
+ acceptEdits: { shellGate: "classify", writeFace: undefined, autoModeRequested: undefined },
45
+ plan: { shellGate: "classify", writeFace: undefined, autoModeRequested: undefined },
46
+ };
47
+ export function writeFaceForMode(effMode) {
48
+ return MODE_ENGINE_FACES[effMode].writeFace;
49
+ }
50
+ export function autoModeRequestedForMode(effMode) {
51
+ return MODE_ENGINE_FACES[effMode].autoModeRequested;
52
+ }
53
+ export function skillListingBudgetFractionOk(v) {
54
+ return typeof v === "number" && Number.isFinite(v) && v > 0 && v <= 1;
55
+ }
56
+ export function skillListingMaxDescOk(v) {
57
+ return typeof v === "number" && Number.isSafeInteger(v) && v > 0;
47
58
  }
48
59
  export const MAX_SETTINGS_OUTPUT_STYLE_CHARS = 16_384;
49
60
  export const MAX_SETTINGS_PERMISSION_RULES = 1_000;
@@ -162,10 +173,10 @@ export function parseTaskSettings(raw) {
162
173
  if (hooksConfig)
163
174
  out.hooks = hooksConfig;
164
175
  const fraction = s.skillListingBudgetFraction;
165
- if (typeof fraction === "number" && Number.isFinite(fraction) && fraction > 0 && fraction <= 1)
176
+ if (skillListingBudgetFractionOk(fraction))
166
177
  out.skillListingBudgetFraction = fraction;
167
178
  const maxDesc = s.skillListingMaxDescChars;
168
- if (typeof maxDesc === "number" && Number.isSafeInteger(maxDesc) && maxDesc > 0)
179
+ if (skillListingMaxDescOk(maxDesc))
169
180
  out.skillListingMaxDescChars = maxDesc;
170
181
  return { settings: Object.keys(out).length > 0 ? out : undefined, deferred };
171
182
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * S-249(DT-13)—— per-tool **风险轴覆写**的值域词表与谓词,**两面共用一份**。
3
+ *
4
+ * ## 病(修前)
5
+ * `task-mcp.ts` 与 `task-a2a.ts` 各自在请求腿上逐键校验同一个三成员结构
6
+ * (`effect` / `egress` / `irreversibility`),而两个**闭集词表**是**手抄的字面串**:四份字面量
7
+ * (两文件 × 两个词轴),没有一份钉在 core 的型上。后果不是「今天判错了」,而是**判据比声明窄**这件事
8
+ * 没有任何门看得见 —— core 哪天给 `ToolEffect` 加一个词(或把 `irreversibility` 的两词改成三词,它在
9
+ * `AskOrigin` 那一面**已经**是 `never|maybe|always` 三词了),两条请求腿会把那个**合法**的新词
10
+ * **静默剔除**(MCP 腿)或**整条 server 判假**(A2A 腿),而 tsc 一声不吭。
11
+ *
12
+ * ## 修法
13
+ * 词表留在本仓(运行期要它),但**两向钉死在 core 的型上**:core 加词 ⇒ 正向门红;本表留了一个 core
14
+ * 已经没有的词 ⇒ 反向门红。两个面共用**同一只**谓词,于是「什么算一个合法的轴值」在本仓只有一个答案。
15
+ *
16
+ * 🔴 **只管值域,不管处置**:两条腿对坏值的处置**刻意不同**(MCP = 丢这一键、留 server;A2A = 整条拒),
17
+ * 判据与理由各写在各自的调用点。合并处置会抹掉那条真实的分歧(MCP 的 `toolAxes` 有 server-hint 腿可折,
18
+ * A2A 收下的就是最终值),而分歧本身是裁定过的。
19
+ */
20
+ import type { McpServerSpec, ToolEffect } from "@sema-agent/core";
21
+ /** 一条 per-tool 轴覆写的形。取 MCP 那一面作代表 —— 下面的同型钉证明两面逐字同声明。 */
22
+ export type ToolAxisEntry = NonNullable<McpServerSpec["toolAxes"]>[string];
23
+ /** 轴覆写上那个**两词**的不可逆档(⚠️ 与 `AskOrigin` 面的**三词** `never|maybe|always` 不是同一个词表:
24
+ * 覆写面只收两极,`maybe` 是引擎按工具自己的声明定的,调用方说不出口)。 */
25
+ export type ToolAxisIrreversibility = NonNullable<ToolAxisEntry["irreversibility"]>;
26
+ /** 值是不是一个合法的 `effect` 词(两条请求腿共用)。 */
27
+ export declare function isToolEffect(v: unknown): v is ToolEffect;
28
+ /** 值是不是一个合法的**覆写面** `irreversibility` 词(两条请求腿共用)。 */
29
+ export declare function isToolAxisIrreversibility(v: unknown): v is ToolAxisIrreversibility;
30
+ //# sourceMappingURL=tool-axis-words.d.ts.map
@@ -0,0 +1,9 @@
1
+ const TOOL_EFFECT_WORDS = ["read", "write", "idempotent"];
2
+ const TOOL_AXIS_IRREVERSIBILITY_WORDS = ["always", "never"];
3
+ export function isToolEffect(v) {
4
+ return typeof v === "string" && TOOL_EFFECT_WORDS.includes(v);
5
+ }
6
+ export function isToolAxisIrreversibility(v) {
7
+ return typeof v === "string" && TOOL_AXIS_IRREVERSIBILITY_WORDS.includes(v);
8
+ }
9
+ //# sourceMappingURL=tool-axis-words.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.78.1",
3
+ "version": "7.79.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -54,7 +54,7 @@
54
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
55
55
  },
56
56
  "dependencies": {
57
- "@sema-agent/core": "7.19.0",
57
+ "@sema-agent/core": "7.20.0",
58
58
  "@sema-agent/settings-schema": "2.1.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",