@sema-agent/server 7.66.0 → 7.68.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 (48) 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 +13 -6
  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/route-ctx.d.ts +5 -1
  13. package/dist/http/routes/diagnostics.d.ts +27 -2
  14. package/dist/http/routes/diagnostics.js +21 -1
  15. package/dist/http/routes/rules.js +15 -10
  16. package/dist/http/routes/runs.js +1 -1
  17. package/dist/http/routes/tasks.js +4 -8
  18. package/dist/http/server.d.ts +6 -1
  19. package/dist/http/server.js +7 -21
  20. package/dist/mcp-tool-faces.js +11 -2
  21. package/dist/observability/fail-open.d.ts +4 -0
  22. package/dist/observability/fail-open.js +4 -0
  23. package/dist/observability/metrics.js +1 -1
  24. package/dist/permission-rule-vocab.d.ts +13 -21
  25. package/dist/permission-rule-vocab.js +6 -5
  26. package/dist/plugins/permission-rule-store-file.d.ts +3 -5
  27. package/dist/plugins/permission-rule-store-file.js +2 -1
  28. package/dist/plugins/permission-rule-store-sql.d.ts +10 -10
  29. package/dist/plugins/permission-rule-store-sql.js +35 -20
  30. package/dist/posture-source.d.ts +57 -0
  31. package/dist/posture-source.js +27 -0
  32. package/dist/read-face-posture.d.ts +9 -4
  33. package/dist/read-face-posture.js +7 -11
  34. package/dist/rules-consent.d.ts +15 -10
  35. package/dist/rules-consent.js +41 -21
  36. package/dist/runs.d.ts +10 -1
  37. package/dist/runs.js +2 -1
  38. package/dist/session-titler.js +3 -12
  39. package/dist/task-mcp.d.ts +50 -6
  40. package/dist/task-mcp.js +71 -35
  41. package/dist/task-settings.d.ts +30 -0
  42. package/dist/task-settings.js +39 -18
  43. package/dist/tool-approval.d.ts +5 -4
  44. package/dist/tool-approval.js +2 -2
  45. package/dist/trace/engine-notice-wire.d.ts +17 -1
  46. package/dist/trace/engine-notice-wire.js +6 -1
  47. package/dist/trace/project.d.ts +4 -3
  48. package/package.json +3 -3
@@ -1,3 +1,4 @@
1
+ import { buildPostureKnobReading } from "../../posture-source.js";
1
2
  import { buildReadFaceWiring } from "../../read-face-posture.js";
2
3
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
3
4
  import { sendJson, sendError } from "../send.js";
@@ -11,7 +12,21 @@ export function buildServerWiringGates(input) {
11
12
  });
12
13
  return {
13
14
  streamApproval: gate.active ? "active" : gate.reason,
14
- durableApproval: input.durableApproval,
15
+ durableApproval: buildPostureKnobReading(input.durableApproval, {
16
+ knob: "DURABLE_APPROVAL",
17
+ source: input.durableApprovalSource,
18
+ postureMeaning: "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set) composed with a durable store being present: park/resume and the /v1/approvals routes are on by default — set DURABLE_APPROVAL=false to pin them off",
19
+ }),
20
+ streamAskWindowMs: buildPostureKnobReading(input.streamAskWindowMs, {
21
+ knob: "STREAM_ASK_WINDOW_MS",
22
+ source: input.streamAskWindowMsSource,
23
+ postureMeaning: "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set): an approval card waits 24h instead of 5 minutes, so stepping away does not settle the run unattended — set STREAM_ASK_WINDOW_MS to pin your own window",
24
+ }),
25
+ sessionAutoTitle: buildPostureKnobReading(input.sessionAutoTitle, {
26
+ knob: "SESSION_AUTO_TITLE",
27
+ source: input.sessionAutoTitleSource,
28
+ postureMeaning: "this knob has no posture arm — its default is the same on every deployment shape",
29
+ }),
15
30
  checkpointStore: input.checkpointStore !== undefined,
16
31
  };
17
32
  }
@@ -100,6 +115,11 @@ async function handleDiagnosticsBody(req, res, url, ctx, miss) {
100
115
  backend: deps.backend,
101
116
  checkpointStore: deps.checkpointStore,
102
117
  durableApproval: deps.config.durableApproval === true,
118
+ durableApprovalSource: deps.config.durableApprovalSource,
119
+ streamAskWindowMs: deps.config.streamApproval?.windowMs ?? 0,
120
+ streamAskWindowMsSource: deps.config.streamAskWindowMsSource,
121
+ sessionAutoTitle: deps.config.sessionAutoTitle === true,
122
+ sessionAutoTitleSource: deps.config.sessionAutoTitleSource,
103
123
  }),
