@sema-agent/server 7.11.0 → 7.12.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.
- package/README.md +1 -1
- package/USAGE.md +56 -0
- package/dist/auth-keys.d.ts +28 -4
- package/dist/auth-keys.js +60 -15
- package/dist/boot/parked-revive-gate.d.ts +18 -2
- package/dist/boot/parked-revive-gate.js +136 -14
- package/dist/boot/permission-rules-audit.d.ts +49 -0
- package/dist/boot/permission-rules-audit.js +57 -0
- package/dist/boot/resolve-spec.js +43 -12
- package/dist/budget.js +22 -0
- package/dist/config-types.d.ts +17 -9
- package/dist/config.d.ts +28 -2
- package/dist/config.js +348 -79
- package/dist/governance-ask-marks.js +8 -2
- package/dist/http/route-ctx.d.ts +6 -3
- package/dist/http/routes/approvals-assistant.js +2 -1
- package/dist/http/routes/capabilities.js +44 -9
- package/dist/http/routes/rules.d.ts +19 -7
- package/dist/http/routes/rules.js +180 -4
- package/dist/http/server.d.ts +4 -2
- package/dist/http/server.js +81 -3
- package/dist/http/wire-types.d.ts +48 -0
- package/dist/main.js +19 -1
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/observability/metrics.js +2 -1
- package/dist/observability/tool-trace.d.ts +5 -1
- package/dist/observability/tool-trace.js +33 -6
- package/dist/parked-decide.d.ts +13 -3
- package/dist/parked-decide.js +10 -1
- package/dist/plugins/permission-rule-store-file.d.ts +83 -0
- package/dist/plugins/permission-rule-store-file.js +371 -0
- package/dist/plugins/permission-rule-store-sql.d.ts +7 -0
- package/dist/plugins/permission-rule-store-sql.js +11 -0
- package/dist/plugins/store-backend.d.ts +12 -6
- package/dist/plugins/store-backend.js +58 -9
- package/dist/rules-consent.d.ts +69 -1
- package/dist/rules-consent.js +43 -1
- package/dist/run-local.js +120 -13
- package/dist/runtime-governance.js +9 -3
- package/dist/task-settings.d.ts +44 -0
- package/dist/task-settings.js +57 -1
- package/dist/tool-approval.d.ts +6 -1
- package/dist/tool-approval.js +106 -27
- package/dist/trace/core-keyset-guard.d.ts +13 -2
- package/dist/trace/project.d.ts +19 -2
- package/dist/trace/project.js +24 -4
- package/package.json +2 -2
|
@@ -10,6 +10,14 @@ import type { AgentDefinition, McpServerSpec } from "@sema-agent/core";
|
|
|
10
10
|
export interface TaskRequestBody {
|
|
11
11
|
objective: string;
|
|
12
12
|
sessionId?: string;
|
|
13
|
+
/** Which deployment SCENARIO assembles this task's tools/prompt/skills (the keys of the worker's scenario
|
|
14
|
+
* table — e.g. "default"/"code-review"/"scan"/"team"). An INTENT bounded by the deployment's own table AND,
|
|
15
|
+
* on a governed worker, by the principal's center-resolved allowlist: an unknown name is a typed 400
|
|
16
|
+
* `scenario_unknown`, a known-but-not-allowed one a typed 400 `scenario_not_allowed`; absent ⇒ the ruling's
|
|
17
|
+
* assigned scenario, else `DEFAULT_SCENARIO` (consumed at `boot/resolve-spec.ts` via `gateScenarioRequest`).
|
|
18
|
+
* ⚠️ scenario-SPECIFIC keys (`repo`/`lenses`/`rounds`, read through `ScenarioRequest`'s index signature) are
|
|
19
|
+
* deliberately NOT declared here — they belong to one scenario each, not to the shared submit surface. */
|
|
20
|
+
scenario?: string;
|
|
13
21
|
/** dispatch-gateway failover 幂等第二级:caller-minted 任务 id(uuidv7)。同 owner 重放同 id ⇒ 幂等重放;
|
|
14
22
|
* 异 owner ⇒ 409 `conflict.run_exists`;非 uuidv7 ⇒ 400 `request.id_invalid`(routes/runs.ts:192-208 真验收)。
|
|
15
23
|
* [2400] TR-16 连带:此键此前只活在 runs.ts 读点,导出类型漏declared——CAPS-OPS-13 同族第 9 键。 */
|
|
@@ -76,6 +84,14 @@ export interface TaskRequestBody {
|
|
|
76
84
|
* a rung fails the gate (default = it didn't complete). Default off; mutually exclusive with `verify`;
|
|
77
85
|
* not on /v1/tasks/stream. ⚠️ each rung is a cold re-run — use for read-only / idempotent tasks. */
|
|
78
86
|
cascade?: boolean;
|
|
87
|
+
/** Multi-lens review council (the `code-review` scenario's expensive tier) — `council:true` fans the review out
|
|
88
|
+
* to N parallel lenses + an arbiter instead of the lead reviewing in-line; `debate:true` additionally runs the
|
|
89
|
+
* L2 peer-debate rounds. Read on TWO seams and both matter: the scenario builder mounts `run_council`
|
|
90
|
+
* (`capabilities/scenarios.ts` `codeReview`), and `boot/resolve-spec.ts` treats either flag as an EXPLICIT team
|
|
91
|
+
* declaration — it widens the task's wall-clock tenancy budget (`resolveTaskLimits`) and suppresses the value
|
|
92
|
+
* router's auto-escalation (`explicitTeam`). Only literal `true` counts on both seams. */
|
|
93
|
+
council?: boolean;
|
|
94
|
+
debate?: boolean;
|
|
79
95
|
/** Work-view correlation id (optional): groups the runs of one logical task across both client doors
|
|
80
96
|
* (MCP façade / portal) so a fragmented set of runs reads as one task. Opaque to the engine — only
|
|
81
97
|
* persisted on the run row + filterable via `GET /v1/tasks?jobId=`. ≤64 chars (matches the column). */
|
|
@@ -102,6 +118,18 @@ export interface TaskRequestBody {
|
|
|
102
118
|
* surfaces the validated object as `TaskResult.structuredOutput` (an invalid submit retries, then fails with
|
|
103
119
|
* `output.invalid`). A plain JSON-schema OBJECT (shape + size validated here; deep validity is core's). */
|
|
104
120
|
outputSchema?: Record<string, unknown>;
|
|
121
|
+
/** Retry budget for an INVALID `submit_output` against `outputSchema` (core `TaskSpec.outputRetries`) — the
|
|
122
|
+
* caller-facing other half of the structured-output pair. Narrow acceptance in `boot/resolve-spec.ts`:
|
|
123
|
+
* finite, ≥1, floored, capped at 10; anything else ⇒ key omitted (core's own default). Meaningless without
|
|
124
|
+
* `outputSchema` (core ignores it there). */
|
|
125
|
+
outputRetries?: number;
|
|
126
|
+
/** Within-task compaction tuning (core design/145 `TaskSpec.compaction`). ⚠️ ONLY `clampTolerance` is a caller
|
|
127
|
+
* knob — the rest of core's compaction object is an OPERATOR axis and is deliberately not on the wire, so this
|
|
128
|
+
* type is narrower than core's field on purpose. `boot/resolve-spec.ts` accepts a finite number in [0,1] and
|
|
129
|
+
* omits the whole `compaction` key otherwise. Pairs with `compactionModel` (which gear compacts). */
|
|
130
|
+
compaction?: {
|
|
131
|
+
clampTolerance?: number;
|
|
132
|
+
};
|
|
105
133
|
/** Reasoning-effort selection (CC `/effort` picker, shell-host contract E7): a neutral effort tier mapped to
|
|
106
134
|
* core's `TaskSpec.thinking` (`ThinkingLevel`). Accepted set = core's tiers (off/minimal/low/medium/high/xhigh/max);
|
|
107
135
|
* the picker's advertised default set is `/v1/models` `supportedEffortLevels`. A provided-but-unknown value is a
|
|
@@ -275,6 +303,26 @@ export interface TaskRequestBody {
|
|
|
275
303
|
* (a caller can't point a shared/cloud worker at an arbitrary host path). Validated absolute (prepareSpec 400s a
|
|
276
304
|
* relative cwd — it would silently resolve against the SERVICE process cwd). */
|
|
277
305
|
cwd?: string;
|
|
306
|
+
/** design/119 (CC `--add-dir`, core `TaskSpec.additionalDirectories`): extra host dirs the FILE tools may reach
|
|
307
|
+
* beyond the containment root. 🔒 Gated exactly like `cwd`/`shellEnv` — honored ONLY on the single-user host
|
|
308
|
+
* lane (`task-cwd.ts` `cwdHonored`); off that lane `boot/resolve-spec.ts` drops them with a loud
|
|
309
|
+
* `task_additional_directories_ignored` (never silently). SHAPE is fail-loud at submit (absolute host paths,
|
|
310
|
+
* no `..` segments, ≤ MAX_ADDITIONAL_DIRS entries) — separate from whether the lane honors them.
|
|
311
|
+
* `additionalReadDirectories` (core 5.11.0) is the same door with a READ-only semantic: it widens the read
|
|
312
|
+
* containment (classify auto-allow + read_file/grep) and never the write face. Rides the persisted body onto
|
|
313
|
+
* resume legs. */
|
|
314
|
+
additionalDirectories?: string[];
|
|
315
|
+
additionalReadDirectories?: string[];
|
|
316
|
+
/** [1909]⑧/[1910]/[1911] (core 5.23.0 `TaskSpec.oneShot`): this SUBMISSION is one-shot — no later turn exists
|
|
317
|
+
* in which an async background notification could land (the archetypal case is a headless `sema -p` whose
|
|
318
|
+
* process exits when the turn ends). PER-REQUEST on purpose: "does this submission expect to be continued" is
|
|
319
|
+
* a property of the submission, not of the connection it arrived on. core consumes it as GUIDANCE ONLY (the
|
|
320
|
+
* `RunWorkflow` / delegation receipts tell the model to block-wait via `TaskOutput({block:true})` instead of
|
|
321
|
+
* "end your turn, you will be notified" — the latter is actively wrong here and loses background results); it
|
|
322
|
+
* grants nothing, so there is no tenancy gate. Sibling of `interactiveTools` (the same `-p` posture) and passed
|
|
323
|
+
* through the same way: a boolean rides, absent/garbage ⇒ key omitted (core's default = interactive). Rides the
|
|
324
|
+
* persisted body onto resume legs. */
|
|
325
|
+
oneShot?: boolean;
|
|
278
326
|
/** [R3] Caller-supplied per-request MCP servers (the TOC client's local `.mcp.json`), aligned to core
|
|
279
327
|
* `McpServerSpec`. 🔒 honored on any SINGLE-USER deployment (task-mcp.ts `mcpInjectionHonored` = `requirePrincipal!==true`)
|
|
280
328
|
* — the requester is the super-admin of their OWN worker (CC-parity), on ANY execution lane (the stdio MCP runs on the
|
package/dist/main.js
CHANGED
|
@@ -44,6 +44,7 @@ import { createParkedReviveInheritedGate } from "./boot/parked-revive-gate.js";
|
|
|
44
44
|
import { startReapers } from "./boot/reapers.js";
|
|
45
45
|
import { openStores } from "./boot/stores.js";
|
|
46
46
|
import { runAdoptionBootScan } from "./boot/adoption.js";
|
|
47
|
+
import { auditDormantPermissionRules } from "./boot/permission-rules-audit.js";
|
|
47
48
|
import { createBudgetAndTracing } from "./boot/budget-tracing.js";
|
|
48
49
|
import { createRuleConsentLane } from "./rules-consent.js";
|
|
49
50
|
import { createExecutionEnv } from "./boot/execution-env.js";
|
|
@@ -188,6 +189,10 @@ async function main() {
|
|
|
188
189
|
// 🔴 总开关在最前(codex round7 [high] 一):关 ⇒ 整条车道根本不装配(店/车道/帧键/两口一起消失)。
|
|
189
190
|
const permissionRuleStores = config.permissionRulesEnabled && backend ? backend.permissionRule() : undefined;
|
|
190
191
|
const ruleConsent = permissionRuleStores ? createRuleConsentLane(permissionRuleStores) : undefined;
|
|
192
|
+
// #203 §3(设计稿 v2 F4 残余):默认 ON 会把**既有**规则桶一并唤醒 —— 在启动日志里把它说出来。
|
|
193
|
+
// 三条判据(店缺席 / 零桶 / 运维显式表过态 ⇒ 都不打行)与失败方向(数不出来只 warn,绝不拒启、
|
|
194
|
+
// 也绝不编一个 0)逐字见 `boot/permission-rules-audit.ts`。
|
|
195
|
+
await auditDormantPermissionRules({ stores: permissionRuleStores, logger, explicit: config.permissionRulesEnabledExplicit });
|
|
191
196
|
// design/158 A10:执行环境装配搬到 src/boot/execution-env.ts(逐字)。
|
|
192
197
|
// ⚠️ 工厂装饰顺序=行为(scratchpad → worktree → SendUserFile 登记 → 附件物化最外层),见该文件头注。
|
|
193
198
|
const { perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, } = createExecutionEnv({ config, logger, metrics, taskAttachmentStore });
|
|
@@ -719,8 +724,21 @@ async function main() {
|
|
|
719
724
|
// `mandatePostureOf`)。此处新铸的转发闭包与 runnerDeps 那只行为逐字相同(都只转 `toolApproval.ask`),
|
|
720
725
|
// 摘要只看在场性;仅 core 的 hook 席位去重按函数身份判,那一侧的身份未命中是**多筛一次**(core 自述
|
|
721
726
|
// 的保守方向),不是漏筛。
|
|
727
|
+
// A-010.1 同族的另外两位(`resolveRuntimeCaps` / `autoModeSeatMounted`):5.22.0 的摘要卷进的是**三**位
|
|
728
|
+
// 决议链元数据,其中 `autoModeArmed` 与 `durableMandate` 的 `forceDurableGate` 项都由 per-principal 的
|
|
729
|
+
// `RuntimeCaps` 决定。两处必须与 core 看到的**同一只**事实源接线,否则同样是「park 记 true 而赎回腿供
|
|
730
|
+
// false ⇒ 摘要恒不匹配 ⇒ 这批 principal 的卡永不可赎」:
|
|
731
|
+
// · `resolveRuntimeCaps` = 递给 `RunnerDeps.runtimeCapsResolver` 的同一只(单一属主 boot/runtime-caps.ts);
|
|
732
|
+
// · `autoModeSeatMounted` = **读** runnerDeps 上那一位,不写字面 `true` —— core 的武装式是
|
|
733
|
+
// `runtimeCaps.autoMode === true ∧ deps.autoMode !== undefined`,第二个半场的真值只有 runnerDeps 知道,
|
|
734
|
+
// 在此处手抄 `true` 会在 runner-deps 那边改成有条件挂载的那一天静默错供一位。
|
|
722
735
|
const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
|
|
723
|
-
? createParkedReviveInheritedGate({
|
|
736
|
+
? createParkedReviveInheritedGate({
|
|
737
|
+
config, question, approvalExemptionStore, logger, localRoot,
|
|
738
|
+
approverSeat: createRunnerDepsOnAsk(toolApproval),
|
|
739
|
+
...(runnerDeps.runtimeCapsResolver ? { resolveRuntimeCaps: runnerDeps.runtimeCapsResolver } : {}),
|
|
740
|
+
autoModeSeatMounted: runnerDeps.autoMode !== undefined,
|
|
741
|
+
})
|
|
724
742
|
: undefined;
|
|
725
743
|
// 场景详情:内建 details 必须在 overlay 合并【前】构建(探针要打纯内建工厂,不是被 center 顶掉的);
|
|
726
744
|
// center details 随 overlay 同判定源盖同名——source 语义与 selectScenario 实际取用永一致(约定①)。
|
|
@@ -49,6 +49,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
49
49
|
readonly cls: "F";
|
|
50
50
|
readonly note: "CC 规则导入票的**认领回滚**失败(认领之后的某一步没成 ⇒ 本该把认领放回去,而这次放回本身也抛了)。放行的最坏后果 = 这张票留在已认领态、属主这一轮不能重试 —— 恰好等于加认领回滚**之前**的行为,不是新损失;票随 TTL 自然消失,属主重走一次 prepare 即可拿新票(导入按 core 的设计幂等:同一条规则再兑付只是同一个 dot 的重放)。方向上没有任何权限被放宽(规则**没有**落地才走到这条臂),故 F 类;必须留痕,否则「票为什么突然不能用了」在遥测里没有任何痕迹。";
|
|
51
51
|
};
|
|
52
|
+
readonly "server.parked-revive.ancestor-classifier-unreachable": {
|
|
53
|
+
readonly cls: "P-DEBT";
|
|
54
|
+
readonly note: "跨副本赎回 parked 审批时,祖先层在 park 时是 **auto-mode 武装**的,但那只分类器是祖先任务上的活闭包(绑着它自己的转写窗+brain),跨进程重建不出来 ⇒ 本仓交一只如实拒答的 decider,core 收到 `unavailable` 后**不产生任何自动裁决**、原样落到祖先冻结审批席那条链(本腿的席位又是无 ALS 的降级形 ⇒ 再 park 给人)。方向:分类器本会 allow 的改成问人(更严),本会 block 的也改成问人(**不是自动放行**,但比自动拒松一档)⇒ 记债不当合法兜底。计数 = 「丢了祖先分类器判决的继承 ask」次数。收口件二选一:core 把分类器判据持久进链条目,或让祖先 decider 有可跨进程重建的形。";
|
|
55
|
+
};
|
|
52
56
|
readonly "server.fleet.subscriber-callback-threw": {
|
|
53
57
|
readonly cls: "F";
|
|
54
58
|
readonly note: "fleet bus 某订阅回调抛错 ⇒ 该回调本帧作废,其余订阅方与发布方不受影响。隔离是承重的:扇出同步,修前异常会传回发布方 put/update 投影点,core 持久化 catch{} 且不推进 storeRev ⇒ durable 行冻在 running 而 notify 已 ack(#183 复审 R3 HIGH)。丢的只是一个消费方的一帧渲染,故 F 类;但必须留痕——静默吞掉等于订阅方病灶永不显形。";
|
|
@@ -72,6 +72,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
72
72
|
cls: "F",
|
|
73
73
|
note: "CC 规则导入票的**认领回滚**失败(认领之后的某一步没成 ⇒ 本该把认领放回去,而这次放回本身也抛了)。放行的最坏后果 = 这张票留在已认领态、属主这一轮不能重试 —— 恰好等于加认领回滚**之前**的行为,不是新损失;票随 TTL 自然消失,属主重走一次 prepare 即可拿新票(导入按 core 的设计幂等:同一条规则再兑付只是同一个 dot 的重放)。方向上没有任何权限被放宽(规则**没有**落地才走到这条臂),故 F 类;必须留痕,否则「票为什么突然不能用了」在遥测里没有任何痕迹。",
|
|
74
74
|
},
|
|
75
|
+
"server.parked-revive.ancestor-classifier-unreachable": {
|
|
76
|
+
cls: "P-DEBT",
|
|
77
|
+
note: "跨副本赎回 parked 审批时,祖先层在 park 时是 **auto-mode 武装**的,但那只分类器是祖先任务上的活闭包(绑着它自己的转写窗+brain),跨进程重建不出来 ⇒ 本仓交一只如实拒答的 decider,core 收到 `unavailable` 后**不产生任何自动裁决**、原样落到祖先冻结审批席那条链(本腿的席位又是无 ALS 的降级形 ⇒ 再 park 给人)。方向:分类器本会 allow 的改成问人(更严),本会 block 的也改成问人(**不是自动放行**,但比自动拒松一档)⇒ 记债不当合法兜底。计数 = 「丢了祖先分类器判决的继承 ask」次数。收口件二选一:core 把分类器判据持久进链条目,或让祖先 decider 有可跨进程重建的形。",
|
|
78
|
+
},
|
|
75
79
|
"server.fleet.subscriber-callback-threw": {
|
|
76
80
|
cls: "F",
|
|
77
81
|
note: "fleet bus 某订阅回调抛错 ⇒ 该回调本帧作废,其余订阅方与发布方不受影响。隔离是承重的:扇出同步,修前异常会传回发布方 put/update 投影点,core 持久化 catch{} 且不推进 storeRev ⇒ durable 行冻在 running 而 notify 已 ack(#183 复审 R3 HIGH)。丢的只是一个消费方的一帧渲染,故 F 类;但必须留痕——静默吞掉等于订阅方病灶永不显形。",
|
|
@@ -336,7 +336,8 @@ export function createMetrics() {
|
|
|
336
336
|
m.counter("web_search_bad_payload_total", "Web-search provider responses whose results field was not an array (LOW), by provider");
|
|
337
337
|
// 收账批(2026-07-08):这 15 个计数器一直只有 inc 没注册 — 在 auto-register 落地前
|
|
338
338
|
// 整批从未上过 /metrics(inc 曾对未注册名静默 no-op)。补显式注册拿正经 HELP 文案。
|
|
339
|
-
m.counter("permission_denied_total", "Tool-gate denials by source (
|
|
339
|
+
m.counter("permission_denied_total", "Tool-gate denials by source (core's PermissionDeniedSource, incl. org governance; 'other' = a word this build does not know; goal B4 always-on deny meter)");
|
|
340
|
+
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)");
|
|
340
341
|
m.counter("compaction_events_total", "Compaction lifecycle events by outcome (started/completed/failed/skipped) and trigger");
|
|
341
342
|
m.counter("hook_llm_calls_total", "prompt/agent hook entries that completed a model call (hooks 3b), by type");
|
|
342
343
|
m.counter("auth_bridge_verify_total", "Registry SSO JWT verifications (auth-bridge), by outcome");
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type { Hooks } from "@sema-agent/core";
|
|
1
|
+
import type { Hooks, PermissionDeniedSource } from "@sema-agent/core";
|
|
2
2
|
import type { Logger } from "./logger.js";
|
|
3
3
|
import type { Metrics } from "./metrics.js";
|
|
4
|
+
/** 表内 ⇒ 收窄成 `PermissionDeniedSource`(类型守卫)。计量点用它判「这个词是不是本 build 认得的真词」。
|
|
5
|
+
* 导出是为了让运行期锚(`test/tool-trace.test.ts`,从装树 core 的 `.d.ts` 解析真词表)**复用同一个谓词**
|
|
6
|
+
* —— 测试自己另写一份判据 = 又一张手抄表,正是本文件刚消灭的那个病。 */
|
|
7
|
+
export declare function isKnownDenySource(v: unknown): v is PermissionDeniedSource;
|
|
4
8
|
/**
|
|
5
9
|
* ALWAYS-ON gate-deny meter (goal B4, 2026-07-08): `permission_denied_total{source}` — how often the
|
|
6
10
|
* adjudicate chain DENIES a tool call, by gate source. Deliberately SEPARATE from {@link createToolTracer}:
|
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
import { redactSecrets } from "../trace/redact.js";
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The gate sources core's `PermissionDeniedPayload.source` names. Treated as an OPEN enum on the WIRE
|
|
4
|
+
* (a core newer than this build may send a word this table has never heard of — that must not explode
|
|
5
|
+
* the Prometheus label set), but as a CLOSED SET against the core we compile with.
|
|
6
|
+
*
|
|
7
|
+
* 🔴 core 5.23.0([3372] 提货批件②):这张表**曾是手抄的**(`new Set([...五个字面量])`,与 core 的类型
|
|
8
|
+
* 零编译期联系),于是 core 先加 `classifier`、又在 5.23.0(design/182 §7)加 `org` 时,两个**真词**都
|
|
9
|
+
* 被静默折进了 `other`。那不是「少一个标签」——`permission_denied_total{source="other"}` 的告警语义是
|
|
10
|
+
* 「引擎发了本 build 不认识的词,去对表」,真词混进来就把这个信号读废了,而 `org`(组织治理层的拒)
|
|
11
|
+
* 恰恰是运维最该单独看见的一类。
|
|
12
|
+
*
|
|
13
|
+
* ⇒ 表改为 `Record<PermissionDeniedSource, true>` 穷举:**core 增删一个词,本表编译期先红**(闭集词表
|
|
14
|
+
* 禁手抄、必须从 core 类型推导的成文纪律)。运行期配对锚在 `test/tool-trace.test.ts`(从装树 core 的
|
|
15
|
+
* `.d.ts` 解析联合词表逐词断言),因为 vitest 走 esbuild 不查类型 —— 两道锚缺一不可。
|
|
16
|
+
*/
|
|
17
|
+
const KNOWN_DENY_SOURCE_TABLE = {
|
|
18
|
+
policy: true,
|
|
19
|
+
hook: true,
|
|
20
|
+
safety: true,
|
|
21
|
+
shellGate: true,
|
|
22
|
+
planMode: true,
|
|
23
|
+
classifier: true,
|
|
24
|
+
org: true,
|
|
25
|
+
};
|
|
26
|
+
/** 表内 ⇒ 收窄成 `PermissionDeniedSource`(类型守卫)。计量点用它判「这个词是不是本 build 认得的真词」。
|
|
27
|
+
* 导出是为了让运行期锚(`test/tool-trace.test.ts`,从装树 core 的 `.d.ts` 解析真词表)**复用同一个谓词**
|
|
28
|
+
* —— 测试自己另写一份判据 = 又一张手抄表,正是本文件刚消灭的那个病。 */
|
|
29
|
+
export function isKnownDenySource(v) {
|
|
30
|
+
return typeof v === "string" && Object.hasOwn(KNOWN_DENY_SOURCE_TABLE, v);
|
|
31
|
+
}
|
|
6
32
|
/**
|
|
7
33
|
* ALWAYS-ON gate-deny meter (goal B4, 2026-07-08): `permission_denied_total{source}` — how often the
|
|
8
34
|
* adjudicate chain DENIES a tool call, by gate source. Deliberately SEPARATE from {@link createToolTracer}:
|
|
@@ -15,7 +41,7 @@ const KNOWN_DENY_SOURCES = new Set(["policy", "hook", "safety", "shellGate", "pl
|
|
|
15
41
|
export function createPermissionDeniedMeter(metrics) {
|
|
16
42
|
return {
|
|
17
43
|
permissionDenied(payload) {
|
|
18
|
-
metrics.inc("permission_denied_total", { source:
|
|
44
|
+
metrics.inc("permission_denied_total", { source: isKnownDenySource(payload.source) ? payload.source : "other" });
|
|
19
45
|
},
|
|
20
46
|
};
|
|
21
47
|
}
|
|
@@ -82,7 +108,8 @@ export function createToolTracer(logger) {
|
|
|
82
108
|
},
|
|
83
109
|
// core 1.257: the gate's DENY short-circuit never reaches postToolUse* (the call
|
|
84
110
|
// doesn't execute), so denied calls were INVISIBLE to this trace — the exact observability hole
|
|
85
|
-
// asked about. `source` = core's adjudicate-chain enum (
|
|
111
|
+
// asked about. `source` = core's adjudicate-chain enum (the closed set is `KNOWN_DENY_SOURCE_TABLE`
|
|
112
|
+
// above — deliberately NOT re-listed here, a second hand-written copy is how the meter drifted); the
|
|
86
113
|
// deny `reason` can quote the adjudicated args (an approver's text, a policy message), so redact+clip
|
|
87
114
|
// like every other line. Same `tool_trace` key so existing grep workflows see denies in sequence.
|
|
88
115
|
permissionDenied(payload) {
|
package/dist/parked-decide.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 判别必须 parked-first:legacy 腿的 `resumeStream` 会绕开 bg registry(无 consumeParkedFlip、
|
|
9
9
|
* 行永 parked、生命周期分叉)。
|
|
10
10
|
*/
|
|
11
|
-
import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, QuestionAnswer, TaskRegistry, ToolSpec } from "@sema-agent/core";
|
|
11
|
+
import type { ApprovalSettledBy, BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, QuestionAnswer, TaskRegistry, ToolSpec } from "@sema-agent/core";
|
|
12
12
|
export interface ParkedAgentMatch {
|
|
13
13
|
handle: string;
|
|
14
14
|
row: BackgroundAgentRecord;
|
|
@@ -46,8 +46,13 @@ export interface ParkedDecideDeps {
|
|
|
46
46
|
* 解析槽一条,单层链;exempt 探针锚= row.rootSessionId,与 remember grant 锚同键)。core 1.396 起
|
|
47
47
|
* `parkedResume.inheritedGate` 席位把它透传进 resume 的官方重供通道——不供 = 重启后带
|
|
48
48
|
* requiresParentConstraint 的 checkpoint 恒被 pre-CAS 门拒(retryable 假话,永不可赎回)。链长形状
|
|
49
|
-
* 校验在 core(fail-closed,供错链长照拒)。
|
|
50
|
-
|
|
49
|
+
* 校验在 core(fail-closed,供错链长照拒)。
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ **async**(A-010.1):重建条目的第三位决议链元数据 `autoModeArmed` 与 `durableMandate` 的
|
|
52
|
+
* `forceDurableGate` 项都要按**本行的 principal** 现解 entitlement(`RunnerDeps.runtimeCapsResolver`
|
|
53
|
+
* 本身就是 sync-or-async 的座),理由全在 `boot/parked-revive-gate.ts`。本腿一直就在 async 里,
|
|
54
|
+
* 「席位必须同步」是那个文件此前自设的约束、不是结构事实。 */
|
|
55
|
+
rebuildInheritedGate?: (row: BackgroundAgentRecord) => Promise<unknown>;
|
|
51
56
|
warn?: (event: string, fields: Record<string, unknown>) => void;
|
|
52
57
|
}
|
|
53
58
|
export interface ParkedDecideRequest {
|
|
@@ -57,6 +62,11 @@ export interface ParkedDecideRequest {
|
|
|
57
62
|
/** cp.pendingAction 原样(outcome 铸造的 persisted 回落源,与 legacy 腿 D-1 姿势逐字同形)。 */
|
|
58
63
|
pendingAction: unknown;
|
|
59
64
|
decision: "approve" | "deny";
|
|
65
|
+
/** #204 件6③ core `ApprovalSettledBy`:**这次结算的出处**,由调用方命名(必填,理由与 legacy 腿
|
|
66
|
+
* `resumeCheckpoint` 的同名参数逐字同源 —— 本函数分辨不出谁在叫它,漏报要是编译错而不是假出处)。
|
|
67
|
+
* 今天唯一的调用方是 `server.ts` 的人为 `/decide`(那道门写着 `req !== undefined`,而内部 D-D SLA
|
|
68
|
+
* deny-sweep 恰是 req 缺席的那条腿,它对 parked 行另有静默跳过臂)⇒ 恒 `"human"`。 */
|
|
69
|
+
settledBy: ApprovalSettledBy;
|
|
60
70
|
reason?: string;
|
|
61
71
|
/** D-1 decision-binding echo(操作员在 GET 看到的 boundCallId/boundInputHash 原样透传;server
|
|
62
72
|
* 绝不重算,core resume 侧做 fail-closed 等值校验——与 legacy 腿同姿势)。 */
|
package/dist/parked-decide.js
CHANGED
|
@@ -97,6 +97,13 @@ export async function decideParkedAgent(deps, req) {
|
|
|
97
97
|
// ([1588] Q1:name 必传承重——省略会把 revive 降级成匿名形)。
|
|
98
98
|
return { status: 409, body: { error: "parked row carries no agent name — the revive path requires a named agent (row damaged?)", errorCode: "decide.row_unrevivable", taskId: match.handle } };
|
|
99
99
|
}
|
|
100
|
+
// [1597]/A-010.1:席位重建**在 claim 之前**解开(codex 对抗复审 R1-中 的真 finding)。
|
|
101
|
+
// 🔴 位置即正确性:claim 之后到下面那道 try/catch(它才挂 `rollbackParkedClaim`)之间是一段
|
|
102
|
+
// **无回滚保护区**。工厂现在是 async(要现解 entitlement),把 await 放进那一段 = 任何 rejection
|
|
103
|
+
// 都带着一个已认领的 claim 逃逸,行上的 `parkClaimId` 要等 stale-claim 清算窗(默认可达一小时)
|
|
104
|
+
// 才放开,这期间该审批**再决不动**。搬到 claim 前:此刻还没有任何东西需要回滚,抛错就是干净地抛错。
|
|
105
|
+
// (工厂本身只读 row 的 scope/handle/sessionId/rootSessionId —— claim 不改这四个字段,搬位零语义差。)
|
|
106
|
+
const inheritedGate = deps.rebuildInheritedGate ? await deps.rebuildInheritedGate(row) : undefined;
|
|
100
107
|
const stores = { agentStore: deps.agentStore, checkpointStore: deps.checkpointStore };
|
|
101
108
|
const claim = await deps.registry.claimParkedAgent(stores, match.handle, { scope: row.scope, owner: row.owner });
|
|
102
109
|
if (!claim.ok) {
|
|
@@ -147,6 +154,8 @@ export async function decideParkedAgent(deps, req) {
|
|
|
147
154
|
// 是两件不同的事(姊妹 approval-hmac.ts `env.reason ?? null` 同判据:`??` 只在 null/undefined 时落
|
|
148
155
|
// null,空串照样入签名载荷)。真值判定会把显式 "" 与缺席折成同一个结果,审计/签名面丢了这个区分。
|
|
149
156
|
...(req.reason !== undefined ? { reason: req.reason } : {}),
|
|
157
|
+
// #204 件6③:结算出处逐字上 outcome(见 `ParkedDecideRequest.settledBy` 顶注)。
|
|
158
|
+
settledBy: req.settledBy,
|
|
150
159
|
};
|
|
151
160
|
const ctx = {
|
|
152
161
|
toolCallId: `drv-${ticket.claimId}`,
|
|
@@ -158,7 +167,7 @@ export async function decideParkedAgent(deps, req) {
|
|
|
158
167
|
outcome,
|
|
159
168
|
// [1597] 席位:重建父约束链(同进程时 core 的内存 registry 也能自动重供,席位供了也无害
|
|
160
169
|
// ——core 侧 count 校验对得上即用;跨进程/重启形全靠这里)。
|
|
161
|
-
...(deps.rebuildInheritedGate ? { inheritedGate
|
|
170
|
+
...(deps.rebuildInheritedGate ? { inheritedGate } : {}),
|
|
162
171
|
},
|
|
163
172
|
},
|
|
164
173
|
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { FilePermissionRuleStoreProvider, type RuleApprovalRecord, type RuleApprovalRecordStore, type RuleScope } from "@sema-agent/core";
|
|
2
|
+
import { type RuleImportTicket, type RuleTicketRedeemResult } from "./permission-rule-store-sql.js";
|
|
3
|
+
import type { PermissionRuleStoreBundle } from "../rules-consent.js";
|
|
4
|
+
/**
|
|
5
|
+
* `RuleApprovalRecordStore` 的 File 形。
|
|
6
|
+
*
|
|
7
|
+
* CAS 按 `rev`(core 硬条款,理由逐字见 SQL 侧同名类的头注:只比 state 会让批记录的第二个候选上两次
|
|
8
|
+
* 并发重试都以为自己赢了)。
|
|
9
|
+
*/
|
|
10
|
+
export declare class FileRuleApprovalRecordStore implements RuleApprovalRecordStore {
|
|
11
|
+
private readonly rows;
|
|
12
|
+
private readonly log;
|
|
13
|
+
private readonly gate;
|
|
14
|
+
constructor(dir: string, onError?: (message: string) => void);
|
|
15
|
+
/** 取证/自证用:本面是否因内部损坏而 fail-closed。 */
|
|
16
|
+
get corrupt(): boolean;
|
|
17
|
+
/** 归还本面 eager 持有的日志描述符(束的 `dispose()` 唯一调用点)。`AppendLog` 上没有 finalizer,
|
|
18
|
+
* 不显式关就是一只跟到进程末尾的 fd —— `LocalBackend` 反复开合(热重载/多根)时按次泄漏。
|
|
19
|
+
* 关后写面抛 `log_closed`(core 语义):**不重开**,因为「这只店已经交还」与「这条命还能写」
|
|
20
|
+
* 不能两立,静默重开会让一次 dispose 之后的写落进一份没人再读的日志。 */
|
|
21
|
+
close(): void;
|
|
22
|
+
get(id: string): Promise<RuleApprovalRecord | undefined>;
|
|
23
|
+
create(record: RuleApprovalRecord): Promise<void>;
|
|
24
|
+
cas(id: string, expectRev: number, next: RuleApprovalRecord): Promise<boolean>;
|
|
25
|
+
/** 与 SQL 侧同名方法同义(超帽拒绝时收掉 `prepareCcImport` 已落盘的那条 pending 记录)。
|
|
26
|
+
* `state === "pending"` 是硬的:已确认/已兑付的记录是一次真人同意的审计事实。 */
|
|
27
|
+
discardPendingRecord(recordId: string): Promise<boolean>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* CC 导入票的 File 形。四条信任边界(principal 绑定 / TTL / 一次性原子消费 / 载荷绑定)与 SQL 形
|
|
31
|
+
* **同语义**;唯一不同的是「原子」由谁保证 —— 那边是引擎行锁,这边是单进程 + 单写者(顶注)。
|
|
32
|
+
*/
|
|
33
|
+
export declare class FileRuleImportTicketStore {
|
|
34
|
+
private readonly rows;
|
|
35
|
+
private readonly log;
|
|
36
|
+
private readonly gate;
|
|
37
|
+
constructor(dir: string, now?: () => number, onError?: (message: string) => void);
|
|
38
|
+
private readonly now;
|
|
39
|
+
/** 取证/自证用:本面是否因内部损坏而 fail-closed。 */
|
|
40
|
+
get corrupt(): boolean;
|
|
41
|
+
/** 归还本面 eager 持有的日志描述符(理由与姊妹面 {@link FileRuleApprovalRecordStore.close} 逐字同源)。 */
|
|
42
|
+
close(): void;
|
|
43
|
+
mint(input: {
|
|
44
|
+
ticketId: string;
|
|
45
|
+
principal: string;
|
|
46
|
+
approvalId: string;
|
|
47
|
+
candidates: ReadonlyArray<{
|
|
48
|
+
rule: string;
|
|
49
|
+
scope: RuleScope;
|
|
50
|
+
}>;
|
|
51
|
+
ttlMs: number;
|
|
52
|
+
}): Promise<RuleImportTicket>;
|
|
53
|
+
/** 一次性**认领**。四条否定项的判序与 SQL 形逐字相同(unknown → wrong-principal → consumed → expired),
|
|
54
|
+
* 于是两形的服务端日志归因可比;wire 面把四类折成同一个 404(零存在性 oracle)。 */
|
|
55
|
+
consume(ticketId: string, principal: string): Promise<RuleTicketRedeemResult>;
|
|
56
|
+
/** 把认领**放回去**。`mustRemainValidMs` = 放回之后至少还要能用多久 —— 撑不过就不算放回成功
|
|
57
|
+
* (理由逐字见 SQL 侧 `release` 的头注:承诺一次必然兑现不了的重试比不承诺更坏)。 */
|
|
58
|
+
release(ticketId: string, principal: string, mustRemainValidMs?: number): Promise<boolean>;
|
|
59
|
+
}
|
|
60
|
+
/** local 车道的三面束 + 生命周期。`dispose()` 释放 core File provider 的写锁(见 {@link createFilePermissionRuleStores})。 */
|
|
61
|
+
export interface FilePermissionRuleStores extends PermissionRuleStoreBundle {
|
|
62
|
+
provider: FilePermissionRuleStoreProvider;
|
|
63
|
+
approvals: FileRuleApprovalRecordStore;
|
|
64
|
+
tickets: FileRuleImportTicketStore;
|
|
65
|
+
dispose(): void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 装配 local 三面束。
|
|
69
|
+
*
|
|
70
|
+
* 🔴 `provider` 必须是**单例**(F1):core 的 `FilePermissionRuleStoreProvider` 在第一次取写面时对规则
|
|
71
|
+
* 目录取一把进程级写锁,每次 `new` 一个就是第二个持有者 —— 而它对第二个持有者是**响亮拒绝**,不是
|
|
72
|
+
* 排队。所以束在 `LocalBackend` 上按字段持有,`LocalBackend.close()` 调 `dispose()` 释放
|
|
73
|
+
* (不释放 ⇒ 一次优雅重启会被自己上一条命留下的锁挡在门外,与数据根 `root/LOCK` 同一个病)。
|
|
74
|
+
*
|
|
75
|
+
* `onError` 接的是 core 规则文件的**披露面**(读不出来 / 校验和不符 / 撞上符号链接时它答零规则并
|
|
76
|
+
* 说出来)。接住它打一条 warn 是**必须**的:那条路径上「零规则」与「真的没有规则」在读面上同形,
|
|
77
|
+
* 不留痕就变成一次静默的 fail-closed(用户会突然被反复询问,却没有任何线索)。
|
|
78
|
+
*/
|
|
79
|
+
export declare function createFilePermissionRuleStores(root: string, opts?: {
|
|
80
|
+
onError?: (message: string) => void;
|
|
81
|
+
now?: () => number;
|
|
82
|
+
}): FilePermissionRuleStores;
|
|
83
|
+
//# sourceMappingURL=permission-rule-store-file.d.ts.map
|