@sema-agent/server 7.4.0 → 7.5.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/USAGE.md +43 -0
- package/dist/approval-card.d.ts +15 -3
- package/dist/approval-card.js +41 -7
- package/dist/approval-reconciler.d.ts +108 -11
- package/dist/approval-reconciler.js +146 -19
- package/dist/boot/coordinators.js +10 -2
- package/dist/boot/org-memory.d.ts +6 -0
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/reapers.d.ts +2 -0
- package/dist/boot/reapers.js +11 -4
- package/dist/boot/resolve-spec.d.ts +3 -2
- package/dist/boot/resolve-spec.js +132 -32
- 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/config-center/apply-effective.js +33 -10
- package/dist/config-types.d.ts +24 -8
- package/dist/config.d.ts +6 -1
- package/dist/config.js +56 -11
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +3 -1
- package/dist/fleet/fleet-bus.d.ts +6 -1
- package/dist/fleet/fleet-bus.js +25 -3
- 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/diagnostics.js +10 -5
- 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 +1 -1
- package/dist/http/routes/tasks.js +59 -22
- package/dist/http/server.d.ts +5 -0
- package/dist/http/server.js +23 -12
- package/dist/http/wire-types.d.ts +7 -2
- package/dist/main.js +17 -5
- package/dist/observability/fail-open.d.ts +13 -2
- package/dist/observability/fail-open.js +15 -4
- package/dist/observability/prompt-manifest.d.ts +5 -1
- package/dist/orchestration/workflow-notify-journal.d.ts +57 -1
- package/dist/orchestration/workflow-notify-journal.js +137 -32
- 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 +27 -5
- package/dist/plugins/approval-ask-store-sql.js +9 -2
- package/dist/plugins/background-shell-support.d.ts +1 -1
- package/dist/plugins/background-shell-support.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +62 -6
- package/dist/plugins/checkpoint-store-sql.js +71 -11
- 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 +57 -4
- package/dist/runs.d.ts +8 -0
- package/dist/runs.js +15 -2
- package/dist/runtime-governance.d.ts +18 -0
- package/dist/runtime-governance.js +90 -3
- 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 +17 -3
- 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.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,11 +15,12 @@
|
|
|
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>>;
|
|
@@ -30,6 +31,19 @@ type AskExcluded = "preview" | "principal" | "requiresRealApproval" | "riskAxes"
|
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.5.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.17.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.8.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
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #156 —— 非 host lane(e2b/k8s/ssh/adb/local-docker)写门的**纯路径规则**过渡形所用的裁决 env。
|
|
3
|
-
*
|
|
4
|
-
* ## 它解决的缺口
|
|
5
|
-
*
|
|
6
|
-
* core 的 `createFsWriteGatePolicy` / `createSensitivePathPolicy` 都把写目标交给
|
|
7
|
-
* `canonicalizeTarget(env, …)` 去「问 fs 要真身」。host lane 上那个 env 就是 hand 工具真正写的那块盘,
|
|
8
|
-
* 答案可信。沙箱 lane 不是:per-task 的沙箱 env 由 core 的 `executionEnvFactory` 在 spec **之后**才铸,
|
|
9
|
-
* spec 期手边只有 worker 本机的 fs——拿它去裁沙箱里的路径会答错(exists/symlink 全是别人机器上的事实),
|
|
10
|
-
* 而**错误的 allow 比没有门更糟**。所以 [816]/[820] 当年在沙箱 lane 诚实不挂门(wiring=undefined ⇒
|
|
11
|
-
* 三个模式臂全回落 base 规则,写门整条缺席)。#153 的 ③ 号注把这笔记成了余款,#156 是它的过渡还款。
|
|
12
|
-
*
|
|
13
|
-
* ## 这个 env 是什么
|
|
14
|
-
*
|
|
15
|
-
* 一个**没有文件系统**的 env:除 `absolutePath` 外的每一个 fs 面(exists / canonicalPath / fileInfo /
|
|
16
|
-
* read* / write* / listDir / createDir / …)与 shell 面都继承 core 的 `StubExecutionEnv`,
|
|
17
|
-
* 恒返回 `not_supported` 错误(`readLink` 是可选面,Stub 干脆没有——core 把缺席当「symlink 不可解」,
|
|
18
|
-
* 且该分支在本 env 恒不可达:exists 先错就进了 canonicalizeNewPath)——这不是伪装,这就是本 env 的诚实回答:**spec 期我们确实不知道沙箱里
|
|
19
|
-
* 那块盘上有什么**。`absolutePath` 是唯一被赋予真实语义的一面,而它本来就是纯路径运算(不碰盘):
|
|
20
|
-
* POSIX 绝对路径做词法归一后原样奉还,其余一律「不知道」。
|
|
21
|
-
*
|
|
22
|
-
* ## 由此得到的裁决语义(core dist 亲读推出,`tools/fs/safety.js` canonicalizeTarget)
|
|
23
|
-
*
|
|
24
|
-
* · 绝对路径(`/…`):`absolutePath` ok ⇒ core 进 exists 探测 ⇒ 我们返回错误 ⇒ core 按「该路径尚不存在」
|
|
25
|
-
* 走 `canonicalizeNewPath`,那里逐级 exists 也全错 ⇒ 原样返回我们给的词法绝对路径。于是 canonical key
|
|
26
|
-
* = **词法归一后的路径**,`canonicalPath` / `fileInfo` / `readLink` 三面在这条路上根本不可达。
|
|
27
|
-
* · 相对路径 / `~/…` / Windows 盘符形:`absolutePath` 返回错误 ⇒ `canon.ok=false` ⇒ 写门直接
|
|
28
|
-
* `ask`(fail-closed)。沙箱的 cwd 在 spec 期不可知,这正是我们要的偏置——**不猜沙箱工作目录**。
|
|
29
|
-
* · 反斜杠 UNC 形(`\\host\share\…`)是唯一**不经过**本 env 的形:core 的 `canonicalizeTarget` 在入口
|
|
30
|
-
* 就把它短路成 `ok:true, key=原样`(复审 2026-08-05 dist 亲读+实测)。结果不变——写门无放行域可越,
|
|
31
|
-
* 落 defaultWrite 的 `ask`;sensitive 段匹配按 `[\\/]` 双分隔符切段,`\\host\share\.env` 照样 deny。
|
|
32
|
-
* · sensitivePatterns 的 deny 腿照常施加:它按 canonical key 的**路径段**做 glob 匹配,词法 key 足够,
|
|
33
|
-
* 且在 `combinePolicies` 折叠里 deny 恒胜(session 豁免 / accept 域越不过)。
|
|
34
|
-
* · `isExempt`(会话「本会话不再询问」探针)是 name-keyed 的,不做 fs 裁决,任何 lane 都安全,照接。
|
|
35
|
-
*
|
|
36
|
-
* ## 已知残余面(过渡形的边界,不是疏漏)
|
|
37
|
-
*
|
|
38
|
-
* · **symlink 形**:词法裁决看不见符号链接。沙箱里一个名字普通的软链可以指向守卫段(`.ssh` 等),
|
|
39
|
-
* 我们只会给 `ask` 而不是 `deny`;host lane 上 core 会 canonicalize 出真身并 deny。相应地,core 的
|
|
40
|
-
* `unresolvedSymlink ⇒ deny` 那条腿在本 env 下永不触发。注意豁免会话下的口径(codex 复审 E):
|
|
41
|
-
* name-keyed 豁免不看路径,这类词法无害的软链目标在豁免会话里是 **allow** 而非 ask——这是本残余面
|
|
42
|
-
* 在「操作员已授 don't-ask-again」情形下的完整代价,host lane 同情形仍会 deny。
|
|
43
|
-
* · **exempt/accept 域**:沙箱 lane 既无可信 cwd 也无 scratchpad 对应物,故本过渡形**一个自动放行域都
|
|
44
|
-
* 不铸**(见 resolve-spec 的 wiring)。代价是 acceptEdits 在沙箱 lane 退化成与 default 同形(全 ask);
|
|
45
|
-
* 这是 fail-safe 方向,与 host lane「accept 域解析不出 ⇒ 该域不生效」的哨兵先例同口径。
|
|
46
|
-
* · **`cwd` 字段**:继承 `StubExecutionEnv` 的 `"/"` 占位。本 env 只活在 policy 折叠里,core 的两个
|
|
47
|
-
* policy 都不读 `env.cwd`(相对路径基准走 `rootPath` 形参,而我们不给)——它不代表沙箱的工作目录。
|
|
48
|
-
*
|
|
49
|
-
* ## 终局 seam
|
|
50
|
-
*
|
|
51
|
-
* 正解是让写门在**工具执行时刻**拿到真沙箱 env:core [2751] 已排期给 `HookToolContext` 加一个只读的
|
|
52
|
-
* env 能力窄面(174/#22 窗)。那一面到货后,本过渡形整体退役换正解——届时沙箱 lane 与 host lane 走同
|
|
53
|
-
* 一条真 fs 裁决,上面三条残余面一并消失。
|
|
54
|
-
*/
|
|
55
|
-
import { posix } from "node:path";
|
|
56
|
-
import { FileError, StubExecutionEnv, err, ok } from "@sema-agent/core";
|
|
57
|
-
/** 词法归一:纯字符串运算,折 `.` / `..` / 重复分隔符,不碰 fs。
|
|
58
|
-
* `posix.normalize` 把 `..` 在根部截断(`/../x` → `/x`),与「沙箱根之上没有东西」的语义一致。
|
|
59
|
-
* 尾部分隔符统一剥掉(根 `/` 除外),让同一目标只有一个 key ——前缀判域靠的就是 key 的唯一性。
|
|
60
|
-
* (core 内部有同形的 `normalizeAbsPathLexically`,但未从包根导出;此处是 Node 标准库的等价运算,
|
|
61
|
-
* 不是它的抄本——若日后 core 导出,这里应改为直接复用。) */
|
|
62
|
-
function normalizeAbsolutePathLexically(path) {
|
|
63
|
-
const collapsed = posix.normalize(path.replace(/^\/+/, "/"));
|
|
64
|
-
return collapsed.length > 1 ? collapsed.replace(/\/+$/, "") : collapsed;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* 无文件系统的路径裁决 env(见文件头)。只覆写 `absolutePath` 一面,其余全部继承 `StubExecutionEnv`
|
|
68
|
-
* 的 `not_supported` ——**继承而非逐一手写**是刻意的:core 日后给 `ExecutionEnv` 加必填面时,新面会
|
|
69
|
-
* 随 `StubExecutionEnv` 一起到位并保持同一个诚实答案,不会在这里留下一个悄悄编出来的假答案。
|
|
70
|
-
*/
|
|
71
|
-
export class LexicalPathExecutionEnv extends StubExecutionEnv {
|
|
72
|
-
absolutePath(path) {
|
|
73
|
-
// POSIX 绝对形是沙箱 lane(全 Linux)上 hand 工具的书面契约形。其余一切——相对路径、`~`、
|
|
74
|
-
// `C:\…`——在 spec 期都无法诚实解析成一个沙箱内的绝对路径,返回错误让 core 落 ask。
|
|
75
|
-
// (反斜杠 UNC 形根本到不了这里:core 在 canonicalizeTarget 入口短路,见文件头「由此得到的裁决语义」。)
|
|
76
|
-
// codex 复审 A(2026-08-05):含 NUL 的字符串不是合法 POSIX 路径(任何 fs 面都写不进去),但词法归一
|
|
77
|
-
// 会照样给它铸出 canonical key——`\0` 尾巴让守卫段 glob 失配,豁免会话下还能拿 allow。拒收让 canon
|
|
78
|
-
// 在豁免咨询**之前**就失败 ⇒ 恒 ask。
|
|
79
|
-
if (path.includes("\u0000")) {
|
|
80
|
-
return Promise.resolve(err(new FileError("not_supported", "this environment adjudicates paths lexically and rejects a path containing a NUL byte (not a representable POSIX path)", path)));
|
|
81
|
-
}
|
|
82
|
-
if (!path.startsWith("/")) {
|
|
83
|
-
return Promise.resolve(err(new FileError("not_supported", "this environment adjudicates paths lexically and cannot resolve a non-absolute path (the sandbox working directory is unknown at spec time)", path)));
|
|
84
|
-
}
|
|
85
|
-
return Promise.resolve(ok(normalizeAbsolutePathLexically(path)));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
//# sourceMappingURL=lexical-path-env.js.map
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { ToolSpec } from "@sema-agent/core";
|
|
2
|
-
/**
|
|
3
|
-
* OA scenario — tools are thin HTTP clients to the OA backend's existing APIs (read = /api/v1/*,
|
|
4
|
-
* write = /api/assets/*). The shared service token + the request's principal are forwarded so OA
|
|
5
|
-
* enforces THAT end-user's RBAC + audit (the service never holds per-user creds). Decoupled: the
|
|
6
|
-
* service knows no OA schema/DB — OA stays the source of truth (its /api/v1/meta is self-describing).
|
|
7
|
-
*/
|
|
8
|
-
export interface OaCtx {
|
|
9
|
-
baseUrl: string;
|
|
10
|
-
token: string;
|
|
11
|
-
principal?: string;
|
|
12
|
-
}
|
|
13
|
-
export declare function oaTools(ctx: OaCtx): ToolSpec[];
|
|
14
|
-
export declare const oaPrompt: import("@sema-agent/core").PromptProvider;
|
|
15
|
-
//# sourceMappingURL=oa-tools.d.ts.map
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { Type } from "typebox";
|
|
2
|
-
import { stablePrompt } from "./prompt.js";
|
|
3
|
-
async function oaFetch(ctx, path, init) {
|
|
4
|
-
if (!ctx.baseUrl || !ctx.token)
|
|
5
|
-
return "OA 后端未配置(OA_API_BASEURL / OA_SERVICE_TOKEN)";
|
|
6
|
-
try {
|
|
7
|
-
const res = await fetch(ctx.baseUrl + path, {
|
|
8
|
-
method: init?.method ?? "GET",
|
|
9
|
-
headers: {
|
|
10
|
-
"content-type": "application/json",
|
|
11
|
-
authorization: `Bearer ${ctx.token}`,
|
|
12
|
-
...(ctx.principal ? { "x-agent-principal": ctx.principal } : {}),
|
|
13
|
-
},
|
|
14
|
-
body: init?.body !== undefined ? JSON.stringify(init.body) : undefined,
|
|
15
|
-
});
|
|
16
|
-
const text = await res.text();
|
|
17
|
-
if (!res.ok)
|
|
18
|
-
return `OA ${res.status}: ${text.slice(0, 600)}`;
|
|
19
|
-
return text.slice(0, 60_000);
|
|
20
|
-
}
|
|
21
|
-
catch (e) {
|
|
22
|
-
return `调用 OA 失败:${e.message}`;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const needWrite = (ctx) => (!ctx.principal ? "缺少身份(principal),无法代表用户写库;请让调用方带上 x-agent-principal。" : null);
|
|
26
|
-
export function oaTools(ctx) {
|
|
27
|
-
const obj = Type.Object({}, { additionalProperties: true });
|
|
28
|
-
return [
|
|
29
|
-
{ name: "oa_schema", effect: "read", description: "取 OA 数据结构(每类资产的字段/必填/条件必填 requiredWhen/联动 showWhen/枚举、生命周期、表名)。建/改前先调它,别凭记忆。", parameters: Type.Object({}), execute: () => oaFetch(ctx, "/api/v1/meta") },
|
|
30
|
-
{ name: "oa_alerts", effect: "read", description: "派生告警:GP 闲置(已注册未变现) / APP 未上架(已接广告未 LIVE)。", parameters: Type.Object({}), execute: () => oaFetch(ctx, "/api/v1/alerts") },
|
|
31
|
-
{ name: "oa_list", effect: "read", description: "列出某类资产。type∈gp/package/ad-platform/ad-unit/email/environment/proxy。可选 q 关键字。", parameters: Type.Object({ type: Type.String(), q: Type.Optional(Type.String()) }), execute: (a) => { const x = a; return oaFetch(ctx, `/api/v1/entities/${encodeURIComponent(x.type)}${x.q ? `?q=${encodeURIComponent(x.q)}` : ""}`); } },
|
|
32
|
-
{ name: "oa_get", effect: "read", description: "取某资产详情(敏感字段为掩码)。", parameters: Type.Object({ type: Type.String(), id: Type.String() }), execute: (a) => { const x = a; return oaFetch(ctx, `/api/v1/entities/${encodeURIComponent(x.type)}/${encodeURIComponent(x.id)}`); } },
|
|
33
|
-
{ name: "oa_lookup", effect: "read", description: "约定字段值反查(同手机/证件=同一实物)。field: phone|idCardNo|bizLicenseNo。", parameters: Type.Object({ field: Type.String(), value: Type.String() }), execute: (a) => { const x = a; return oaFetch(ctx, `/api/v1/lookup?field=${encodeURIComponent(x.field)}&value=${encodeURIComponent(x.value)}`); } },
|
|
34
|
-
{ name: "oa_create", effect: "write", description: "创建实体(走 OA 校验+当前用户 RBAC+审计)。type + data(字段对象,参照 oa_schema)。失败会返回缺失/非法字段。", parameters: Type.Object({ type: Type.String(), data: obj }), execute: (a) => { const e = needWrite(ctx); if (e)
|
|
35
|
-
return e; const x = a; return oaFetch(ctx, `/api/assets/${encodeURIComponent(x.type)}`, { method: "POST", body: x.data ?? {} }); } },
|
|
36
|
-
{ name: "oa_update", effect: "write", description: "改字段(敏感留空=保持原值;状态字段改不动,用 oa_transition)。", parameters: Type.Object({ type: Type.String(), id: Type.String(), data: obj }), execute: (a) => { const e = needWrite(ctx); if (e)
|
|
37
|
-
return e; const x = a; return oaFetch(ctx, `/api/assets/${encodeURIComponent(x.type)}/${encodeURIComponent(x.id)}`, { method: "PATCH", body: x.data ?? {} }); } },
|
|
38
|
-
{ name: "oa_transition", effect: "write", description: "受控状态流转(先 oa_schema 看可用迁移;风险/弃用等需 note)。", parameters: Type.Object({ type: Type.String(), id: Type.String(), to: Type.String(), note: Type.Optional(Type.String()) }), execute: (a) => { const e = needWrite(ctx); if (e)
|
|
39
|
-
return e; const x = a; return oaFetch(ctx, `/api/assets/${encodeURIComponent(x.type)}/${encodeURIComponent(x.id)}/transition`, { method: "POST", body: { to: x.to, note: x.note ?? null } }); } },
|
|
40
|
-
];
|
|
41
|
-
}
|
|
42
|
-
// OA business prompt MOVED OUT — migration complete (design/29, 2026-06-05). Persona / 工作准则 / UI
|
|
43
|
-
// markers (::FORM:: / ::GOTO:: / ::SUGGEST:: / ::ISSUE_FORM::) / 分级确认 / 报bug流程 are OA's business,
|
|
44
|
-
// and OA now injects the WHOLE thing per request via `request.systemPrompt` (it holds the service token,
|
|
45
|
-
// content is stable per OA version → cacheable prefix; page context still rides `objective`). Keeping
|
|
46
|
-
// any of it here would just DUPLICATE OA's prompt, so the base is EMPTY — single source of truth = OA.
|
|
47
|
-
// The service stays business-agnostic: it provides the oa_* tools (referenced by NAME from OA's prompt;
|
|
48
|
-
// they carry their own descriptions) and nothing else. `stablePrompt` keeps STABLE→VARIABLE order —
|
|
49
|
-
// OA's systemPrompt is appended after this (empty) base, before core's <user_memory>.
|
|
50
|
-
// (::ISSUE_FORM:: is parsed/rendered + submitted entirely by OA's frontend via OA /api/ai/issue — the
|
|
51
|
-
// service never touches it; `open_gitea_issue` remains only as the unattended/automated fallback.)
|
|
52
|
-
const OA_SYSTEM = "";
|
|
53
|
-
export const oaPrompt = stablePrompt(OA_SYSTEM);
|
|
54
|
-
//# sourceMappingURL=oa-tools.js.map
|