@sema-agent/server 7.66.0 → 7.67.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 (38) hide show
  1. package/USAGE.md +6 -3
  2. package/dist/approval-card.d.ts +8 -0
  3. package/dist/approval.d.ts +3 -0
  4. package/dist/approval.js +2 -1
  5. package/dist/boot/resolve-spec.js +12 -5
  6. package/dist/config-catalog.js +2 -2
  7. package/dist/config-center/apply-effective.js +1 -1
  8. package/dist/config-types.d.ts +15 -2
  9. package/dist/config.d.ts +25 -3
  10. package/dist/config.js +58 -30
  11. package/dist/deployment-governance.d.ts +8 -0
  12. package/dist/http/routes/diagnostics.d.ts +27 -2
  13. package/dist/http/routes/diagnostics.js +21 -1
  14. package/dist/http/routes/rules.js +15 -10
  15. package/dist/http/routes/tasks.js +3 -8
  16. package/dist/http/server.js +3 -0
  17. package/dist/observability/fail-open.d.ts +4 -0
  18. package/dist/observability/fail-open.js +4 -0
  19. package/dist/observability/metrics.js +1 -1
  20. package/dist/permission-rule-vocab.d.ts +13 -21
  21. package/dist/permission-rule-vocab.js +6 -5
  22. package/dist/plugins/permission-rule-store-file.d.ts +3 -5
  23. package/dist/plugins/permission-rule-store-file.js +2 -1
  24. package/dist/plugins/permission-rule-store-sql.d.ts +10 -10
  25. package/dist/plugins/permission-rule-store-sql.js +35 -20
  26. package/dist/posture-source.d.ts +57 -0
  27. package/dist/posture-source.js +27 -0
  28. package/dist/read-face-posture.d.ts +9 -4
  29. package/dist/read-face-posture.js +7 -11
  30. package/dist/rules-consent.d.ts +15 -10
  31. package/dist/rules-consent.js +41 -21
  32. package/dist/session-titler.js +3 -12
  33. package/dist/task-settings.d.ts +30 -0
  34. package/dist/task-settings.js +39 -18
  35. package/dist/tool-approval.d.ts +5 -4
  36. package/dist/tool-approval.js +2 -2
  37. package/dist/trace/project.d.ts +4 -3
  38. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedAllowRule, type EffectivePermissionRule, type RemoveResult, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core";