104
124
  });
105
125
  return;
@@ -5,8 +5,8 @@ import { createLogger } from "../../observability/logger.js";
5
5
  import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
6
6
  import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
7
7
  import { HttpError } from "../../security.js";
8
- import { MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
9
- import { PERSISTED_RULE_MATCHES, PERSISTED_RULE_TOOLS } from "../../permission-rule-vocab.js";
8
+ import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS } from "@sema-agent/core";
9
+ import { PERSISTED_RULE_MATCHES, isPersistedRuleTool } from "../../permission-rule-vocab.js";
10
10
  const logger = createLogger();
11
11
  export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
12
12
  export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
@@ -46,7 +46,8 @@ const LocalImportRowSchema = z
46
46
  .object({
47
47
  rule: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
48
48
  scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
49
- tool: z.enum(PERSISTED_RULE_TOOLS),
49
+ behavior: z.enum(RULE_BEHAVIORS),
50
+ tool: z.string().min(1).max(190).refine(isPersistedRuleTool, { message: "not a tool the persisted-rule grammar speaks for" }),
50
51
  match: z.enum(PERSISTED_RULE_MATCHES),
51
52
  command: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
52
53
  adds: z.array(LocalImportAddSchema).max(64),
@@ -58,16 +59,18 @@ const LocalImportPrepareBodySchema = z.object({ rows: z.array(LocalImportRowSche
58
59
  const LocalImportRedeemBodySchema = z.object({ ticket: TicketStringSchema }).strict();
59
60
  const RuleRevokeBodySchema = z
60
61
  .object({
62
+ behavior: z.enum(RULE_BEHAVIORS),
61
63
  rule: z.string().min(1).max(1024),
62
64
  scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
63
65
  principal: z.string().min(1).max(190).optional(),
64
66
  })
65
67
  .strict();
68
+ const RULES_CURSOR_FORMAT = 2;
66
69
  const RulesCursorSchema = z
67
- .object({ rev: z.number().int().nonnegative().safe(), principal: z.string(), scope: z.string(), after: z.string() })
70
+ .object({ v: z.literal(RULES_CURSOR_FORMAT), rev: z.number().int().nonnegative().safe(), principal: z.string(), scope: z.string(), after: z.string() })
68
71
  .strict();
69
72
  function encodeRulesCursor(c) {
70
- return Buffer.from(JSON.stringify(c)).toString("base64url");
73
+ return Buffer.from(JSON.stringify({ v: RULES_CURSOR_FORMAT, ...c })).toString("base64url");
71
74
  }
72
75
  function decodeRulesCursor(raw) {
73
76
  try {
@@ -78,7 +81,7 @@ function decodeRulesCursor(raw) {
78
81
  return { ok: false, why: err instanceof Error ? err.message : String(err) };
79
82
  }
80
83
  }
81
- const rowKey = (r) => `${r.scope}\u0000${r.rule}`;
84
+ const rowKey = (r) => `${r.scope}\u0000${r.rule}\u0000${r.behavior}`;
82
85
  export async function handleRules(req, res, url, ctx) {
83
86
  const miss = { fell: false };
84
87
  await handleRulesBody(req, res, url, ctx, miss);
@@ -132,7 +135,7 @@ async function handleRulesBody(req, res, url, ctx, miss) {
132
135
  if (isLocalPrepare) {
133
136
  const parsed = LocalImportPrepareBodySchema.safeParse(rawBody);
134
137
  if (!parsed.success) {
135
- sendError(res, 400, "request.body_shape", "body must be {rows:[{rule,scope,tool,match,command,adds}]}");
138
+ sendError(res, 400, "request.body_shape", "body must be {rows:[{behavior,rule,scope,tool,match,command,adds}]}");
136
139
  return;
137
140
  }
138
141
  const prepared = await lane.prepareLocalImport(principal, parsed.data.rows);
@@ -241,7 +244,9 @@ async function handleRuleList(req, res, caller, lane, operatorPrincipals) {
241
244
  sendError(res, 400, "request.query_invalid", "the rules cursor was minted at a different revision — restart the listing", { rev: listing.rev });
242
245
  return;
243
246
  }
244
- const matching = listing.rows.filter((r) => (filterKey === "" || r.scope === filterKey) && (cursor === undefined || rowKey(r) > cursor.after));
247
+ const matching = listing.rows
248
+ .filter((r) => (filterKey === "" || r.scope === filterKey) && (cursor === undefined || rowKey(r) > cursor.after))
249
+ .sort((x, y) => (rowKey(x) < rowKey(y) ? -1 : rowKey(x) > rowKey(y) ? 1 : 0));
245
250
  const page = matching.slice(0, limit);
246
251
  const last = page[page.length - 1];
247
252
  const nextCursor = matching.length > limit && last !== undefined
@@ -252,7 +257,7 @@ async function handleRuleList(req, res, caller, lane, operatorPrincipals) {
252
257
  async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals) {
253
258
  const parsed = RuleRevokeBodySchema.safeParse(rawBody);
254
259
  if (!parsed.success) {
255
- sendError(res, 400, "request.body_shape", "body must be {rule, scope} with an optional principal");
260
+ sendError(res, 400, "request.body_shape", "body must be {behavior, rule, scope} with an optional principal");
256
261
  return;
257
262
  }
258
263
  const scope = parseRuleScope(parsed.data.scope);
@@ -265,7 +270,7 @@ async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals)
265
270
  sendError(res, 403, "auth.operator_only", "revoking another principal's rules is an operator-only action");
266
271
  return;
267
272
  }
268
- const outcome = await lane.removeRule({ principal: target.principal, rule: parsed.data.rule, scope });
273
+ const outcome = await lane.removeRule({ principal: target.principal, behavior: parsed.data.behavior, rule: parsed.data.rule, scope });
269
274
  switch (outcome.status) {
270
275
  case "removed":
271
276
  sendJson(res, 200, { status: "removed", rev: outcome.rev, stillLive: outcome.stillLive });
@@ -1609,7 +1609,7 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1609
1609
  windowMs: deps.config.streamApproval?.windowMs,
1610
1610
  windowMarginMs: deps.config.streamAskWindowMarginMs,
1611
1611
  }
1612
- : undefined, deps.checkpointStore);
1612
+ : undefined, deps.checkpointStore, prepared.engineNotices);
1613
1613
  return { status: 202, body: { taskId, sessionId, status: "running" } };
1614
1614
  }, (r) => r.status === 202);
1615
1615
  return resp;
@@ -231,20 +231,15 @@ async function handleTasksBody(req, res, url, ctx, miss) {
231
231
  markChildrenStoppedByUserOnAbort(ac.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
232
232
  let ledgerSink;
233
233
  let ledgerTerminal = false;
234
- if (detachOnDisconnect && durableTaskId && deps.runStore) {
235
- const rs = deps.runStore;
236
- const tid = durableTaskId;
237
- ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData) });
238
- usageKey = durableTaskId;
239
- if (usageKey)
240
- deps.modelUsage?.register(usageKey);
241
- }
242
234
  if (deps.checkpointStore && prepared.spec.sessionId) {
243
235
  await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
244
236
  }
245
237
  if (durableTaskId && deps.runStore) {
246
238
  const rs = deps.runStore;
247
239
  const tid = durableTaskId;
240
+ ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData) });
241
+ usageKey = tid;
242
+ deps.modelUsage?.register(tid);
248
243
  const owner = principal ?? null;
249
244
  const abortFromVerb = () => {
250
245
  cancelledViaVerb.add(tid);
@@ -304,6 +299,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
304
299
  const noticeLedger = ledgerSink;
305
300
  const unregisterEngineNotice = registerEngineNoticeLeg({
306
301
  sessionId: prepared.spec.sessionId,
302
+ ...(prepared.engineNotices !== undefined ? { pending: prepared.engineNotices } : {}),
307
303
  live: (row) => {
308
304
  if (res.writableEnded || res.destroyed) {
309
305
  recordFailOpen(failOpenTagForDroppedFrame("engine_notice"), "frame=engine_notice");
@@ -1,6 +1,6 @@
1
1
  import http from "node:http";
2
2
  import type { IncomingMessage } from "node:http";
3
- 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 } from "@sema-agent/core";
3
+ 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";
4
4
  import type { TaskRequestBody } from "./wire-types.js";
5
5
  import type { ServiceConfig } from "../config-types.js";
6
6
  import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
@@ -92,9 +92,14 @@ export interface ServiceCoreDeps {
92
92
  * `boot/resolve-spec.ts` 的 `declaresInteractivePosture`)。缺席/false ⇒ 无人腿,不声明 posture ——
93
93
  * bg(`POST /v1/runs`)、A2A、resume、cron/wake 一律走这一侧,行为字节不变。
94
94
  * ⚠️ 它是**腿身份**不是能力表态:绝不从请求体读,只由路由装配点按 URL 定(客户端说了不算)。 */
95
+ /** `opts.onNotice` (L-167, core 7.9.1):装配期铸下的、**这条会话**的用户面通告(今天只有一族:请求腿
96
+ * MCP 注入被丢弃)。装配跑在通告腿开口**之前**(`prepareSpec` → 路由注册 `registerEngineNoticeLeg`),
97
+ * 所以它不能在铸点直接 `route()` ——收下来交给调用腿,由腿在开口那一拍连同 `pending` 一起投。
98
+ * 缺席(桩、resume 家族)⇒ 只走结构化日志终点,行为逐字不变。 */
95
99
  resolveSpec: (body: TaskRequestBody, req: IncomingMessage | undefined, auth?: RequestAuth, opts?: {
96
100
  leg?: "fresh" | "resume";
97
101
  liveStream?: boolean;
102
+ onNotice?: (notice: EngineNotice) => void;
98
103
  }) => TaskSpec | Promise<TaskSpec>;
99
104
  /**
100
105
  * [ref] 场景 hands lane 选路:按 `resolveSpec` 刚产出的 TaskSpec 取本任务的执行 Runner。
@@ -2,7 +2,7 @@ import http from "node:http";
2
2
  import { redactErrorMessage, redactHead } from "../observability/run-terminal-log.js";
3
3
  import { once } from "node:events";
4
4
  import { createHash } from "node:crypto";
5
- import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs, RUN_WORKFLOW_TOOL_NAME, terminalProjection, PAUSE_REGISTRY } from "@sema-agent/core";
5
+ import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs, terminalProjection, PAUSE_REGISTRY } from "@sema-agent/core";
6
6
  import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
7
7
  import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
8
8
  import {} from "../config-center/facade.js";
@@ -24,7 +24,6 @@ import { publicStoreProbeError } from "../store-live-probe.js";
24
24
  import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
25
25
  import {} from "../orchestration/workflow-agent-steer.js";
26
26
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
27
- import { HANDS_BAND_TOOL_NAMES } from "../capabilities/hands-lane.js";
28
27
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
29
28
  import { defaultSubagentTailBus, projectTailFrame } from "../fleet/subagent-tail-bus.js";
30
29
  import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, resolveApprovalLeg, resolveStreamApprovalGate } from "../tool-approval.js";
@@ -115,10 +114,10 @@ export function validateUserSkills(skills) {
115
114
  }
116
115
  return null;
117
116
  }
117
+ const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
118
118
  const MAX_BODY = 8 * 1024 * 1024;
119
119
  const MAX_IMAGES_PER_REQUEST = 20;
120
120
  const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
121
- const HAND_TOOL_NAMES = HANDS_BAND_TOOL_NAMES;
122
121
  function mentionableNames(config, catalog) {
123
122
  const allowlist = config.atModelAllowlist ?? [];
124
123
  return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist));
@@ -323,6 +322,7 @@ export function createHttpServer(rawDeps) {
323
322
  ...(deps.config.localDataRoot ? { dataRoot: deps.config.localDataRoot } : {}),
324
323
  configHash,
325
324
  pid: process.pid,
325
+ startedAt: PROCESS_STARTED_AT_MS,
326
326
  port: deps.config.port,
327
327
  ...(deps.config.directDoorActive ? { directDoorActive: true } : {}),
328
328
  ...(restart ? { restartRequired: true, restart: { reasons: restart.reasons, version: restart.version, since: restart.since } } : {}),
@@ -997,8 +997,9 @@ export function createHttpServer(rawDeps) {
997
997
  }
998
998
  try {
999
999
  const auth = deps.authorize ? await deps.authorize({ req, body }) : undefined;
1000
- const spec = await deps.resolveSpec(body, req, auth, { leg: "fresh", ...(legFacts?.liveStream === true ? { liveStream: true } : {}) });
1001
- return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: body.jobId, body };
1000
+ const engineNotices = [];
1001
+ const spec = await deps.resolveSpec(body, req, auth, { leg: "fresh", ...(legFacts?.liveStream === true ? { liveStream: true } : {}), onNotice: (n) => engineNotices.push(n) });
1002
+ return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: body.jobId, body, ...(engineNotices.length > 0 ? { engineNotices } : {}) };
1002
1003
  }
1003
1004
  catch (err) {
1004
1005
  if (err instanceof HttpError) {
@@ -1222,22 +1223,6 @@ export function createHttpServer(rawDeps) {
1222
1223
  throw e;
1223
1224
  }
1224
1225
  const pendingTool = cp.pendingAction.toolName;
1225
- const pendingToolCanon = pendingTool;
1226
- const availableTools = new Set((spec.tools ?? []).map((t) => t.name));
1227
- if (deps.config.remoteExec)
1228
- for (const name of HAND_TOOL_NAMES)
1229
- availableTools.add(name);
1230
- if (deps.checkpointStore)
1231
- availableTools.add("AskUserQuestion");
1232
- if (spec.selfOrchestration === true && deps.config.selfOrchestrationEnabled) {
1233
- availableTools.add(RUN_WORKFLOW_TOOL_NAME);
1234
- }
1235
- if (decision === "approve" && pendingToolCanon && !availableTools.has(pendingToolCanon)) {
1236
- return {
1237
- status: 422,
1238
- body: { error: `pending action no longer satisfiable: tool "${pendingToolCanon}" is not in the current task config (scenario changed since suspend)`, errorCode: "conflict.pending_action_unsatisfiable" },
1239
- };
1240
- }
1241
1226
  if (decision === "approve" && pendingTool === "AskUserQuestion" && !answer) {
1242
1227
  return { status: 400, body: { error: "pending action is AskUserQuestion — approve must carry body.answer ({ answers: [{ header, selected: string[], note? }] })", errorCode: "request.field_conflict" } };
1243
1228
  }
@@ -2434,6 +2419,7 @@ const WORKER_SWAP_REDEEMABLE = {
2434
2419
  env_factory_missing: true,
2435
2420
  governed_unwired: true,
2436
2421
  origin_missing: true,
2422
+ ledger_event_unknown: true,
2437
2423
  real_approval_damaged: false,
2438
2424
  real_approval_forged: false,
2439
2425
  read_face_damaged: false,
@@ -1,4 +1,4 @@
1
- import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_PATH_ACCESSES } from "@sema-agent/core";
1
+ import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_PATH_ABSENCES, TOOL_PATH_ACCESSES, TOOL_PATH_BASES } from "@sema-agent/core";
2
2
  function isRecord(v) {
3
3
  return typeof v === "object" && v !== null && !Array.isArray(v);
4
4
  }
@@ -26,17 +26,26 @@ function readOneFace(raw) {
26
26
  const pt = raw.pathTarget;
27
27
  if (!isRecord(pt) || typeof pt.param !== "string" || !memberOf(TOOL_PATH_ACCESSES, pt.access))
28
28
  return undefined;
29
- if (!onlyKeys(pt, ["param", "access", "aliases", "skillScopeEligible"]))
29
+ if (!onlyKeys(pt, ["param", "access", "aliases", "skillScopeEligible", "base", "absent", "patternParam"]))
30
30
  return undefined;
31
31
  if (pt.aliases !== undefined && !isStringArray(pt.aliases))
32
32
  return undefined;
33
33
  if (pt.skillScopeEligible !== undefined && typeof pt.skillScopeEligible !== "boolean")
34
34
  return undefined;
35
+ if (pt.base !== undefined && !memberOf(TOOL_PATH_BASES, pt.base))
36
+ return undefined;
37
+ if (pt.absent !== undefined && !memberOf(TOOL_PATH_ABSENCES, pt.absent))
38
+ return undefined;
39
+ if (pt.patternParam !== undefined && typeof pt.patternParam !== "string")
40
+ return undefined;
35
41
  out.pathTarget = {
36
42
  param: pt.param,
37
43
  access: pt.access,
38
44
  ...(pt.aliases !== undefined ? { aliases: pt.aliases } : {}),
39
45
  ...(pt.skillScopeEligible === true ? { skillScopeEligible: true } : {}),
46
+ ...(pt.base !== undefined ? { base: pt.base } : {}),
47
+ ...(pt.absent !== undefined ? { absent: pt.absent } : {}),
48
+ ...(pt.patternParam !== undefined ? { patternParam: pt.patternParam } : {}),
40
49
  };
41
50
  }
42
51
  if (raw.renderHints !== undefined) {
@@ -69,6 +69,10 @@ export declare const FAIL_OPEN_TAGS: {
69
69
  readonly cls: "F";
70
70
  readonly note: "elicitation_complete 面包屑持久化失败。同上——答案已经在壳手里,丢的是收尾渲染。";
71
71
  };
72
+ readonly "server.session-titler.gateway-unreachable": {
73
+ readonly cls: "F";
74
+ readonly note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。";
75
+ };
72
76
  readonly "server.prompts.artifact-cache-write-failed": {
73
77
  readonly cls: "F";
74
78
  readonly note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。";
@@ -60,6 +60,10 @@ export const FAIL_OPEN_TAGS = {
60
60
  cls: "F",
61
61
  note: "elicitation_complete 面包屑持久化失败。同上——答案已经在壳手里,丢的是收尾渲染。",
62
62
  },
63
+ "server.session-titler.gateway-unreachable": {
64
+ cls: "F",
65
+ note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。",
66
+ },
63
67
  "server.prompts.artifact-cache-write-failed": {
64
68
  cls: "F",
65
69
  note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。",
@@ -260,7 +260,7 @@ export function createMetrics() {
260
260
  m.counter("stale_cache_conflict_total", "Warm session-cache entries evicted by a cross-instance write while cached (S25)");
261
261
  m.counter("breaker_writethrough_failed_total", "Circuit-breaker cross-replica write-through failures (LOW), by backend");
262
262
  m.counter("web_search_bad_payload_total", "Web-search provider responses whose results field was not an array (LOW), by provider");
263
- m.counter("permission_denied_total", "Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution; 'other' = this build cannot name the layer; label is `deniedBy` since server 7.64.0 — it was `source` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)");
263
+ m.counter("permission_denied_total", "Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution/persisted_rule; 'other' = this build cannot name the layer; label is `deniedBy` since server 7.64.0 — it was `source` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)");
264
264
  m.counter("permission_rule_events_total", "Persisted allow-rule seam events by event (persisted_rule_allowed = a rule authorized a call with nobody asked; rule_store_unreadable = the store failed to read so the call was adjudicated with ZERO rules)");
265
265
  m.counter("compaction_events_total", "Compaction lifecycle events by outcome (started/completed/failed/skipped) and trigger");
266
266
  m.counter("hook_llm_calls_total", "prompt/agent hook entries that completed a model call (hooks 3b), by type");
@@ -1,29 +1,21 @@
1
+ import type { RuleApprovalRecord } from "@sema-agent/core";
1
2
  /**
2
- * [ref] + [ref](core `e298c9e0`,随 7.2.x 提货生效;[ref])—— 规则车道**闭词表的单一属主**。
3
- *
4
- * 为什么要一个模块而不是各处各写一份 `z.enum([...])`:同一张词表此前在四处各抄一份字面量
5
- * (`approval-card.ts` 两腿的 offer schema、`http/routes/rules.ts` 的直通票口行形、
6
- * `plugins/permission-rule-store-sql.ts` 的 `rules_json` 读/写形),core 给 `PersistedRuleMatch` 加两员
7
- * (wildcard / subpath)、给 `PersistedRuleTool` 加 Read 时,四份抄件**没有一份**会编译红 —— zod 的 enum
8
- * 只是运行期的一个 Set,静默把一次上游语义扩张翻成一整类合法行的拒收([ref] 词表纪律说的正是这形:
9
- * 未知词必须是**编译**错,不是运行期一个安静的 false)。
3
+ * `PersistedRuleTool` **不再是闭词表**(core 7.9.0 / [ref]):工具名自 core 的**目录**派生
4
+ * (`pathTarget` 声明 ⇒ 路径文法;`Bash` ⇒ 命令文法),型是 `string`。于是本文件对它的处置从
5
+ * 「转录一份名单」换成「转 core 的**谓词**」——名单在 core 的目录里,抄一份的那天目录加一个带
6
+ * `pathTarget` 的工具,本仓会静默拒收一整类合法行(正是本文件顶注说的那个病)。
10
7
  *
11
- * 🔴 每张表都带一条**双向全等钉**(`Exact<>`):元组的成员并 ≡ core 的联合型。core 加员 ⇒ 这里编译红,
12
- * 由人裁定新词在每个消费面的处置(收 / 拒 / 剥),而不是让它在某一处静默落进「不认识」臂;本仓多写一个
13
- * core 没有的词 ⇒ 同样红(词表属主是 core,本仓只转录)。
14
- *
15
- * ⚠️ 转录**不是**判据:一个词在表里只说「这是 core 定义的一员」,不说「本仓每条腿都接受它」。逐腿的收窄
16
- * (例如卡编辑臂对 `subpath` 的拒)写在那条腿自己的穷举 `switch` 里,并各带理由。
8
+ * 谓词方向仍是 fail-CLOSED:`ruleToolGrammarOf` 认不出的名字**不是**一条可持久化规则的工具位
9
+ * (既非命令文法也非路径文法 ⇒ 这条行永远匹配不上任何调用),两只店与票口一律拒。
17
10
  */
18
- import type { RuleApprovalRecord } from "@sema-agent/core";
19
- /** `PersistedRuleTool`([ref] §2.2:`"Read"` 只说一种文法 —— `Read(//abs-dir/**)` 目录只读授权)。 */
20
- export declare const PERSISTED_RULE_TOOLS: readonly ["Bash", "Read"];
11
+ export declare function isPersistedRuleTool(name: string): boolean;
21
12
  /**
22
- * `PersistedRuleMatch` 四员(core 顶注逐字):`exact` / `prefix`(历史 colon-star,永久兼容读、复合前缀形的
13
+ * `PersistedRuleMatch` 五员(core 顶注逐字):`exact` / `prefix`(历史 colon-star,永久兼容读、复合前缀形的
23
14
  * 唯一拼写)/ `wildcard`([ref] 尾 space-star,单命令体上与 prefix **同一谓词**,建议面自此铸它)/
24
- * `subpath`(Read 目录形,永不容纳任何命令)。
15
+ * `subpath`(Read 目录形,永不容纳任何命令)/ `path`(core 7.9.0 [ref]:路径族 deny/ask/allow 的模式文法
16
+ * —— `//abs`、`~/`、`/root-relative`、cwd 相对,段内 `*`、跨段 `**`)。
25
17
  */
26
- export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath"];
18
+ export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath", "path"];
27
19
  /** [ref] §2.3 B3:batch offer 成员的判别位(`command` = 历史逐段 Bash 规则;`directoryRead` = cd 段铸的目录只读授权)。 */
28
20
  export declare const RULE_OFFER_BATCH_MEMBER_KINDS: readonly ["command", "directoryRead"];
29
21
  /** [ref] §3.5:一段「为什么仍未被覆盖」的闭三词集(`redirection` 永久逐次 / `no_rule_form` 无可覆盖形 / `cap_overflow` 去重+帽挤出)。 */
@@ -31,7 +23,7 @@ export declare const UNCOVERED_SEGMENT_REASONS: readonly ["redirection", "no_rul
31
23
  /** [ref] §3.3-5:卡编辑面的 breadth 警告码(记录 `edited.warnings` 存的就是这两个字面)。 */
32
24
  export declare const EDITED_RULE_BREADTH_WARNING_CODES: readonly ["broad_prefix", "compound_prefix"];
33
25
  /**
34
- * [ref] §4.5 / [ref] §3.5 —— 审批记录的**形版本号**(core `RuleApprovalRecord.schema`,本批 2→3)。
26
+ * [ref] §4.5 / [ref] §3.5 —— 审批记录的**形版本号**(core `RuleApprovalRecord.schema`,本批 3→4;[ref] 三态 behavior 进候选行)。
35
27
  * 类型绑 core 的字面型:core 再升号 ⇒ 这里编译红,两只 durable 店(SQL 双方言 + File)的 stale 判别与写侧钉
36
28
  * 同批随之;判别是**等式**(`!==`)不是下限 —— 旧号的行按 stale 信封交还(不迁移、不 widen、不丢行),
37
29
  * pending 卡短命,重触发即取新卡(B7/B8 既诺)。
@@ -1,7 +1,8 @@
1
- export const PERSISTED_RULE_TOOLS = ["Bash", "Read"];
2
- const TOOLS_PIN = true;
3
- void TOOLS_PIN;
4
- export const PERSISTED_RULE_MATCHES = ["exact", "prefix", "wildcard", "subpath"];
1
+ import { ruleToolGrammarOf } from "@sema-agent/core";
2
+ export function isPersistedRuleTool(name) {
3
+ return ruleToolGrammarOf(name) !== undefined;
4
+ }
5
+ export const PERSISTED_RULE_MATCHES = ["exact", "prefix", "wildcard", "subpath", "path"];
5
6
  const MATCHES_PIN = true;
6
7
  void MATCHES_PIN;
7
8
  export const RULE_OFFER_BATCH_MEMBER_KINDS = ["command", "directoryRead"];
@@ -13,5 +14,5 @@ void REASONS_PIN;
13
14
  export const EDITED_RULE_BREADTH_WARNING_CODES = ["broad_prefix", "compound_prefix"];
14
15
  const WARNING_CODES_PIN = true;
15
16
  void WARNING_CODES_PIN;
16
- export const RULE_APPROVAL_RECORD_SCHEMA = 3;
17
+ export const RULE_APPROVAL_RECORD_SCHEMA = 4;
17
18
  //# sourceMappingURL=permission-rule-vocab.js.map
@@ -1,6 +1,7 @@
1
- import { FileDurableRulePartitionProvider, type RuleApprovalRecord, type RuleApprovalRecordStore, type RuleScope, type StaleRuleApprovalRecord } from "@sema-agent/core";
1
+ import { FileDurableRulePartitionProvider, type RuleApprovalRecord, type RuleApprovalRecordStore, type StaleRuleApprovalRecord } from "@sema-agent/core";
2
2
  import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./permission-rule-store-sql.js";
3
3
  import type { PermissionRuleStoreBundle } from "../rules-consent.js";
4
+ import { type RuleCandidate } from "@sema-agent/core";
4
5
  /**
5
6
  * `RuleApprovalRecordStore` 的 File 形。
6
7
  *
@@ -62,10 +63,7 @@ export declare class FileRuleImportTicketStore {
62
63
  principal: string;
63
64
  approvalId: string;
64
65
  purpose: RuleTicketPurpose;
65
- candidates: ReadonlyArray<{
66
- rule: string;
67
- scope: RuleScope;
68
- }>;
66
+ candidates: ReadonlyArray<RuleCandidate>;
69
67
  ttlMs: number;
70
68
  }): Promise<RuleImportTicket>;
71
69
  /** 一次性**认领**。五条否定项的判序与 SQL 形逐字相同(unknown → wrong-principal → wrong-purpose →
@@ -6,12 +6,13 @@ import { FileDurableRulePartitionProvider, AppendLog, ensureDir, readJsonlRecord
6
6
  import { z } from "zod";
7
7
  import { buildRulePayloadHash } from "./permission-rule-store-sql.js";
8
8
  import { EDITED_RULE_BREADTH_WARNING_CODES, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
9
+ import { RULE_BEHAVIORS } from "@sema-agent/core";
9
10
  const RuleScopeSchema = z.union([
10
11
  z.object({ kind: z.literal("global") }).strict(),
11
12
  z.object({ kind: z.literal("project"), root: z.string().min(1) }).strict(),
12
13
  ]);
13
14
  const RuleDotSchema = z.object({ actor: z.string().min(1), counter: z.number().int().nonnegative().safe() }).strict();
14
- const RuleCandidateSchema = z.object({ rule: z.string(), scope: RuleScopeSchema }).strict();
15
+ const RuleCandidateSchema = z.object({ behavior: z.enum(RULE_BEHAVIORS), rule: z.string(), scope: RuleScopeSchema }).strict();
15
16
  const RuleOffer2Schema = z.union([
16
17
  z.object({ kind: z.literal("single"), candidate: z.number().int().nonnegative().safe() }).strict(),
17
18
  z
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { type PermissionRuleWriter, type DurableRulePartition, type DurableRulePartitionProvider, type RuleScope, type RuleSyncFrontier, type RuleApprovalRecord, type StaleRuleApprovalRecord, type RuleApprovalRecordStore, type RuleOwner } from "@sema-agent/core";
2
+ import { type PermissionRuleWriter, type DurableRulePartition, type DurableRulePartitionProvider, type RuleScope, type RuleSyncFrontier, type RuleCandidate, type RuleApprovalRecord, type StaleRuleApprovalRecord, type RuleApprovalRecordStore, type RuleOwner } from "@sema-agent/core";
3
3
  import type { Pool as MySqlPool } from "mysql2/promise";
4
4
  import type { PgQueryFn } from "./pg-query.js";
5
5
  import { type SqlDriver } from "./sql-driver.js";
@@ -274,11 +274,14 @@ export declare const SQL_DB_NOW_MS: {
274
274
  readonly tidb: "CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS SIGNED)";
275
275
  readonly pg: "(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
276
276
  };
277
- /** prepare 时刻候选集的规范摘要(载荷绑定 F1 ④ 的判据)。纯数据 ⇒ `build*`。 */
278
- export declare function buildRulePayloadHash(candidates: ReadonlyArray<{
279
- rule: string;
280
- scope: RuleScope;
281
- }>): string;
277
+ /** prepare 时刻候选集的规范摘要(载荷绑定 F1 ④ 的判据)。纯数据 ⇒ `build*`。
278
+ *
279
+ * 🔴 入参型收到 core 的 `RuleCandidate`(core 7.9.0 [ref] 起含 `behavior`)—— 规则的身份是
280
+ * **(behavior, 文本, 作用域) 三元组**,而这道摘要正是「prepare 那一刻的候选集」与「redeem 那一刻的
281
+ * 候选集」之间的等式。此前的形参写成 `{rule, scope}` 二元组:序列化器吃的是整只对象所以运行期**恰好**
282
+ * 覆盖了 behavior,但那是巧合不是保证 —— 哪天有人从一个只有二元组的地方喂进来,一次把候选从 `deny`
283
+ * 改写成 `allow`(文本一个字节不动)在这道等式下就认不出来了。收到域型让机器替人守这一条。 */
284
+ export declare function buildRulePayloadHash(candidates: ReadonlyArray<RuleCandidate>): string;
282
285
  /**
283
286
  * 票面三态协议的 SQL 实现。
284
287
  *
@@ -315,10 +318,7 @@ export declare class SqlRuleImportTicketStore {
315
318
  principal: string;
316
319
  approvalId: string;
317
320
  purpose: RuleTicketPurpose;
318
- candidates: ReadonlyArray<{
319
- rule: string;
320
- scope: RuleScope;
321
- }>;
321
+ candidates: ReadonlyArray<RuleCandidate>;
322
322
  ttlMs: number;
323
323
  }): Promise<RuleImportTicket>;
324
324
  /**