@sema-agent/server 7.4.0 → 7.6.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/LICENSE +1 -1
- package/README.md +18 -3
- package/README.zh-CN.md +14 -3
- package/USAGE.md +80 -1
- package/dist/approval-card.d.ts +15 -3
- package/dist/approval-card.js +41 -7
- package/dist/approval-reconciler.d.ts +109 -12
- package/dist/approval-reconciler.js +152 -24
- package/dist/boot/config-center.js +15 -2
- package/dist/boot/coordinators.js +10 -2
- package/dist/boot/execution-env.js +1 -1
- package/dist/boot/org-memory.d.ts +6 -0
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/parked-revive-gate.d.ts +78 -0
- package/dist/boot/parked-revive-gate.js +114 -0
- package/dist/boot/reapers.d.ts +2 -0
- package/dist/boot/reapers.js +11 -4
- package/dist/boot/resolve-spec.d.ts +3 -19
- package/dist/boot/resolve-spec.js +73 -67
- package/dist/boot/runner-deps.d.ts +23 -1
- package/dist/boot/runner-deps.js +8 -11
- package/dist/boot/workflow-orchestration.d.ts +8 -3
- package/dist/boot/workflow-orchestration.js +23 -1
- package/dist/budget.d.ts +1 -1
- package/dist/budget.js +1 -1
- package/dist/capabilities/repo-tools.d.ts +1 -1
- package/dist/capabilities/repo-tools.js +8 -2
- package/dist/config-center/apply-effective.js +33 -10
- package/dist/config-provider.d.ts +1 -0
- package/dist/config-provider.js +23 -3
- package/dist/config-types.d.ts +27 -9
- package/dist/config.d.ts +6 -1
- package/dist/config.js +61 -15
- package/dist/deployment-governance.d.ts +168 -0
- package/dist/deployment-governance.js +206 -0
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +3 -1
- package/dist/fleet/fleet-bus.d.ts +17 -2
- package/dist/fleet/fleet-bus.js +68 -3
- package/dist/fleet/fleet-terminal-window.d.ts +98 -0
- package/dist/fleet/fleet-terminal-window.js +316 -0
- package/dist/governance-ask-marks.d.ts +31 -0
- package/dist/governance-ask-marks.js +122 -0
- package/dist/hooks/hook-runner.d.ts +28 -0
- package/dist/hooks/hook-runner.js +149 -25
- package/dist/http/routes/approvals-assistant.js +6 -7
- package/dist/http/routes/diagnostics.js +10 -5
- package/dist/http/routes/fleet.js +160 -14
- package/dist/http/routes/memory-policy.d.ts +2 -1
- package/dist/http/routes/memory-policy.js +77 -13
- package/dist/http/routes/runs.js +6 -2
- package/dist/http/routes/tasks.js +59 -22
- package/dist/http/routes/trace-usage.js +3 -4
- package/dist/http/send.d.ts +23 -0
- package/dist/http/send.js +23 -0
- package/dist/http/server.d.ts +9 -0
- package/dist/http/server.js +28 -14
- package/dist/http/sse-log.js +3 -4
- package/dist/http/wire-types.d.ts +7 -2
- package/dist/leader/diffout.d.ts +10 -0
- package/dist/leader/diffout.js +14 -2
- package/dist/leader/diffup.js +3 -2
- package/dist/leader/planner.js +7 -0
- package/dist/main.js +39 -31
- package/dist/observability/fail-open.d.ts +17 -2
- package/dist/observability/fail-open.js +19 -4
- package/dist/observability/prompt-manifest.d.ts +5 -1
- package/dist/orchestration/workflow-notify-journal.d.ts +58 -2
- package/dist/orchestration/workflow-notify-journal.js +130 -45
- package/dist/parked-decide.js +9 -4
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
- package/dist/plugins/approval-ask-store-memory.js +3 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +60 -5
- package/dist/plugins/approval-ask-store-sql.js +75 -35
- package/dist/plugins/background-agent-store-sql.js +16 -16
- package/dist/plugins/background-shell-support.d.ts +1 -1
- package/dist/plugins/background-shell-support.js +2 -2
- package/dist/plugins/breaker-state-sql.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +67 -8
- package/dist/plugins/checkpoint-store-sql.js +76 -13
- package/dist/plugins/image-bake-store-sql.d.ts +1 -1
- package/dist/plugins/image-bake-store-sql.js +27 -27
- package/dist/plugins/image-index-sql.js +15 -15
- package/dist/plugins/local-checkpoint-store.d.ts +20 -1
- package/dist/plugins/local-checkpoint-store.js +19 -0
- package/dist/plugins/mailbox-store-sql.d.ts +4 -10
- package/dist/plugins/mailbox-store-sql.js +59 -6
- package/dist/plugins/memory-engine-pg.js +9 -9
- package/dist/plugins/memory-engine-tidb.js +7 -7
- package/dist/plugins/memory-sync-store-pg.js +13 -13
- package/dist/plugins/memory-sync-store-tidb.js +5 -5
- package/dist/plugins/outcome-ledger-sql.js +7 -7
- package/dist/plugins/pg-cost-quota.js +3 -3
- package/dist/plugins/pg-pool.js +84 -75
- package/dist/plugins/pg-rate-limiter.js +3 -3
- package/dist/plugins/pg-session-storage.d.ts +1 -1
- package/dist/plugins/pg-session-storage.js +12 -13
- package/dist/plugins/remote-env-host.js +3 -1
- package/dist/plugins/remote-env-local-docker.js +6 -3
- package/dist/plugins/remote-env-ssh.d.ts +13 -1
- package/dist/plugins/roster-store-sql.js +8 -8
- package/dist/plugins/store-contracts.d.ts +19 -0
- package/dist/plugins/store-contracts.js +42 -0
- package/dist/plugins/task-attachment-store.js +5 -5
- package/dist/plugins/task-list-store-sql.js +1 -1
- package/dist/plugins/tidb-cost-quota.js +1 -1
- package/dist/plugins/tidb-pool.js +83 -60
- package/dist/plugins/tidb-rate-limiter.js +1 -1
- package/dist/plugins/tidb-session-store.js +2 -5
- package/dist/plugins/tool-result-store-sql.js +2 -2
- package/dist/plugins/usage-window-store-sql.js +13 -13
- package/dist/plugins/write-behind-counter.d.ts +10 -2
- package/dist/plugins/write-behind-counter.js +13 -3
- package/dist/resource-suspend.d.ts +3 -1
- package/dist/resource-suspend.js +3 -1
- package/dist/run-local.d.ts +73 -1
- package/dist/run-local.js +146 -5
- package/dist/runs.d.ts +11 -1
- package/dist/runs.js +18 -3
- package/dist/runtime-governance.d.ts +18 -0
- package/dist/runtime-governance.js +90 -3
- package/dist/security.d.ts +12 -0
- package/dist/security.js +12 -0
- package/dist/session-sync-kernel.d.ts +13 -0
- package/dist/session-sync-kernel.js +13 -0
- package/dist/task-settings.d.ts +3 -9
- package/dist/task-settings.js +16 -13
- package/dist/tool-approval.d.ts +33 -6
- package/dist/tool-approval.js +80 -23
- package/dist/trace/core-keyset-guard.d.ts +18 -4
- package/dist/trace/project.d.ts +10 -1
- package/dist/trace/project.js +31 -0
- package/package.json +3 -3
- package/dist/boot/lexical-path-env.d.ts +0 -10
- package/dist/boot/lexical-path-env.js +0 -88
- package/dist/capabilities/oa-tools.d.ts +0 -15
- package/dist/capabilities/oa-tools.js +0 -54
- package/dist/finance/cost-taxonomy.d.ts +0 -34
- package/dist/finance/cost-taxonomy.js +0 -26
- package/dist/plugins/approval-store-sql.d.ts +0 -116
- package/dist/plugins/approval-store-sql.js +0 -151
- package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
- package/dist/plugins/file-workflow-journal-store.js +0 -12
- package/dist/plugins/pg-approval-store.d.ts +0 -9
- package/dist/plugins/pg-approval-store.js +0 -9
- package/dist/plugins/pg-breaker-state.d.ts +0 -8
- package/dist/plugins/pg-breaker-state.js +0 -8
- package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
- package/dist/plugins/pg-checkpoint-store.js +0 -10
- package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/pg-file-snapshot-store.js +0 -8
- package/dist/plugins/pg-image-bake.d.ts +0 -12
- package/dist/plugins/pg-image-bake.js +0 -11
- package/dist/plugins/pg-image-index.d.ts +0 -12
- package/dist/plugins/pg-image-index.js +0 -11
- package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
- package/dist/plugins/pg-outcome-ledger.js +0 -11
- package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/pg-resume-anchor-store.js +0 -7
- package/dist/plugins/pg-run-store.d.ts +0 -9
- package/dist/plugins/pg-run-store.js +0 -9
- package/dist/plugins/pg-session-policy-store.d.ts +0 -7
- package/dist/plugins/pg-session-policy-store.js +0 -7
- package/dist/plugins/pg-session-store.d.ts +0 -12
- package/dist/plugins/pg-session-store.js +0 -12
- package/dist/plugins/pg-tool-result-store.d.ts +0 -9
- package/dist/plugins/pg-tool-result-store.js +0 -9
- package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-journal-store.js +0 -9
- package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-run-store.js +0 -9
- package/dist/plugins/tidb-approval-store.d.ts +0 -8
- package/dist/plugins/tidb-approval-store.js +0 -8
- package/dist/plugins/tidb-breaker-state.d.ts +0 -7
- package/dist/plugins/tidb-breaker-state.js +0 -7
- package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
- package/dist/plugins/tidb-checkpoint-store.js +0 -9
- package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/tidb-file-snapshot-store.js +0 -8
- package/dist/plugins/tidb-image-bake.d.ts +0 -12
- package/dist/plugins/tidb-image-bake.js +0 -11
- package/dist/plugins/tidb-image-index.d.ts +0 -12
- package/dist/plugins/tidb-image-index.js +0 -11
- package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
- package/dist/plugins/tidb-outcome-ledger.js +0 -12
- package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/tidb-resume-anchor-store.js +0 -7
- package/dist/plugins/tidb-run-store.d.ts +0 -10
- package/dist/plugins/tidb-run-store.js +0 -9
- package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
- package/dist/plugins/tidb-session-policy-store.js +0 -7
- package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
- package/dist/plugins/tidb-tool-result-store.js +0 -10
- package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/tidb-workflow-journal-store.js +0 -9
- package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
- package/dist/plugins/tidb-workflow-run-store.js +0 -10
- package/dist/plugins/workflow-journal-limits.d.ts +0 -12
- package/dist/plugins/workflow-journal-limits.js +0 -12
- package/dist/sema-registry.d.ts +0 -41
- package/dist/sema-registry.js +0 -40
package/dist/task-settings.d.ts
CHANGED
|
@@ -192,16 +192,10 @@ export interface FsWriteGateWiring {
|
|
|
192
192
|
* allow with decisionReason "rule", a THROWN probe ⇒ not exempt = fail-closed to ask). Same canonical
|
|
193
193
|
* toolName key space as the ask-policy layer's exemption probe — ONE grant serves both layers; the gate-level
|
|
194
194
|
* check is what makes "allow all session" bite SAME-TURN for the parent and inherited child tasks (the ask
|
|
195
|
-
* path is never re-entered). (codex M1
|
|
196
|
-
*
|
|
197
|
-
*
|
|
195
|
+
* path is never re-entered). (codex M1 的边界注记:sensitive-path DENY 腿与本探针的「豁免越不过 deny」
|
|
196
|
+
* 保证仍成立——但那条腿已随 #177 搬到 governance 层,deny-wins 是 tightenTaskSpec 折叠给的,不再依赖
|
|
197
|
+
* 本 wiring 的组合点。) */
|
|
198
198
|
isExempt?: (toolName: string, canonicalPath: string) => boolean | Promise<boolean>;
|
|
199
|
-
/** ③ (core 1.295): the sensitive-path write DENY set composed onto the fs-write gate leg —
|
|
200
|
-
* `createSensitivePathPolicy({env, patterns, rootPath: cwd})`, deny-wins in the SAME combinePolicies fold, so a
|
|
201
|
-
* session exemption (isExempt) or an acceptEdits auto-allow can never cross it (the 1.199-batch documented gap,
|
|
202
|
-
* now closed). Source = config `SENSITIVE_WRITE_PATTERNS` (unset ⇒ core RECOMMENDED_SENSITIVE_PATTERNS; the
|
|
203
|
-
* set curation is core's — the server passes through). Absent/empty ⇒ no sensitive policy (pre-1.295 shape). */
|
|
204
|
-
sensitivePatterns?: string[];
|
|
205
199
|
}
|
|
206
200
|
export declare function deriveSettingsPolicy(settings: ParsedTaskSettings, gate?: FsWriteGateWiring, workflowGate?: WorkflowGateWiring): {
|
|
207
201
|
toolPolicy?: ToolPolicy;
|
package/dist/task-settings.js
CHANGED
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
* - (`hooks` NO LONGER deferred either — 轴B #2 注释保鲜:applied 形已接线(本文件 hooks 分片 +
|
|
36
36
|
* main.ts 单用户闸),仅 malformed 才报 deferred;旧「需要 remote hook-runner」是接线前拍照。)
|
|
37
37
|
*/
|
|
38
|
-
import { combinePolicies, createAllowDenyPolicy, createFsWriteGatePolicy,
|
|
39
|
-
import { findUnmatchableToolNames, formatUnmatchableToolNames } from "./config.js";
|
|
38
|
+
import { combinePolicies, createAllowDenyPolicy, createFsWriteGatePolicy, tightenTaskSpec, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, } from "@sema-agent/core";
|
|
39
|
+
import { findCcRuleFormNames, findUnmatchableToolNames, formatUnmatchableToolNames } from "./config.js";
|
|
40
40
|
import { parseHooksConfig } from "./hooks/hook-runner.js";
|
|
41
41
|
/**
|
|
42
42
|
* R4: coerce a raw top-level `body.permissionMode` (the LIGHT per-turn wire — the shell sends the
|
|
@@ -367,9 +367,12 @@ function createWorkflowPlanDenyPolicy() {
|
|
|
367
367
|
* silent on this leg. Throwing here surfaces as a 422 at the HTTP boundary (resolve-spec already wraps every
|
|
368
368
|
* throw out of `applyTaskSettings`), i.e. the same fail-loud posture the other two legs have.
|
|
369
369
|
*
|
|
370
|
-
* `allow` is deliberately NOT judged: it is a WHITELIST — a name that matches nothing leaves
|
|
371
|
-
* effectively empty, so every tool falls to "not in the allowlist" and is DENIED. That fails closed
|
|
372
|
-
* immediately visible to the caller, which is a different (and acceptable) failure mode from a silent hole.
|
|
370
|
+
* `allow` is deliberately NOT judged by the GENERAL arm: it is a WHITELIST — a name that matches nothing leaves
|
|
371
|
+
* the allowlist effectively empty, so every tool falls to "not in the allowlist" and is DENIED. That fails closed
|
|
372
|
+
* and is immediately visible to the caller, which is a different (and acceptable) failure mode from a silent hole.
|
|
373
|
+
* The ONE exception is the CC-rule-form arm ({@link findCcRuleFormNames}, #186): `allow: ["Bash(ps:*)"]` is the
|
|
374
|
+
* shape a CC user writes to get FEWER prompts and it silently costs them Bash entirely — a spelling-family error,
|
|
375
|
+
* not a typo, so it is refused on all three lists. See the config.ts docblock for the two knobs it points at.
|
|
373
376
|
*/
|
|
374
377
|
function rejectUnmatchableSettingsNames(perms) {
|
|
375
378
|
for (const [field, names] of [["permissions.ask", perms.ask], ["permissions.deny", perms.deny]]) {
|
|
@@ -379,6 +382,11 @@ function rejectUnmatchableSettingsNames(perms) {
|
|
|
379
382
|
if (bad.length > 0)
|
|
380
383
|
throw new Error(formatUnmatchableToolNames(field, bad));
|
|
381
384
|
}
|
|
385
|
+
if (perms.allow) {
|
|
386
|
+
const bad = findCcRuleFormNames(perms.allow);
|
|
387
|
+
if (bad.length > 0)
|
|
388
|
+
throw new Error(formatUnmatchableToolNames("permissions.allow", bad));
|
|
389
|
+
}
|
|
382
390
|
}
|
|
383
391
|
export function deriveSettingsPolicy(settings, gate, workflowGate) {
|
|
384
392
|
const perms = settings.permissions;
|
|
@@ -398,11 +406,8 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
|
|
|
398
406
|
// the given env, only ever allow/ask). undefined without wiring (see FsWriteGateWiring). #156/#165: the sandbox
|
|
399
407
|
// lanes supply wiring too — same factory, a deferred real-env proxy and no cwd; the shape difference lives
|
|
400
408
|
// entirely in what `resolve-spec.ts` puts in the wiring, not in a second code path here.
|
|
401
|
-
// ③
|
|
402
|
-
//
|
|
403
|
-
// neither the gate's own allow legs (exemptDirs/acceptDirs/isExempt session exemption) nor a settings allow can
|
|
404
|
-
// cross it (the 1.199-batch gap: an exempted tool could write .git/hooks — closed here). Same env/rootPath as
|
|
405
|
-
// the gate (the fs the hands really write; symlink canonicalization inside the policy, dist-read).
|
|
409
|
+
// (③ sensitive-path DENY 已随 #177 搬 governance 层——见 FsWriteGateWiring 尾注。deny-wins 由
|
|
410
|
+
// tightenTaskSpec 折叠保证:豁免/acceptDirs/settings allow 仍然越不过部署 DENY 基线。)
|
|
406
411
|
const fsWriteGate = (acceptCwd) => {
|
|
407
412
|
if (!gate)
|
|
408
413
|
return undefined;
|
|
@@ -422,9 +427,7 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
|
|
|
422
427
|
// NotebookEdit 时 notebook_path 恒先(诱饵 file_path 失效),其余工具 file_path 先(反向诱饵也封);均 dist
|
|
423
428
|
// 亲读。回归锚保留在 test/task-settings.test.ts(NotebookEdit deny + 双向诱饵),现在锁的是 core 行为经
|
|
424
429
|
// 我方折叠——core 若回退,锚变红。
|
|
425
|
-
return
|
|
426
|
-
? combinePolicies(createSensitivePathPolicy({ env: gate.env, patterns: gate.sensitivePatterns, ...(gate.cwd !== undefined ? { rootPath: gate.cwd } : {}) }), gatePolicy)
|
|
427
|
-
: gatePolicy;
|
|
430
|
+
return gatePolicy;
|
|
428
431
|
};
|
|
429
432
|
const compose = (...extra) => {
|
|
430
433
|
const all = [...policies, ...extra.filter((p) => p !== undefined)];
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AskRequest, type AskOutcome } from "@sema-agent/core";
|
|
2
2
|
import { type ApprovalRequestFrame, type ApprovalRevokeFrame } from "./approval-card.js";
|
|
3
3
|
import type { ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
|
|
4
|
+
import { type GovernanceAskMarks } from "./governance-ask-marks.js";
|
|
4
5
|
/** A live approval frame delivered to whoever tails this run's stream. `type` IS the SSE event name (named-event
|
|
5
6
|
* convention, same as question). The shell renders `tool_approval` as the CC three-choice card and dismisses on
|
|
6
7
|
* `tool_approval_complete`. */
|
|
@@ -37,6 +38,19 @@ export interface ToolApprovalFrame {
|
|
|
37
38
|
agentName?: string;
|
|
38
39
|
};
|
|
39
40
|
message?: string;
|
|
41
|
+
/**
|
|
42
|
+
* [2942]/[2943] **ADDITIVE**,`"tool_approval"` only —— `true` ⇔ 这只 ask 的门来自**运维治理层**
|
|
43
|
+
* (`AUTONOMY` / `commandPolicy` / `MANUAL_MODE_SHELL_GATE` / `SENSITIVE_WRITE_PATTERNS` 合成的那条
|
|
44
|
+
* policy,`applyRuntimeGovernance`),而不是模型默认门或客户端表态。壳据此渲染「治理强制」徽标,回答
|
|
45
|
+
* [3031]#1/[3038] 实证的那个 UX 缺口:「我都开 bypassPermissions 了为什么还在问」。
|
|
46
|
+
*
|
|
47
|
+
* 🔴 **缺席 ≠ `false`**:本键**只在为真时在场**,缺席的含义是「没有治理来源的证据」——既覆盖真正的
|
|
48
|
+
* 非治理 ask(如 `APPROVAL_REQUIRE` 显式列名),也覆盖判据够不着的形(如治理层 shellGate 停在
|
|
49
|
+
* `"classify"` 档时,一次 shell ask 究竟出自分类器还是别的门,在 `AskRequest` 上无从分辨)。
|
|
50
|
+
* 消费端**禁**把缺席读成「这不是治理门」。判定缝的真形与为什么不能走 `decisionReason`:
|
|
51
|
+
* `governance-ask-marks.ts` 顶注。
|
|
52
|
+
*/
|
|
53
|
+
governanceForced?: true;
|
|
40
54
|
/** "tool_approval" only: the tool call's args, secret-redacted, UNTRUSTED-for-display. Absent (with
|
|
41
55
|
* `argsOmitted: true`) when over the byte cap or unserializable. */
|
|
42
56
|
args?: unknown;
|
|
@@ -61,8 +75,10 @@ export interface ToolApprovalRunContext {
|
|
|
61
75
|
* 协议**(`approval_request` 族)的一员,消费方是新壳 —— 分口投递是诚实的分层,装配点(车3 3b /
|
|
62
76
|
* 车4 域)可以与 `approval_request` 的发射点**同批**接上。
|
|
63
77
|
*
|
|
64
|
-
* 缺席 ⇒
|
|
65
|
-
*
|
|
78
|
+
* 缺席 ⇒ 本连接不收撤卡帧;壳侧的结构补偿恒是重连 preamble 的全量对账基准。
|
|
79
|
+
* (原注拿「`approval_request` 今天也还没接上发射点」作同栏类比 —— 那个类比已被 60846e6 证伪:呈卡口
|
|
80
|
+
* 三腿都接上了,而本钩子至今只有 sync 腿接 live 口。两者不再同栏,类比句因此删掉,别再据它推断
|
|
81
|
+
* 「两族都还没接线」。)
|
|
66
82
|
*/
|
|
67
83
|
emitRevoke?: (frame: ApprovalRevokeFrame) => void | Promise<void>;
|
|
68
84
|
/**
|
|
@@ -115,7 +131,8 @@ export declare function parseToolApprovalResponse(body: unknown): {
|
|
|
115
131
|
*
|
|
116
132
|
* 五个合取项,缺一即不上场(每一项的缺席都有它自己的 `reason`,供启动期 info 与诊断分辨):
|
|
117
133
|
* 1. `toolApprovalEnabled` —— 连活卡腿都没有,谈不上流内协议;
|
|
118
|
-
* 2. `streamApprovalEnabled` —— 协议总开关(
|
|
134
|
+
* 2. `streamApprovalEnabled` —— 协议总开关(`STREAM_APPROVAL_ENABLED`,树上已**默认 ON**;显式 `false`
|
|
135
|
+
* 是唯一干净还原键。默认极性与版本坐标的属主口径见 `config.ts` 的 `streamApprovalConfig()` 头注);
|
|
119
136
|
* 3. `backend` 在场 —— env-only worker 没有 `StoreBackend` 本体;
|
|
120
137
|
* 4. **账必须是持久的**(`kind !== "local"`)—— InMemory 形重启即丢**已接受的决议**,那与「对账便利
|
|
121
138
|
* 丢失」不是一个量级(§2.2(b));File 形 ask 店落地后这一项自然翻真;
|
|
@@ -135,9 +152,13 @@ export type StreamApprovalGate = {
|
|
|
135
152
|
* 协调器的家,不该为一个布尔判据把整棵 store 依赖树拖进类型面。 */
|
|
136
153
|
export interface StreamApprovalGateInput {
|
|
137
154
|
toolApprovalEnabled: boolean;
|
|
138
|
-
/** 协议总开关。⚠️ 调用点一律写 `config.streamApproval?.enabled === true` ——
|
|
139
|
-
*
|
|
140
|
-
*
|
|
155
|
+
/** 协议总开关。⚠️ 调用点一律写 `config.streamApproval?.enabled === true` —— **可选链的缺席臂折算
|
|
156
|
+
* false**(段整段缺席 ⇒ 协议不上场)。
|
|
157
|
+
* 🔴 当年这条写的理由是「缺席折算值 == 产品默认(那时是 OFF)」,所以 `?.` 纯粹无副作用;产品默认
|
|
158
|
+
* 现已翻 ON(`config.ts` 的 `streamApprovalConfig()`),两者**不再相等**,那条理由作废。现行的
|
|
159
|
+
* (更弱也更诚实的)判据:真实 boot 恒有此段(`config.ts` 无条件铸 + 白名单门盯着),`?.` 服务的只是
|
|
160
|
+
* 那些只填被测路由用得到的键的 stub-harness,而一个没填这个段的 harness 要的必然是「别上场」。
|
|
161
|
+
* **不要**照「缺席 = 与产品默认同」去推理。 */
|
|
141
162
|
streamApprovalEnabled: boolean;
|
|
142
163
|
backend: {
|
|
143
164
|
readonly kind: "mysql" | "pg" | "local";
|
|
@@ -276,12 +297,18 @@ export declare class ToolApprovalCoordinator {
|
|
|
276
297
|
/** #151 车2(D5 一次性 warn 节流):同实例只报第一次,后续只计数(避免 store 抖动期间刷屏)。 */
|
|
277
298
|
private storeErrorWarned;
|
|
278
299
|
private storeErrorTally;
|
|
300
|
+
/** [2942]/[2943] `governanceForced` 的**读侧**表(写侧 = runtime-governance 的两只观察器)。
|
|
301
|
+
* 缺席(生产形)⇒ 每条 run 腿由 {@link runWithContext} 现铸一张、经 ALS 与写侧共享;在场 ⇒ 测试注入的
|
|
302
|
+
* 固定表(此时不进 ALS 作用域,读写都走这一张)。作用域理由见 `governance-ask-marks.ts` 顶注。 */
|
|
303
|
+
private readonly governanceAskMarks;
|
|
279
304
|
constructor(opts?: {
|
|
280
305
|
ttlMs?: number;
|
|
281
306
|
askStore?: ApprovalAskStore;
|
|
282
307
|
windowMarginMs?: number;
|
|
283
308
|
admitMaxPerTask?: number;
|
|
284
309
|
admitMaxPerOwner?: number;
|
|
310
|
+
/** 缺省 = 进程级单表(写侧默认同一张)。注入口只为测试与将来的多实例形。 */
|
|
311
|
+
governanceAskMarks?: GovernanceAskMarks;
|
|
285
312
|
});
|
|
286
313
|
/**
|
|
287
314
|
* #151 车3 刀 3b —— design/172 §3.3 / 设计稿 §0 X-2 的**写侧准入门**。
|
package/dist/tool-approval.js
CHANGED
|
@@ -25,8 +25,11 @@
|
|
|
25
25
|
* grant. This is the cheapest mid-task implementation; a mode SWITCH (default → acceptEdits) still goes through the
|
|
26
26
|
* shell's intent on the next task ([820] table).
|
|
27
27
|
*
|
|
28
|
-
* 🔒 FAIL-CLOSED everywhere (the inverse of question
|
|
29
|
-
*
|
|
28
|
+
* 🔒 FAIL-CLOSED everywhere (the inverse of the question lane — an unanswered QUESTION never blocks the run: since
|
|
29
|
+
* #166 the coordinator reports `unavailable` and CORE decides the landing, which depends on WHEN the human became
|
|
30
|
+
* unreachable — at policy time the durable gate says `ask` and the leg parks; once the tool is already running core is
|
|
31
|
+
* past `suspendAsk`, so it continues on `declined_unavailable`; an unanswered APPROVAL must NOT let the write
|
|
32
|
+
* proceed): TTL expiry ⇒ false; run abort / client
|
|
30
33
|
* disconnect ⇒ false. Since [879] G1 (core 1.295 OnAsk 三值化) the "no reachable human" class returns
|
|
31
34
|
* "unavailable" instead of false — no ALS context AND no bound-closure broker hit(1.258 起 bg/嵌套子代经
|
|
32
35
|
* spec.onAsk 的 bound 闭包 + streams broker 直达宿主活流,「无 ctx」只剩 durable-submit/headless resume
|
|
@@ -46,14 +49,21 @@ import { redactDeep, redactSecrets } from "./trace/redact.js";
|
|
|
46
49
|
import { createLogger } from "./observability/logger.js";
|
|
47
50
|
import { deriveAskId, deriveBatchId } from "./approval-ask-machine.js";
|
|
48
51
|
import { ApprovalCardEnvelopeSchema, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, } from "./approval-card.js";
|
|
52
|
+
import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
|
|
49
53
|
/** #151 车2:本模块自有的日志出口——同 config-provider.ts/runtime-caps-resolver.ts 先例(协调器不走
|
|
50
54
|
* DI logger,构造签名是设计定稿钉死的三键 options bag,加第四个 logger 键属于重议已裁事项)。仅用于
|
|
51
55
|
* D5 的一次性 store-故障 warn。 */
|
|
52
56
|
const defaultLogger = createLogger();
|
|
53
57
|
/** Size bound on the redacted args payload in a `tool_approval` frame (parity with question's MAX_QUESTIONS_BYTES).
|
|
54
|
-
* Over the cap ⇒ the frame still goes out WITHOUT args (`argsOmitted: true`)
|
|
55
|
-
*
|
|
56
|
-
*
|
|
58
|
+
* Over the cap ⇒ the frame still goes out WITHOUT args (`argsOmitted: true`). The sibling lane makes the OPPOSITE
|
|
59
|
+
* call and that asymmetry is deliberate: an over-cap question is DROPPED and the ask reports `unavailable`
|
|
60
|
+
* (`question.ts` `MAX_QUESTIONS_BYTES`; since #166 that is an honest "nobody answered" and CORE picks the landing —
|
|
61
|
+
* here the tool is already running, so core is past `suspendAsk` and continues on `declined_unavailable` rather than
|
|
62
|
+
* parking). Dropping an approval would land it on
|
|
63
|
+
* this module's "no reachable human" class instead — `"unavailable"`, which durable-parks where a checkpoint store
|
|
64
|
+
* exists and fail-closed DENIES where it does not (see the module header). Both of those postpone or refuse the
|
|
65
|
+
* write; keeping the card on the wire without args is the only option that still buys an IMMEDIATE human decision —
|
|
66
|
+
* toolName+message suffice to decide, and the fail-safe decision on blind args is the human's No. */
|
|
57
67
|
const MAX_APPROVAL_ARGS_BYTES = 16384;
|
|
58
68
|
/** How long an unanswered approval waits before it DENIES (fail-closed — the human walked away; don't hold core). */
|
|
59
69
|
const DEFAULT_APPROVAL_TTL_MS = 5 * 60_000;
|
|
@@ -325,7 +335,12 @@ export class ToolApprovalCoordinator {
|
|
|
325
335
|
/** #151 车2(D5 一次性 warn 节流):同实例只报第一次,后续只计数(避免 store 抖动期间刷屏)。 */
|
|
326
336
|
storeErrorWarned = false;
|
|
327
337
|
storeErrorTally = 0;
|
|
338
|
+
/** [2942]/[2943] `governanceForced` 的**读侧**表(写侧 = runtime-governance 的两只观察器)。
|
|
339
|
+
* 缺席(生产形)⇒ 每条 run 腿由 {@link runWithContext} 现铸一张、经 ALS 与写侧共享;在场 ⇒ 测试注入的
|
|
340
|
+
* 固定表(此时不进 ALS 作用域,读写都走这一张)。作用域理由见 `governance-ask-marks.ts` 顶注。 */
|
|
341
|
+
governanceAskMarks;
|
|
328
342
|
constructor(opts) {
|
|
343
|
+
this.governanceAskMarks = opts?.governanceAskMarks;
|
|
329
344
|
this.ttlMs = opts?.ttlMs ?? DEFAULT_APPROVAL_TTL_MS;
|
|
330
345
|
this.askStore = opts?.askStore;
|
|
331
346
|
this.windowMarginMs = opts?.windowMarginMs ?? DEFAULT_WINDOW_MARGIN_MS;
|
|
@@ -562,7 +577,13 @@ export class ToolApprovalCoordinator {
|
|
|
562
577
|
}
|
|
563
578
|
const liveSet = set;
|
|
564
579
|
liveSet.add(ctx);
|
|
565
|
-
|
|
580
|
+
// [2942]/[2943](codex round3/round5 两轮的修形):**写侧**的治理标作用域与审批 ctx 同拍进出,分格键
|
|
581
|
+
// 就是上面这把 broker `key` —— 读侧(askBroadcast)按**出处**显式算出同一把,于是委派/长命 bg 子代的
|
|
582
|
+
// ask 即便在另一条腿的上下文里发出也查得到自己的标(round5 的红先用例)。分格本身封的是 round3:
|
|
583
|
+
// `toolCallId` 是提供方给的、并非全局唯一,不分格会跨任务/跨租户串味(理由全文见 governance-ask-marks.ts)。
|
|
584
|
+
// 注入了固定表的实例(测试形)不再进作用域:读写都走那一张,行为与注入前逐字一致。
|
|
585
|
+
const enter = (body) => (this.governanceAskMarks ? body() : runWithGovernanceAskScope(key, body));
|
|
586
|
+
return enter(() => this.als.run(ctx, async () => {
|
|
566
587
|
try {
|
|
567
588
|
return await fn();
|
|
568
589
|
}
|
|
@@ -580,7 +601,7 @@ export class ToolApprovalCoordinator {
|
|
|
580
601
|
p.onTargetGone?.(ctx);
|
|
581
602
|
}
|
|
582
603
|
}
|
|
583
|
-
});
|
|
604
|
+
}));
|
|
584
605
|
}
|
|
585
606
|
/** 测试/可观测性钩子:该 (owner, session/taskId) broker key 下当前活跃连接数——断言多连接注册/清扫
|
|
586
607
|
* 行为时用,免得伸手进私有内部状态。 */
|
|
@@ -608,6 +629,12 @@ export class ToolApprovalCoordinator {
|
|
|
608
629
|
* emit 时经 broker 取该 (owner, session) 的「当前」活跃流(宿主重连/续聊的新 SSE 自动接卡;
|
|
609
630
|
* internalsSnapshot 冻结的闭包因此永不携死流)。查无活流 = "unavailable"(G1 回路:durable 部署
|
|
610
631
|
* park;inherited plain-ask 的 park 回路 = core RB-39①,落地前该臂 = deny,与 1.257 前行为一致)。 */
|
|
632
|
+
/* [2942]/[2943] 与本闭包的关系(codex round4/round5 两轮追出来的形):本闭包**铸于**
|
|
633
|
+
* `runWithContext` 之前(装配点 routes/tasks.ts 就是这个顺序),而委派/长命 bg 子代经 core 继承链拿到它
|
|
634
|
+
* 之后,可能在**宿主腿早已退场**、甚至任何审批作用域之外的上下文里把 ask 发出来。治理来源标因此
|
|
635
|
+
* **不读环境**:`askBroadcast` 按 `identity`(经 `origin`/`primary`)显式算出与写侧同一把 broker key 去查
|
|
636
|
+
* ——「出处决定判定,投递到哪条连接不参与」,与 {@link AskOriginIdentity} 顶注同源。两条腿的钉见
|
|
637
|
+
* test/governance-forced-signal.test.ts(「长命 bg 子代形」与「脱钩定时器形」,改形前后者是红的)。 */
|
|
611
638
|
boundAsk = (identity) => {
|
|
612
639
|
return (req, signal) => {
|
|
613
640
|
const set = this.streams.get(this.streamKey(identity.owner, identity.sessionId, identity.taskId));
|
|
@@ -694,6 +721,11 @@ export class ToolApprovalCoordinator {
|
|
|
694
721
|
}
|
|
695
722
|
: {}),
|
|
696
723
|
message: typeof req.message === "string" ? redactDeep(req.message) : "",
|
|
724
|
+
// [2942]/[2943]:治理来源标(additive,只在为真时在场——缺席绝不编 false,见字段顶注)。
|
|
725
|
+
// 读的是**非消费式** peek:同一 toolCallId 的重播/failover 再入必须拿到同一份判定。
|
|
726
|
+
...((this.governanceAskMarks ?? governanceAskMarksFor(this.streamKey(primary.owner, primary.sessionId, origin.taskId))).isMarked(req.toolCallId)
|
|
727
|
+
? { governanceForced: true }
|
|
728
|
+
: {}),
|
|
697
729
|
...(bounded.omitted ? { argsOmitted: true } : { args: bounded.args }),
|
|
698
730
|
};
|
|
699
731
|
// #151 车2(design/172 §3.3 D3 窗长三元):有效窗——legDeadlineMonotonic 缺席时退化成 ttlMs(D1 零
|
|
@@ -732,7 +764,8 @@ export class ToolApprovalCoordinator {
|
|
|
732
764
|
// R4-1 的三件套(见下方 ensureAsk 调用点注):迟到落盘的行 id / 本地是否已放弃 / 放弃时的收尾动作。
|
|
733
765
|
let lateEnsuredAskId;
|
|
734
766
|
let abandonedEnsure = false;
|
|
735
|
-
/** 本次 `ensureAsk` 传给店里的 `createdAtMs`
|
|
767
|
+
/** 本次 `ensureAsk` 传给店里的 `createdAtMs`(纯时间戳;归属判别已改由店给的 `inserted` 位回答,
|
|
768
|
+
* 见 `EnsureAskResult` 顶注与下方 onLateSuccess)。 */
|
|
736
769
|
const attemptCreatedAtMs = Date.now();
|
|
737
770
|
const voidAbandonedRow = (orphanAskId) => {
|
|
738
771
|
const store = this.askStore;
|
|
@@ -788,15 +821,26 @@ export class ToolApprovalCoordinator {
|
|
|
788
821
|
expiresAtMs,
|
|
789
822
|
createdAtMs: attemptCreatedAtMs,
|
|
790
823
|
}), "ensureAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
|
|
791
|
-
// 🔴 codex 交叉复审 round2 R2-2(2026-08-06 真 finding)
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
// 随后干净地输,人的批准被吞成拒绝/延迟收敛。
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
// (
|
|
799
|
-
|
|
824
|
+
// 🔴 codex 交叉复审 round2 R2-2(2026-08-06 真 finding)+ #168 件2 根治:`ensureAsk` 是**幂等
|
|
825
|
+
// upsert**,它迟到返回的那一行可能根本不是本次插的 —— 而是一条**早已存在、且正被另一次调用
|
|
826
|
+
// 持有**的行(askId 是确定性派生,两次并发调用天然指向同一行)。把任何 STREAM_PENDING 结果都
|
|
827
|
+
// 当成「本次留下的孤儿」去收 VOID,会在本次超时放弃时把**别人正在等的那张活卡**作废掉:真属主
|
|
828
|
+
// 的 decideAsk 随后干净地输,人的批准被吞成拒绝/延迟收敛。
|
|
829
|
+
// 归属判据 = 店给的 `inserted` 位(引擎报的 affected 行数,`EnsureAskResult` 顶注)。R2-2 首修
|
|
830
|
+
// 用的 `createdAtMs === attemptCreatedAtMs` 是**猜**,在同一毫秒的两次并发插入上会给假阳性
|
|
831
|
+
// (那条残余随本件销账);`inserted` 是引擎对同一个问题的权威回答,严格窄于旧判据。
|
|
832
|
+
// `state` 复核仍留:行是本次插的,不代表此刻还没人接手(别的副本可能已经在驱动它)。
|
|
833
|
+
//
|
|
834
|
+
// 🔴 **残余,登记而不假称已解**(codex 交叉复审 2026-08-07 [high] 二,验真;**先存**,本批只把它
|
|
835
|
+
// 收窄没有扩大):`inserted` 回答的是「谁插的」,不是「此刻谁在驱动」。极窄的一支仍会伤人 ——
|
|
836
|
+
// 本次插入**已提交**、提交后那次回读慢到超时、随后**每一次**复核读也全失败(店严重退化,才会走
|
|
837
|
+
// `indeterminate` 臂),而这期间另一个副本幂等命中这条 STREAM_PENDING 行并开始真驱动它;此时
|
|
838
|
+
// 迟到结果带着 `inserted: true` 回来,会把别人正在等的卡收成 VOID。
|
|
839
|
+
// 旧的 `createdAtMs` 判据在同一支上同样会 VOID(那是本次自己写的时间戳)⇒ 本批不是引入方,是
|
|
840
|
+
// 严格缩小了误伤集合(幂等命中的行从此一个字都不碰)。
|
|
841
|
+
// 这条残余与 R4-1 的取舍是同一枚硬币:不收 = 留一条无属主的行被孤儿腿 park 出**幻影 gate**。
|
|
842
|
+
// 真解 = 给行一个 attempt 级的属主租约(新列 + CAS 带租约),属协议改动,开题上报,不在本批。
|
|
843
|
+
lateEnsuredAskId = late.inserted && late.row.state === "STREAM_PENDING" ? late.row.askId : undefined;
|
|
800
844
|
if (abandonedEnsure && lateEnsuredAskId !== undefined)
|
|
801
845
|
voidAbandonedRow(lateEnsuredAskId);
|
|
802
846
|
});
|
|
@@ -808,9 +852,9 @@ export class ToolApprovalCoordinator {
|
|
|
808
852
|
// 是**过去**某次早已返回的 askBroadcast 调用,进程里已经没有任何存活的闭包会再驱动它 settle,
|
|
809
853
|
// TTL 到点时 expireAsk 对一条非 STREAM_PENDING 行同样只会干净地输,一样什么都不做——挂死到进程
|
|
810
854
|
// 重启。修法:行不是 STREAM_PENDING ⇒ 不重新挂卡,直接回放既有终局(不落 pending、不 emit)。
|
|
811
|
-
if (existingOrCreated.state !== "STREAM_PENDING") {
|
|
855
|
+
if (existingOrCreated.row.state !== "STREAM_PENDING") {
|
|
812
856
|
releaseAdmission(); // X-2:回放既有终局 = 这只 ask 从未占用一条未决腿,名额立刻还回去
|
|
813
|
-
return replayTerminalAskRow(existingOrCreated);
|
|
857
|
+
return replayTerminalAskRow(existingOrCreated.row);
|
|
814
858
|
}
|
|
815
859
|
// 🔴 F2 修:行 = 真源。刚插的行上这三样与本地一致(采信是恒等操作);**重入**拿到既有行时,
|
|
816
860
|
// 采信才是唯一正确的做法 —— 帧、定时器、落库三处从此只有一个 deadline、一个 approvalId、一份卡。
|
|
@@ -820,17 +864,30 @@ export class ToolApprovalCoordinator {
|
|
|
820
864
|
// 跳过),而车4 的回决端点按 askId 仍决得动它:人有可能照着一份**不是行上那份**的卡做授权。
|
|
821
865
|
// 采信行上一个**无界/非有限**的 deadline 同样危险:它会把 pending 条目与准入名额钉在那里远超配置窗。
|
|
822
866
|
// 裁:两样任一不合 ⇒ 不编造替身、不发帧,**fail-safe 走 park**(行本身交给对账收敛器处理)。
|
|
823
|
-
const persistedEnvelope = ApprovalCardEnvelopeSchema.safeParse(existingOrCreated.cardJson);
|
|
824
|
-
const sanePersistedDeadline = Number.isFinite(existingOrCreated.expiresAtMs) && existingOrCreated.expiresAtMs <= Date.now() + effectiveWindowMs + DURABLE_CALL_TIMEOUT_MS;
|
|
867
|
+
const persistedEnvelope = ApprovalCardEnvelopeSchema.safeParse(existingOrCreated.row.cardJson);
|
|
868
|
+
const sanePersistedDeadline = Number.isFinite(existingOrCreated.row.expiresAtMs) && existingOrCreated.row.expiresAtMs <= Date.now() + effectiveWindowMs + DURABLE_CALL_TIMEOUT_MS;
|
|
825
869
|
if (!persistedEnvelope.success || !sanePersistedDeadline) {
|
|
826
|
-
|
|
870
|
+
// #168 件2:`inserted` 让这条臂能分清两种成因 —— 幂等命中的**别人的**行不合形(存量/旧版/坏行),
|
|
871
|
+
// 与「本次刚写下的行自己读不回来」(店在改写我们的字节,或本地铸形与 schema 不一致)。处置相同
|
|
872
|
+
// (都不编造替身、都走 park),但后者是本进程自己的账,归因不许并成前者。
|
|
873
|
+
this.noteStoreError(new Error(`persisted approval row is unusable (envelopeOk=${persistedEnvelope.success}, deadlineOk=${sanePersistedDeadline}, ` +
|
|
874
|
+
`origin=${existingOrCreated.inserted ? "row-this-call-just-inserted" : "row-hit-idempotently"})`), "ensureAsk(persisted-row-unusable)");
|
|
827
875
|
releaseAdmission();
|
|
828
876
|
return "unavailable"; // park 路由;绝不用一个编造的身份把这张卡推上 wire
|
|
829
877
|
}
|
|
830
|
-
persistedExpiresAtMs = existingOrCreated.expiresAtMs;
|
|
878
|
+
persistedExpiresAtMs = existingOrCreated.row.expiresAtMs;
|
|
831
879
|
wireApprovalId = persistedEnvelope.data.approvalId;
|
|
832
880
|
frame.approvalId = wireApprovalId; // 旧帧的去重键必须与新帧同值(两帧同带 approvalId 是消费端的对账口)
|
|
833
881
|
cardForFrame = persistedEnvelope.data.card;
|
|
882
|
+
// 🔴 [2942] codex 交叉复审 round1 [high](验真):`governanceForced` 也归入「行 = 真源」的 F2 家族。
|
|
883
|
+
// 本地判据(`governanceAskMarks`)是**进程内**表:它不跨副本、不跨重启。幂等命中一条既有行时
|
|
884
|
+
// (failover / 重启后重入),本地表恒空而行上的卡里存着它**落库那一刻**的判定 —— 两族帧若各读各的,
|
|
885
|
+
// 同一个 `approvalId` 会带着**互相矛盾**的出处上 wire(旧帧无键、呈卡帧 true,反向亦然)。
|
|
886
|
+
// 与 approvalId/deadline 同一条裁定:采信行。缺席也要**如实同步**(删键,不是留着本地的旧真值)。
|
|
887
|
+
if (cardForFrame.governanceForced === true)
|
|
888
|
+
frame.governanceForced = true;
|
|
889
|
+
else
|
|
890
|
+
delete frame.governanceForced;
|
|
834
891
|
}
|
|
835
892
|
catch (err) {
|
|
836
893
|
this.noteStoreError(err, "ensureAsk");
|
|
@@ -15,21 +15,35 @@
|
|
|
15
15
|
* 维护:core 新键到来时,transparently 透传的加进对应 PROJECTED 并去实现投影;有意不上 wire 的加进
|
|
16
16
|
* EXCLUDED 并写一行理由。谁改投影(project.ts / fleet-bus.ts / roster-store-sql.ts)谁同步本文件。
|
|
17
17
|
*/
|
|
18
|
-
import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, TaskEvent } from "@sema-agent/core";
|
|
18
|
+
import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, TaskEvent, MailboxMessage, MailboxStore } from "@sema-agent/core";
|
|
19
19
|
/** 编译期断言:T 必须收敛到 never(有残余键 = tsc 红)。 */
|
|
20
20
|
type AssertAllKeysHandled<T extends never> = T;
|
|
21
21
|
type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId" | "error" | "errorCode";
|
|
22
|
-
type
|
|
22
|
+
type NotificationExcluded = "peer";
|
|
23
|
+
type _GuardNotification = AssertAllKeysHandled<Exclude<keyof TaskNotificationPayload, NotificationProjected | NotificationExcluded>>;
|
|
23
24
|
type BgNotifProjected = "taskId" | "sessionId" | "seq" | "status" | "summary" | "stoppedBy" | "resumable" | "recentSteps" | "editedFiles" | "usage" | "transcriptId" | "rootSessionId" | "parentTaskId" | "parentToolCallId" | "completionId";
|
|
24
25
|
type BgNotifExcluded = "kind" | "sessionScoped" | "owner" | "scope" | "description" | "agentType" | "name" | "currentAction" | "currentTool" | "parentSessionId" | "startedAt" | "workflowRunId" | "progressTaskId" | "progressParentTaskId";
|
|
25
26
|
type _GuardBgNotif = AssertAllKeysHandled<Exclude<keyof BackgroundChildEvent, BgNotifProjected | BgNotifExcluded>>;
|
|
26
27
|
type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
|
|
27
28
|
type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
|
|
28
29
|
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation";
|
|
29
|
-
type AskExcluded = "preview" | "principal" | "requiresRealApproval" | "riskAxes" | "boundInputHash";
|
|
30
|
+
type AskExcluded = "preview" | "principal" | "requiresRealApproval" | "riskAxes" | "boundInputHash" | "ruleSuggestions";
|
|
30
31
|
type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
|
|
31
32
|
type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest";
|
|
32
33
|
type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
|
|
33
|
-
|
|
34
|
+
type MailboxProjected = "seq" | "from" | "content" | "sentAt" | "hopChain";
|
|
35
|
+
type _GuardMailbox = AssertAllKeysHandled<Exclude<keyof MailboxMessage, MailboxProjected>>;
|
|
36
|
+
type MailboxAppendMessage = Parameters<MailboxStore["append"]>[2];
|
|
37
|
+
type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain";
|
|
38
|
+
type _GuardMailboxAppend = AssertAllKeysHandled<Exclude<keyof MailboxAppendMessage, MailboxAppendProjected>>;
|
|
39
|
+
export type CoreKeysetGuards = [
|
|
40
|
+
_GuardNotification,
|
|
41
|
+
_GuardBgNotif,
|
|
42
|
+
_GuardRoster,
|
|
43
|
+
_GuardAsk,
|
|
44
|
+
_GuardTaskEvent,
|
|
45
|
+
_GuardMailbox,
|
|
46
|
+
_GuardMailboxAppend
|
|
47
|
+
];
|
|
34
48
|
export {};
|
|
35
49
|
//# sourceMappingURL=core-keyset-guard.d.ts.map
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* old session-join `toolOutput` hook is gone, there's a real source now); `eventId`/`parentToolCallId` (§E2) ride
|
|
13
13
|
* each content event, additive / tolerate-absent.
|
|
14
14
|
*/
|
|
15
|
-
import type { TaskResult } from "@sema-agent/core";
|
|
15
|
+
import type { ApprovalSettledBy, TaskResult } from "@sema-agent/core";
|
|
16
16
|
import type { RunEvent, RunRecord } from "../plugins/store-contracts.js";
|
|
17
17
|
import type { ModelUsageDelta, ModelUsageTracker, PromptManifestRecord } from "../budget.js";
|
|
18
18
|
type AppendFn = (type: string, data: unknown) => Promise<void>;
|
|
@@ -90,6 +90,7 @@ export type TraceBlock = {
|
|
|
90
90
|
totalChars?: number;
|
|
91
91
|
structured?: unknown;
|
|
92
92
|
errorCode?: string;
|
|
93
|
+
settledBy?: ApprovalSettledBy;
|
|
93
94
|
eventId?: string;
|
|
94
95
|
parentToolCallId?: string;
|
|
95
96
|
} | {
|
|
@@ -131,6 +132,7 @@ export declare function toolEndEventData(ev: {
|
|
|
131
132
|
totalChars?: number;
|
|
132
133
|
structured?: unknown;
|
|
133
134
|
errorCode?: string;
|
|
135
|
+
settledBy?: ApprovalSettledBy;
|
|
134
136
|
eventId?: string;
|
|
135
137
|
parentToolCallId?: string;
|
|
136
138
|
}): Record<string, unknown>;
|
|
@@ -284,6 +286,13 @@ export declare function workspaceChangedEventData(ev: {
|
|
|
284
286
|
* · `interaction.posture`(三值:interactive / headless / absent)—— **裁:租户可见**。它是部署形的
|
|
285
287
|
* 自述(这条腿有没有人能答问题),消费端据此决定渲不渲交互面;不是治理秘密,也不构成任何治理位的
|
|
286
288
|
* 侧信道(它不参与 governance 那 4 个布尔的还原)。
|
|
289
|
+
* · `permissionRules.storeWired`(布尔;core 5.18.0 design/179 新段)—— **裁:租户可见**。同族判据:
|
|
290
|
+
* 它与 `parkLane.capable` / `elicit.seamWired` / `fleet.backgroundAgentStore` 是同一类「这条腿接没接
|
|
291
|
+
* 某个可选设施」的部署形自述,而那三个早已租户可见。消费端的真需求也在租户侧:审批卡要不要渲染
|
|
292
|
+
* 「不再询问」这一格,取决于这台 worker 有没有规则店 —— 剥掉它,客户端只能靠试(提交后被拒)才知道。
|
|
293
|
+
* 它**不在** `governance` 段里(core 自己把它放在段外),也不构成那 4 个治理布尔的侧信道。
|
|
294
|
+
* ⚠️ 恒在段(core 侧 `permissionRules` 无 `?`):本仓未接规则店 ⇒ `storeWired: false`,这是**真值**
|
|
295
|
+
* 不是缺席 —— 消费端读到 false 就是「这台 worker 没有这条车道」,不许当成「这版引擎还没这个概念」。
|
|
287
296
|
* 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
|
|
288
297
|
* (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来。
|
|
289
298
|
*/
|
package/dist/trace/project.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// 值导入(非 `import type`):`settledBy` 的闭集判据必须**复用 core 的谓词**——本仓抄一份三元词表
|
|
2
|
+
// 就是值拷贝漂移源(上游加员时两处静默不同步)。core 5.18.1 #187 导出的单一属主。
|
|
3
|
+
import { isApprovalSettledBy } from "@sema-agent/core";
|
|
1
4
|
import { redactSecrets, redactDeep } from "./redact.js";
|
|
2
5
|
/** E8 (shell-host): drain the per-task model-usage accumulator into an append-only `model_usage` DELTA event.
|
|
3
6
|
* Shared by BOTH durable-write legs (runInBackground + the resume leg) so the persisted shape never drifts; the
|
|
@@ -163,6 +166,16 @@ export function toolEndEventData(ev) {
|
|
|
163
166
|
// 带 default 臂,禁按两值穷举。verbatim 依据:码是机器短串标识符、非用户内容(不脱敏),边界靠
|
|
164
167
|
// 显式上限而非词表 —— >128 字符=非法形,整键丢弃(不截断:截断会铸出 core 从未发过的码)。
|
|
165
168
|
...(typeof ev.errorCode === "string" && ev.errorCode.length <= 128 ? { errorCode: ev.errorCode } : {}),
|
|
169
|
+
// core 5.18.1 #187([3110] 提货批):`settledBy` = 这一次调用的**结算来源**("human"/"timeout"/
|
|
170
|
+
// "aborted"),只挂在经审批结算的那个 tool_end 上。与上面的 errorCode **不同族**,两条差别都承重:
|
|
171
|
+
// ① 它是**闭集**,判据走 core 导出的 `isApprovalSettledBy`(单一属主)—— 不在本仓抄词表,也不拿
|
|
172
|
+
// 长度上限当边界(那是开集的兜底姿势,对闭集等于没门:出格词照样上 wire,下游按闭集写的
|
|
173
|
+
// switch 会撞 default 或更坏地把它当成一次真结算)。上游加员时判据自动跟随。
|
|
174
|
+
// ② **缺席不是语义**:core 契约逐字写「Absence = an older caller — consumers must not infer
|
|
175
|
+
// semantics from a missing key」;姿态臂(headless auto-deny / 整体放行)本就不填。所以缺席
|
|
176
|
+
// 一律不铸键 —— 既不折 "human" 也不折 null,把「不知道」如实表达成「没有这个键」。
|
|
177
|
+
// 值是引擎铸的枚举短串、非用户内容 ⇒ 不脱敏。
|
|
178
|
+
...(isApprovalSettledBy(ev.settledBy) ? { settledBy: ev.settledBy } : {}),
|
|
166
179
|
...identityFields(ev),
|
|
167
180
|
};
|
|
168
181
|
}
|
|
@@ -363,6 +376,13 @@ export function workspaceChangedEventData(ev) {
|
|
|
363
376
|
* · `interaction.posture`(三值:interactive / headless / absent)—— **裁:租户可见**。它是部署形的
|
|
364
377
|
* 自述(这条腿有没有人能答问题),消费端据此决定渲不渲交互面;不是治理秘密,也不构成任何治理位的
|
|
365
378
|
* 侧信道(它不参与 governance 那 4 个布尔的还原)。
|
|
379
|
+
* · `permissionRules.storeWired`(布尔;core 5.18.0 design/179 新段)—— **裁:租户可见**。同族判据:
|
|
380
|
+
* 它与 `parkLane.capable` / `elicit.seamWired` / `fleet.backgroundAgentStore` 是同一类「这条腿接没接
|
|
381
|
+
* 某个可选设施」的部署形自述,而那三个早已租户可见。消费端的真需求也在租户侧:审批卡要不要渲染
|
|
382
|
+
* 「不再询问」这一格,取决于这台 worker 有没有规则店 —— 剥掉它,客户端只能靠试(提交后被拒)才知道。
|
|
383
|
+
* 它**不在** `governance` 段里(core 自己把它放在段外),也不构成那 4 个治理布尔的侧信道。
|
|
384
|
+
* ⚠️ 恒在段(core 侧 `permissionRules` 无 `?`):本仓未接规则店 ⇒ `storeWired: false`,这是**真值**
|
|
385
|
+
* 不是缺席 —— 消费端读到 false 就是「这台 worker 没有这条车道」,不许当成「这版引擎还没这个概念」。
|
|
366
386
|
* 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
|
|
367
387
|
* (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来。
|
|
368
388
|
*/
|
|
@@ -386,6 +406,7 @@ export function wiringManifestEventData(ev) {
|
|
|
386
406
|
const fleetIn = sec(m.fleet) ?? {};
|
|
387
407
|
const legIn = sec(m.leg) ?? {};
|
|
388
408
|
const interactionIn = sec(m.interaction) ?? {};
|
|
409
|
+
const rulesIn = sec(m.permissionRules) ?? {};
|
|
389
410
|
const ask = nonEmpty({
|
|
390
411
|
...(str(askIn.form) !== undefined ? { form: askIn.form } : {}),
|
|
391
412
|
...(str(askIn.provenance) !== undefined ? { provenance: askIn.provenance } : {}),
|
|
@@ -413,6 +434,11 @@ export function wiringManifestEventData(ev) {
|
|
|
413
434
|
...(bool(fleetIn.backgroundAgentStore) !== undefined ? { backgroundAgentStore: fleetIn.backgroundAgentStore } : {}),
|
|
414
435
|
...(bool(fleetIn.hostChildEventSink) !== undefined ? { hostChildEventSink: fleetIn.hostChildEventSink } : {}),
|
|
415
436
|
});
|
|
437
|
+
// core 5.18.0 design/179 段(裁定见头注:租户可见)。逐键挑,与其余段同规:非布尔按缺席处理
|
|
438
|
+
// (不铸假值——一个编出来的 `false` 会被读成「确实没接规则店」,而真相是这段读不出来)。
|
|
439
|
+
const permissionRules = nonEmpty({
|
|
440
|
+
...(bool(rulesIn.storeWired) !== undefined ? { storeWired: rulesIn.storeWired } : {}),
|
|
441
|
+
});
|
|
416
442
|
return {
|
|
417
443
|
...(num(m.schemaVersion) !== undefined ? { schemaVersion: m.schemaVersion } : {}),
|
|
418
444
|
...(str(legIn.kind) !== undefined ? { leg: { kind: legIn.kind } } : {}),
|
|
@@ -423,6 +449,7 @@ export function wiringManifestEventData(ev) {
|
|
|
423
449
|
...(parkLane !== undefined ? { parkLane } : {}),
|
|
424
450
|
...(str(sessionIn.store) !== undefined ? { session: { store: sessionIn.store } } : {}),
|
|
425
451
|
...(fleet !== undefined ? { fleet } : {}),
|
|
452
|
+
...(permissionRules !== undefined ? { permissionRules } : {}),
|
|
426
453
|
// governance —— **刻意不在此**(见头注:受众标 operator,无投影不披露)。
|
|
427
454
|
// configFingerprint —— **也刻意不在此**(codex 复审 2026-08-06 F1,验真后采纳)。剥掉一个段却把
|
|
428
455
|
// **对它取的哈希**发出去,等于没剥:core 的算法是
|
|
@@ -685,6 +712,10 @@ export function toolResultFieldsOf(d) {
|
|
|
685
712
|
// core 5.9.0 W3([2535]):机器可判别的 park/死投影码(`gate.parked`/`tool.not_found`)——写侧
|
|
686
713
|
// toolEndEventData 已带,读腿必须同键 verbatim(absent⇒absent),否则冷回放退回锚 abort 文案字面。
|
|
687
714
|
...(typeof d.errorCode === "string" ? { errorCode: d.errorCode } : {}),
|
|
715
|
+
// core 5.18.1 #187:结算来源(闭集)——写侧 toolEndEventData 已带,读腿同键 verbatim(absent⇒absent)。
|
|
716
|
+
// 🔴 读腿的闭集门是**独立的一道**,不是写侧那道的复述:账本行可以来自任何引擎世代、任何第三方
|
|
717
|
+
// producer,写侧的门管不到它们;这里放行 = 把一个 core 从未发过的词直接送上 wire。
|
|
718
|
+
...(isApprovalSettledBy(d.settledBy) ? { settledBy: d.settledBy } : {}),
|
|
688
719
|
...identityFields(d),
|
|
689
720
|
};
|
|
690
721
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.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": "^5.
|
|
57
|
+
"@sema-agent/core": "^5.19.0",
|
|
58
58
|
"@sema-agent/registry-core": "^0.16.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": "^6.
|
|
72
|
+
"@sema-agent/sdk": "^6.9.0",
|
|
73
73
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
74
74
|
"@types/node": "22.10.2",
|
|
75
75
|
"@types/pg": "^8.20.0",
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { FileError, StubExecutionEnv, type Result } from "@sema-agent/core";
|
|
2
|
-
/**
|
|
3
|
-
* 无文件系统的路径裁决 env(见文件头)。只覆写 `absolutePath` 一面,其余全部继承 `StubExecutionEnv`
|
|
4
|
-
* 的 `not_supported` ——**继承而非逐一手写**是刻意的:core 日后给 `ExecutionEnv` 加必填面时,新面会
|
|
5
|
-
* 随 `StubExecutionEnv` 一起到位并保持同一个诚实答案,不会在这里留下一个悄悄编出来的假答案。
|
|
6
|
-
*/
|
|
7
|
-
export declare class LexicalPathExecutionEnv extends StubExecutionEnv {
|
|
8
|
-
absolutePath(path: string): Promise<Result<string, FileError>>;
|
|
9
|
-
}
|
|
10
|
-
//# sourceMappingURL=lexical-path-env.d.ts.map
|