1
+ import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedRule, type RuleBehavior, type RuleCandidate, type EffectivePermissionRule, type RemoveResult, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core";
2
2
  import type { DurableRulePartitionProvider, PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
3
3
  import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./plugins/permission-rule-store-sql.js";
4
4
  /**
@@ -20,10 +20,7 @@ export interface RuleConsentRecordStores {
20
20
  principal: string;
21
21
  approvalId: string;
22
22
  purpose: RuleTicketPurpose;
23
- candidates: ReadonlyArray<{
24
- rule: string;
25
- scope: RuleScope;
26
- }>;
23
+ candidates: ReadonlyArray<RuleCandidate>;
27
24
  ttlMs: number;
28
25
  }): Promise<RuleImportTicket>;
29
26
  /** 一次**认领**(不是「已完成」)。语义与「认领之后怎么办」见 {@link RuleConsentStores.tickets.release}。
@@ -144,9 +141,12 @@ export declare const MAX_LOCAL_IMPORT_BYTES: number;
144
141
  * 救回来",而不是等下一次 prepare。运维旋钮暂不开(没有部署形要求它可调)。
145
142
  */
146
143
  export declare const LOCAL_IMPORT_CLAIM_LEASE_MS = 60000;
147
- /** 一条规则的 wire 身份键(规则文本 + {@link serializeRuleScope} 的判别式串)。两个面(预览 / 兑付回执)
148
- * 用**同一把键**,客户端拿它把两张表 join 起来。 */
144
+ /** 一条规则的 wire 身份键(**态** + 规则文本 + {@link serializeRuleScope} 的判别式串)。两个面
145
+ * (预览 / 兑付回执)用**同一把键**,客户端拿它把两张表 join 起来。
146
+ * 🔴 `behavior` 自 7.67.0 起是这把键的一格(core 7.9.0 [ref]:规则的身份是三元组)—— 少了它,同文本
147
+ * 同作用域的 deny 与 allow 两行会被本口判成 `duplicate_row`,而它们是两条不同的规则。 */
149
148
  export interface LocalImportRuleKey {
149
+ behavior: RuleBehavior;
150
150
  rule: string;
151
151
  scope: string;
152
152
  }
@@ -502,14 +502,18 @@ export declare function parseRuleScope(text: string): DurableRuleScope | undefin
502
502
  * `adds` **如实上 wire**、不折叠成单值:同一条规则可以被批准过多次(不同 dot、不同来源、不同时刻),
503
503
  * 折成「一个 addedAt」会让「这条规则是我导入的还是我点过卡的」在治理面上不可分。 */
504
504
  export interface PersistedRuleWireRow {
505
+ /** core 7.9.0([ref])—— 这一行是哪一态的规则(`deny` / `ask` / `allow`)。**身份的一格**:
506
+ * 规则的身份是 (behavior, text, scope) 三元组,同文本不同态是两条不同的行(core `sameRuleIdentity`),
507
+ * 所以它既在列举面上,也在**导入/撤销的输入面**上必填 —— 少了它,一次撤销会瞄准同文本的另一态。 */
508
+ behavior: PersistedRule["behavior"];
505
509
  rule: string;
506
510
  /** {@link serializeRuleScope} 的判别式串。 */
507
511
  scope: string;
508
- tool: PersistedAllowRule["tool"];
509
- match: PersistedAllowRule["match"];
512
+ tool: PersistedRule["tool"];
513
+ match: PersistedRule["match"];
510
514
  /** 去规范化的命令(core 的匹配器信的就是它 + `match`,不是重新解析 `rule`)。 */
511
515
  command: string;
512
- adds: PersistedAllowRule["adds"];
516
+ adds: PersistedRule["adds"];
513
517
  /** [ref](core 7.5.0):这一行来自哪个源。**由 `scope` 派生**(`global`⟺`user`、`project`⟺
514
518
  * `project`、`session`⟺`session`),不是行上另存的第二个字节 —— core 的 `ruleSourceOf` 是整条映射。
515
519
  * 今天本部署只接 durable 一格分区 ⇒ 值域实为 `user | project`。
@@ -600,6 +604,7 @@ export interface RuleConsentLane {
600
604
  */
601
605
  removeRule(input: {
602
606
  principal: RuleBucketRef;
607
+ behavior: RuleBehavior;
603
608
  rule: string;
604
609
  scope: RuleScope;
605
610
  }): Promise<RemoveResult>;
@@ -4,7 +4,7 @@ import { recordFailOpen } from "./observability/fail-open.js";
4
4
  import { createLogger } from "./observability/logger.js";
5
5
  import { MAX_CWD_CHARS } from "./task-cwd.js";
6
6
  import { CONTROL_AND_BIDI_CHARS, hasBidiControls } from "./text-bidi.js";
7
- import { confirmRuleApproval, parseAllowRuleText, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, removePersistedRule, effectiveOrThrow, } from "@sema-agent/core";
7
+ import { confirmRuleApproval, RULE_BEHAVIORS, parseRuleText, ruleReachesProgramRun, ruleToolGrammarOf, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, removePersistedRule, effectiveOrThrow, readRowBehavior, } from "@sema-agent/core";
8
8
  import { buildRulePayloadHash } from "./plugins/permission-rule-store-sql.js";
9
9
  export const RULE_IMPORT_TICKET_TTL_MS = 10 * 60_000;
10
10
  export const RULE_IMPORT_RETRY_AFTER_SEC = 2;
@@ -18,7 +18,7 @@ const LocalImportOutcomeSchema = z
18
18
  .object({
19
19
  members: z.array(z
20
20
  .object({
21
- key: z.object({ rule: z.string().min(1), scope: z.string().min(1) }).strict(),
21
+ key: z.object({ behavior: z.preprocess((v) => readRowBehavior(v) ?? v, z.enum(RULE_BEHAVIORS)), rule: z.string().min(1), scope: z.string().min(1) }).strict(),
22
22
  outcome: z.enum(["persisted", "no-op", "refused", "indeterminate"]),
23
23
  reason: z.enum(["store_refused", "walk_failed"]).optional(),
24
24
  detail: z.string().optional(),
@@ -83,6 +83,7 @@ function isSupportedRuleMatch(match) {
83
83
  case "wildcard":
84
84
  return true;
85
85
  case "subpath":
86
+ case "path":
86
87
  return false;
87
88
  default: {
88
89
  const unknown = match;
@@ -177,13 +178,25 @@ function screenLocalImportRow(row) {
177
178
  `${derived.tool}/${derived.match}/${JSON.stringify(derived.command)} but the row says ${String(row.tool)}/${String(row.match)}/${JSON.stringify(row.command)}`,
178
179
  };
179
180
  }
180
- return { rule: derived.rule, scope };
181
+ if (derived.behavior !== row.behavior) {
182
+ return { reason: "row_inconsistent", detail: `this row says behavior ${JSON.stringify(String(row.behavior))} but the parser resolved ${JSON.stringify(derived.behavior)}` };
183
+ }
184
+ return { behavior: derived.behavior, rule: derived.rule, scope };
181
185
  };
182
- const written = parseAllowRuleText(row.rule);
186
+ const written = parseRuleText(row.rule, row.behavior);
183
187
  if ("reject" in written)
184
188
  return { reason: "rule_rejected", detail: `${written.reject.code}: ${written.reject.message}` };
185
- if (written.rule.tool === "Read")
189
+ if (ruleToolGrammarOf(written.rule.tool) !== "command")
190
+ return consistent(written.rule);
191
+ if (written.rule.behavior !== "allow") {
192
+ if (!ruleReachesProgramRun(written.rule, row.command)) {
193
+ return {
194
+ reason: "row_inconsistent",
195
+ detail: `this tightening rule does not speak about the command it names: ${JSON.stringify(written.rule.rule)} vs ${JSON.stringify(row.command)}`,
196
+ };
197
+ }
186
198
  return consistent(written.rule);
199
+ }
187
200
  const pre = precheckCardRuleText(row.rule, row.command);
188
201
  if (pre === undefined) {
189
202
  return { reason: "rule_uncheckable", detail: "the rule text gate could not be asked about this row (its command is absent or not a string) — the row itself is malformed" };
@@ -199,22 +212,29 @@ function buildLocalImportLayers(candidates) {
199
212
  const byRoot = new Map();
200
213
  for (const c of candidates) {
201
214
  const key = c.scope.kind === "global" ? "" : c.scope.root;
202
- const bucket = byRoot.get(key);
203
- if (bucket === undefined)
204
- byRoot.set(key, [c.rule]);
205
- else
206
- bucket.push(c.rule);
215
+ let bucket = byRoot.get(key);
216
+ if (bucket === undefined) {
217
+ bucket = { deny: [], ask: [], allow: [] };
218
+ byRoot.set(key, bucket);
219
+ }
220
+ bucket[c.behavior].push(c.rule);
207
221
  }
208
- return [...byRoot].map(([root, rules]) => ({
222
+ return [...byRoot].map(([root, lists]) => ({
209
223
  layer: root === "" ? "userSettings" : "projectSettings",
210
224
  path: root === "" ? LOCAL_IMPORT_LAYER_PATH : `${LOCAL_IMPORT_LAYER_PATH}:${root}`,
211
225
  root: root === "" ? "/" : root,
212
- readFile: async () => JSON.stringify({ permissions: { allow: rules } }),
226
+ readFile: async () => JSON.stringify({
227
+ permissions: {
228
+ ...(lists.deny.length > 0 ? { deny: lists.deny } : {}),
229
+ ...(lists.ask.length > 0 ? { ask: lists.ask } : {}),
230
+ ...(lists.allow.length > 0 ? { allow: lists.allow } : {}),
231
+ },
232
+ }),
213
233
  }));
214
234
  }
215
235
  function buildLocalImportMembers(members) {
216
236
  return members.map((m) => {
217
- const key = { rule: m.rule, scope: serializeRuleScope(m.scope) };
237
+ const key = { behavior: m.behavior, rule: m.rule, scope: serializeRuleScope(m.scope) };
218
238
  switch (m.status) {
219
239
  case "persisted":
220
240
  return { key, outcome: "persisted" };
@@ -312,7 +332,7 @@ export function createRuleConsentLane(stores, opts) {
312
332
  if (minted === undefined) {
313
333
  return { ok: false, reason: "confirm-refused", detail: "the edited confirmation landed without a minted candidate — nothing to redeem" };
314
334
  }
315
- const canonical = parseAllowRuleText(minted.rule);
335
+ const canonical = parseRuleText(minted.rule, "allow");
316
336
  if ("reject" in canonical) {
317
337
  return { ok: false, reason: "edit-rejected", detail: `the canonical spelling the engine minted does not re-parse: ${canonical.reject.message}` };
318
338
  }
@@ -444,11 +464,11 @@ export function createRuleConsentLane(stores, opts) {
444
464
  for (const row of rows) {
445
465
  const screened = screenLocalImportRow(row);
446
466
  if ("reason" in screened) {
447
- members.push({ key: { rule: row.rule, scope: row.scope }, admitted: false, reason: screened.reason, detail: screened.detail });
467
+ members.push({ key: { behavior: row.behavior, rule: row.rule, scope: row.scope }, admitted: false, reason: screened.reason, detail: screened.detail });
448
468
  continue;
449
469
  }
450
- const key = { rule: screened.rule, scope: serializeRuleScope(screened.scope) };
451
- const dedupKey = `${key.scope}${LOCAL_IMPORT_KEY_SEP}${key.rule}`;
470
+ const key = { behavior: screened.behavior, rule: screened.rule, scope: serializeRuleScope(screened.scope) };
471
+ const dedupKey = `${key.behavior}${LOCAL_IMPORT_KEY_SEP}${key.scope}${LOCAL_IMPORT_KEY_SEP}${key.rule}`;
452
472
  if (seen.has(dedupKey)) {
453
473
  members.push({ key, admitted: false, reason: "duplicate_row", detail: "this (rule, scope) appears earlier in the same batch — the engine would fold the two silently, so it is reported here instead" });
454
474
  continue;
@@ -460,12 +480,12 @@ export function createRuleConsentLane(stores, opts) {
460
480
  if (admitted.length === 0)
461
481
  return { ok: true, preview: { members } };
462
482
  const { preview, approvalId } = await prepareCcImport({ layers: buildLocalImportLayers(admitted), principal, deps });
463
- const engineKeys = new Set(preview.candidates.map((c) => `${serializeRuleScope(c.scope)}${LOCAL_IMPORT_KEY_SEP}${c.rule}`));
483
+ const engineKeys = new Set(preview.candidates.map((c) => `${c.behavior}${LOCAL_IMPORT_KEY_SEP}${serializeRuleScope(c.scope)}${LOCAL_IMPORT_KEY_SEP}${c.rule}`));
464
484
  for (let i = 0; i < members.length; i++) {
465
485
  const m = members[i];
466
486
  if (m === undefined || !m.admitted)
467
487
  continue;
468
- if (engineKeys.has(`${m.key.scope}${LOCAL_IMPORT_KEY_SEP}${m.key.rule}`))
488
+ if (engineKeys.has(`${m.key.behavior}${LOCAL_IMPORT_KEY_SEP}${m.key.scope}${LOCAL_IMPORT_KEY_SEP}${m.key.rule}`))
469
489
  continue;
470
490
  const why = preview.skipped.find((s) => s.rule === m.key.rule)?.reason ?? "the engine's import gate did not take it, and said nothing about it";
471
491
  localImportLogger.warn("local_import_row_admitted_but_not_importable", {
@@ -683,12 +703,12 @@ export function createRuleConsentLane(stores, opts) {
683
703
  },
684
704
  async listRules(bucket) {
685
705
  const view = await effectiveOrThrow(storeForBucket(stores.provider, bucket));
686
- const rows = view.rules.map((r) => ({ rule: r.rule, scope: serializeRuleScope(r.scope), tool: r.tool, match: r.match, command: r.command, adds: r.adds, source: r.source, status: r.status }));
706
+ const rows = view.rules.map((r) => ({ behavior: r.behavior, rule: r.rule, scope: serializeRuleScope(r.scope), tool: r.tool, match: r.match, command: r.command, adds: r.adds, source: r.source, status: r.status }));
687
707
  rows.sort((a, b) => (a.scope < b.scope ? -1 : a.scope > b.scope ? 1 : a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : 0));
688
708
  return { rows, rev: view.rev };
689
709
  },
690
710
  async removeRule(input) {
691
- return await removePersistedRule({ rule: input.rule, scope: input.scope, principal: input.principal, provider: stores.provider });
711
+ return await removePersistedRule({ behavior: input.behavior, rule: input.rule, scope: input.scope, principal: input.principal, provider: stores.provider });
692
712
  },
693
713
  };
694
714
  }
@@ -1,10 +1,10 @@
1
1
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
2
+ import { recordFailOpen } from "./observability/fail-open.js";
2
3
  export const TITLE_MAX_CHARS = 80;
3
4
  const OBJECTIVE_SNIPPET_CHARS = 600;
4
5
  const LLM_TIMEOUT_MS = 15_000;
5
6
  const NO_ROW_RETRY_MS = 20_000;
6
7
  const FAIL_CAUSE_KEY_CHARS = 80;
7
- const FAIL_CAUSE_CAP = 100;
8
8
  const SEEN_CAP = 10_000;
9
9
  export function sanitizeTitle(raw) {
10
10
  const oneLine = raw.replace(/[\r\n]+/g, " ").replace(CONTROL_AND_BIDI_STRIP_RE, "").trim();
@@ -22,18 +22,9 @@ export function buildTitlePrompt(objective) {
22
22
  }
23
23
  export function createSessionTitler(deps) {
24
24
  const seen = new Set();
25
- const warnedCauses = new Set();
26
25
  const logFailure = (sessionId, error) => {
27
- const cause = error.slice(0, FAIL_CAUSE_KEY_CHARS);
28
- const meta = { sessionId, error: error.slice(0, 200) };
29
- if (warnedCauses.has(cause)) {
30
- deps.logger?.debug?.("session_title_failed", meta);
31
- return;
32
- }
33
- if (warnedCauses.size >= FAIL_CAUSE_CAP)
34
- warnedCauses.clear();
35
- warnedCauses.add(cause);
36
- deps.logger?.warn?.("session_title_failed", { ...meta, note: "同 cause 后续降为 debug 级" });
26
+ deps.logger?.debug?.("session_title_failed", { sessionId, error: error.slice(0, 200) });
27
+ recordFailOpen("server.session-titler.gateway-unreachable", error.slice(0, FAIL_CAUSE_KEY_CHARS));
37
28
  };
38
29
  return {
39
30
  maybeTitle(sessionId, objective, model) {
@@ -293,6 +293,36 @@ export interface FsWriteGateWiring {
293
293
  * (⇒ ask) and `acceptEdits` degrades to the `default` arm — the same fail-safe direction as the host lane's
294
294
  * never-created sentinel dir (resolve-spec 修5). */
295
295
  cwd?: string;
296
+ /**
297
+ * 🔴 S-177 / codex r1 [high](验真后修)—— **权限规则**的路径基,与上面那个 `cwd` **刻意分成两个字段**。
298
+ *
299
+ * 两者在 host 腿上常常同值,但它们回答的不是同一个问题,而混用一个字段会让其中一个静默失效:
300
+ * · `cwd` 是**写门**的基。它在「有 remoteExec 但这条 session 还没登记工作目录」时取的是一个
301
+ * **永不创建的哨兵目录**(`resolve-spec` 的 `fs-write-gate-unrooted`)。对写门那是 fail-SAFE 的:
302
+ * 相对目标在哨兵下解析不出真身 ⇒ 落 ask。
303
+ * · `taskRoot` 是**规则模式**的基(core `PathRuleBases.root`)。规则编译器按**词法**匹配,不问目录
304
+ * 存不存在 ⇒ 把哨兵递给它,`deny Edit(src/**)` 会编译成功、指向一个谁也不会写的目录,对真实工作区
305
+ * 的 `<工作区>/src/secret.ts` 这类目标判 allow —— 一条运维亲手写下的收紧规则**静默失效**,方向与写门那条正相反。
306
+ *
307
+ * ⇒ 本字段**只在真任务根已知时**在场。缺席 ⇒ 规则编译器拿不到 `pathBases.root` ⇒ 需要该基的拼法
308
+ * (`Edit(src/**)` / `Edit(/etc/**)`)被 core 以 `unsupported.path_base` **整表拒**(→ 422,fail-loud),
309
+ * 而不是编成一条够不着任何东西的规则。绝对形(`//abs`)与 `~/` 形不需要基,两种部署形都照常编得出。
310
+ */
311
+ taskRoot?: string;
312
+ /**
313
+ * 🔴 S-177 / codex r2 [high](验真后修)—— **执行环境的 home**(core `PathRuleBases.home`,`~/…` 形的基)。
314
+ *
315
+ * core 对缺席的 `home` 回落 `os.homedir()` —— 那是**这个 server 进程**的 home。引擎与执行同机时它就是
316
+ * 对的;**远端车道**(SSH / 容器 / 沙箱)上执行用户的 home 与 server 的 home 是两个目录,于是
317
+ * `deny Edit(~/.sema/**)` 编译成功、却拿 server 的 home 去匹配远端的目标 ⇒ 一条收紧规则**静默失效**
318
+ * (与 `taskRoot` 那条哨兵病同族、同方向)。
319
+ *
320
+ * ⇒ 本字段只在 home **真知道**时在场(引擎与执行同机)。缺席时,`~/` 形的规则由
321
+ * {@link rejectUnmatchableSettingsNames} **响亮拒**(422),绝不回落到 server 的 home。
322
+ * ⚠️ 上游缺口如实登记:core 今天没有「这条规则需要哪些 base」的查询口,也不把 `home` 当必答项 ——
323
+ * 所以「这条规则用了 `~/` 吗」由本仓读**core 自己解析出的规范 command**(`~/` 前缀)来判,不自铸文法。
324
+ */
325
+ taskHome?: string;
296
326
  /** The session scratchpad dir (envFacts.scratchpadDir, [ref]③) — writes there are always auto-allowed. */
297
327
  scratchpadDir?: string;
298
328
  /** [ref]① / core 1.294 exemption seam: the session "don't ask again" probe (approvalExemptionStore.has),
@@ -1,5 +1,5 @@
1
- import { combinePolicies, createAllowDenyPolicy, createFsWriteGatePolicy, createPermissionRulePolicy, tightenTaskSpec, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, } from "@sema-agent/core";
2
- import { findCcRuleFormNames, findUnmatchableToolNames, formatUnmatchableToolNames } from "./config.js";
1
+ import { combinePolicies, createAllowDenyPolicy, createFsWriteGatePolicy, createPermissionRulePolicy, parseRuleText, tightenTaskSpec, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, ruleToolGrammarOf, } from "@sema-agent/core";
2
+ import { findUnmatchableToolNames, formatUnmatchableToolNames } from "./config.js";
3
3
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
4
4
  import { parseHooksConfig } from "./hooks/hook-runner.js";
5
5
  export function coercePermissionMode(raw) {
@@ -225,17 +225,18 @@ export function acceptAppendSystemPrompt(v, warn, packDropsAppend) {
225
225
  }
226
226
  return v;
227
227
  }
228
- const ASK_RULE_SOURCE = "settings.permissions.ask";
229
- function createAskListPolicy(askNames) {
228
+ const RULE_SOURCE_BY_BEHAVIOR = { ask: "settings.permissions.ask", deny: "settings.permissions.deny" };
229
+ function createRuleListPolicy(names, behavior, pathBases) {
230
230
  try {
231
- return createPermissionRulePolicy(askNames.map((name) => ({ rule: name, behavior: "ask", source: ASK_RULE_SOURCE })), {
231
+ return createPermissionRulePolicy(names.map((name) => ({ rule: name, behavior, source: RULE_SOURCE_BY_BEHAVIOR[behavior] })), {
232
232
  defaultAction: "allow",
233
233
  caps: { maxRules: MAX_SETTINGS_PERMISSION_RULES },
234
+ ...(pathBases !== undefined ? { pathBases } : {}),
234
235
  });
235
236
  }
236
237
  catch (e) {
237
238
  const coreMsg = (e instanceof Error ? e.message : String(e)).replace(CONTROL_AND_BIDI_STRIP_RE, "");
238
- throw new Error(`permissions.ask has rule(s) core cannot compile — ${coreMsg}`, { cause: e });
239
+ throw new Error(`${RULE_SOURCE_BY_BEHAVIOR[behavior].replace("settings.", "")} has rule(s) core cannot compile — ${coreMsg}`, { cause: e });
239
240
  }
240
241
  }
241
242
  const WORKFLOW_ASK_NAMES = [RUN_WORKFLOW_TOOL_NAME];
@@ -267,31 +268,51 @@ function createWorkflowPlanDenyPolicy() {
267
268
  nameSets: [{ deny: WORKFLOW_ASK_NAMES }],
268
269
  };
269
270
  }
270
- function rejectUnmatchableSettingsNames(perms) {
271
- for (const [field, names] of [["permissions.ask", perms.ask], ["permissions.deny", perms.deny]]) {
271
+ function rejectUnmatchableSettingsNames(perms, pathBases) {
272
+ if (pathBases?.home === undefined) {
273
+ for (const [field, names, behavior] of [
274
+ ["permissions.ask", perms.ask, "ask"],
275
+ ["permissions.deny", perms.deny, "deny"],
276
+ ["permissions.allow", perms.allow, "allow"],
277
+ ]) {
278
+ for (const name of names ?? []) {
279
+ const parsed = parseRuleText(name, behavior);
280
+ if ("rule" in parsed && ruleToolGrammarOf(parsed.rule.tool) === "path" && parsed.rule.command.startsWith("~/")) {
281
+ throw new Error(`${field} has a home-relative path rule (${name}) but this deployment cannot resolve the EXECUTION environment's home directory — ` +
282
+ `on a remote lane the engine's own home is a different user's directory, so the rule would compile and then match nothing that runs. ` +
283
+ `Spell the path absolutely (${"Edit(//abs/path/**)"} form), or run the task on a lane whose home this worker knows.`);
284
+ }
285
+ }
286
+ }
287
+ }
288
+ for (const [field, names, behavior, consumer] of [
289
+ ["permissions.ask", perms.ask, "ask", "rule-policy"],
290
+ ["permissions.deny", perms.deny, "deny", "rule-policy"],
291
+ ["permissions.allow", perms.allow, "allow", "tool-name-table"],
292
+ ]) {
272
293
  if (!names)
273
294
  continue;
274
- const bad = findUnmatchableToolNames(names);
295
+ const bad = findUnmatchableToolNames(names, { consumer, behavior, ...(pathBases !== undefined ? { pathBases } : {}) });
275
296
  if (bad.length > 0)
276
297
  throw new Error(formatUnmatchableToolNames(field, bad));
277
298
  }
278
- if (perms.allow) {
279
- const bad = findCcRuleFormNames(perms.allow);
280
- if (bad.length > 0)
281
- throw new Error(formatUnmatchableToolNames("permissions.allow", bad));
282
- }
283
299
  }
284
300
  export function deriveSettingsPolicy(settings, gate, workflowGate) {
285
301
  const perms = settings.permissions;
286
302
  if (!perms)
287
303
  return {};
288
304
  const { allow, deny, ask, defaultMode } = perms;
289
- rejectUnmatchableSettingsNames(perms);
305
+ const pathBases = gate?.taskRoot !== undefined || gate?.taskHome !== undefined
306
+ ? { ...(gate.taskRoot !== undefined ? { root: gate.taskRoot } : {}), ...(gate.taskHome !== undefined ? { home: gate.taskHome } : {}) }
307
+ : undefined;
308
+ rejectUnmatchableSettingsNames(perms, pathBases);
290
309
  const policies = [];
291
- if (allow || deny)
292
- policies.push(createAllowDenyPolicy({ ...(allow ? { allow } : {}), ...(deny ? { deny } : {}) }));
310
+ if (allow)
311
+ policies.push(createAllowDenyPolicy({ allow }));
312
+ if (deny)
313
+ policies.push(createRuleListPolicy(deny, "deny", pathBases));
293
314
  if (ask)
294
- policies.push(createAskListPolicy(ask));
315
+ policies.push(createRuleListPolicy(ask, "ask", pathBases));
295
316
  const fsWriteGate = (acceptCwd) => {
296
317
  if (!gate)
297
318
  return undefined;
@@ -126,8 +126,9 @@ export interface ToolApprovalFrame {
126
126
  requiresRealApproval?: true;
127
127
  /**
128
128
  * **S-125③/[ref]**(core 7.5.0;**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 的**出身**:
129
- * 谁提的这一问,一个 core 闭集词(`ASK_ORIGINS` 八员 content_question / unresolvable / org_unavailable /
130
- * org_rule / hook / ask_rule / denial_limit_fallback / policy)。引擎在门上盖章(core d.ts:
129
+ * 谁提的这一问,一个 core 闭集词(`ASK_ORIGINS` **11 员**,core 7.9.0 现值:content_question /
130
+ * unresolvable / org_unavailable / org_rule / rule_store_unavailable / hook / ask_rule /
131
+ * denial_limit_fallback / shell_gate_tighten / safety_tighten / policy)。引擎在门上盖章(core d.ts:
131
132
  * 「engine-stamped at the gate, never a policy's claim」),`AskRequest.origin` 逐字透传。
132
133
  *
133
134
  * 🔴 **echo-only,零消费**:自动车道的资格判据全在 core(`classifierMayAnswer` 的闭表),server
@@ -864,7 +865,7 @@ export declare class ToolApprovalCoordinator {
864
865
  * [ref] / 5.58 复审 F-1 —— OFFER 的**文本座尺**(红先修)。
865
866
  *
866
867
  * 🔴 病灶(真复现,`npm run build --flag=<700 字> && git status`):`batch` 成员的 `segment` 是复合命令
867
- * 那一段的**原字节**,它**不经** `parseAllowRuleText`,所以 core 的 `MAX_RULE_TEXT_CHARS` 对它一个字
868
+ * 那一段的**原字节**,它**不经** `parseRuleText`,所以 core 的 `MAX_RULE_TEXT_CHARS` 对它一个字
868
869
  * 都不管;而卡的 `ApprovalCardSchema` 对四个文本座一律 `.max(MAX_RULE_TEXT_CHARS)`。逐字拷进卡 ⇒
869
870
  * `card_json` 落库 ⇒ `ensureAsk` 当场回读 `safeParse` 判假 ⇒ `persisted-row-unusable` ⇒ **整只 ask 走
870
871
  * park**,重放腿一并跳过该行。与「重扫二轮 §基数帽」是**同一条悬崖的另一条边**(那次是 offer 条数
@@ -885,7 +886,7 @@ export declare class ToolApprovalCoordinator {
885
886
  /** 兑付侧座(single 的 rule/command、command 成员的 rule/command、directoryRead 成员的 rule/directory)任一超尺?
886
887
  * `segment` 与 `uncoveredDetail[].segment` 是纯渲染座,不在此列(截长是诚实省略)。 */
887
888
  private static ruleOfferTextsOverSize;
888
- /** codex r1 [high]([ref]):offer 的自称座与 `parseAllowRuleText(rule)` 派生的三元组逐字一致?(理由见调用点) */
889
+ /** codex r1 [high]([ref]):offer 的自称座与 `parseRuleText(rule, "allow")` 派生的三元组逐字一致?(理由见调用点) */
889
890
  private static ruleOfferDerivedFactsAgree;
890
891
  private static boundRuleOfferTexts;
891
892
  private buildRuleLaneMaterial;
@@ -1,6 +1,6 @@
1
1
  import { contentKindOf } from "./approval-content-kind.js";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
- import { uuidv7, MAX_RULE_TEXT_CHARS, parseAllowRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
3
+ import { uuidv7, MAX_RULE_TEXT_CHARS, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
4
4
  import { precheckCardRuleText } from "./rules-consent.js";
5
5
  import { redactDeep, redactSecrets } from "./trace/redact.js";
6
6
  import { hasBidiControls } from "./text-bidi.js";
@@ -381,7 +381,7 @@ export class ToolApprovalCoordinator {
381
381
  }
382
382
  static ruleOfferDerivedFactsAgree(offer) {
383
383
  const derive = (rule) => {
384
- const p = parseAllowRuleText(rule);
384
+ const p = parseRuleText(rule, "allow");
385
385
  return "rule" in p ? p.rule : undefined;
386
386
  };
387
387
  if (offer.kind === "single") {
@@ -419,9 +419,10 @@ export declare function workspaceChangedEventData(ev: {
419
419
  * typebox schema `ToolRosterDelta`,过了整只透传,没过 ⇒ **整帧不发** + F 类留痕。半张名册变更帧比
420
420
  * 不发更坏:消费端会据它把没读到的行当成「这只工具被卸了」。
421
421
  *
422
- * ⚠️ **如实披露(core 7.8.0 自述)**:7.8.0 的 runtask **还没有**推这条帧([ref] S3 才接线),座位与
423
- * 类型先到货。本臂因此是**前瞻接线**:core 开始推的那一版,server 这一侧零改动即到达消费端;在那之前
424
- * 它永不触发。消费端在那一版之前**不得**依赖本帧(core 契约原文)。
422
+ * ✅ **已转正(core 7.9.0 / [ref] S3)**:引擎自 7.9.0 起**真推**这条帧 —— 驱动在推 `wiring_manifest`
423
+ * 的那一点订阅腿的名册增量座,一次改变名册的 refresh ⇒ **恰一帧**(序在 manifest 之后、那次 refresh 自己的
424
+ * `tool_end` 之前),`delta.fromDigest` = 消费端手上那份名册的 digest。7.66.0 写下的「前瞻接线」披露随本版
425
+ * 退役:本臂现在会真的触发,消费端可以依赖它了。
425
426
  */
426
427
  export declare function toolRosterDeltaEventData(ev: {
427
428
  delta?: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.66.0",
3
+ "version": "7.67.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.8.0",
57
+ "@sema-agent/core": "7.9.0",
58
58
  "@sema-agent/settings-schema": "1.9.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",
@@ -69,7 +69,7 @@
69
69
  "sharp": "^0.35.3"
70
70
  },
71
71
  "devDependencies": {
72
- "@sema-agent/sdk": "8.5.0",
72
+ "@sema-agent/sdk": "8.6.0",
73
73
  "@types/libsodium-wrappers": "^0.7.14",
74
74
  "@types/node": "22.10.2",
75
75
  "@types/pg": "^8.20.0",