@sema-agent/server 7.10.0 → 7.11.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/dist/adoption/plan.d.ts +152 -0
- package/dist/adoption/plan.js +513 -0
- package/dist/adoption/runner.d.ts +54 -0
- package/dist/adoption/runner.js +505 -0
- package/dist/adoption/sql.d.ts +76 -0
- package/dist/adoption/sql.js +106 -0
- package/dist/adoption/wire.d.ts +250 -0
- package/dist/adoption/wire.js +153 -0
- package/dist/approval-card.d.ts +24 -0
- package/dist/approval-card.js +32 -0
- package/dist/boot/adoption.d.ts +30 -0
- package/dist/boot/adoption.js +57 -0
- package/dist/boot/coordinators.d.ts +4 -0
- package/dist/boot/coordinators.js +3 -1
- package/dist/boot/parked-revive-gate.d.ts +38 -5
- package/dist/boot/parked-revive-gate.js +53 -6
- package/dist/boot/runner-deps.d.ts +10 -2
- package/dist/boot/runner-deps.js +12 -1
- package/dist/config-types.d.ts +16 -1
- package/dist/config.js +5 -1
- package/dist/http/routes/adoption.d.ts +26 -0
- package/dist/http/routes/adoption.js +120 -0
- package/dist/http/routes/capabilities.js +12 -0
- package/dist/http/routes/rules.d.ts +23 -0
- package/dist/http/routes/rules.js +117 -0
- package/dist/http/routes/shared-memory.d.ts +31 -0
- package/dist/http/routes/shared-memory.js +181 -0
- package/dist/http/routes/trace-usage.js +139 -3
- package/dist/http/server.d.ts +19 -1
- package/dist/http/server.js +42 -0
- package/dist/main.js +52 -3
- package/dist/observability/fail-open.d.ts +8 -0
- package/dist/observability/fail-open.js +8 -0
- package/dist/plugins/adoption-log-sql.d.ts +191 -0
- package/dist/plugins/adoption-log-sql.js +273 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +13 -0
- package/dist/plugins/checkpoint-store-sql.js +11 -0
- package/dist/plugins/local-checkpoint-store.d.ts +10 -0
- package/dist/plugins/local-checkpoint-store.js +8 -0
- package/dist/plugins/permission-rule-store-sql.d.ts +242 -0
- package/dist/plugins/permission-rule-store-sql.js +817 -0
- package/dist/plugins/pg-pool.js +37 -0
- package/dist/plugins/session-policy-store-sql.d.ts +6 -0
- package/dist/plugins/session-policy-store-sql.js +7 -1
- package/dist/plugins/shared-memory-store-sql.d.ts +223 -0
- package/dist/plugins/shared-memory-store-sql.js +516 -0
- package/dist/plugins/store-backend.d.ts +30 -0
- package/dist/plugins/store-backend.js +14 -0
- package/dist/plugins/tidb-pool.js +47 -0
- package/dist/rules-consent.d.ts +126 -0
- package/dist/rules-consent.js +198 -0
- package/dist/shared-memory-scope-authorizer.d.ts +29 -0
- package/dist/shared-memory-scope-authorizer.js +17 -0
- package/dist/tool-approval.d.ts +55 -0
- package/dist/tool-approval.js +124 -5
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/package.json +3 -3
package/dist/tool-approval.js
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* and asks are turn-blocking by construction — the TTL bounds the human-side exposure instead.
|
|
45
45
|
*/
|
|
46
46
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
47
|
-
import { uuidv7 } from "@sema-agent/core";
|
|
47
|
+
import { uuidv7, MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
|
|
48
48
|
import { redactDeep, redactSecrets } from "./trace/redact.js";
|
|
49
49
|
import { createLogger } from "./observability/logger.js";
|
|
50
50
|
import { deriveAskId, deriveBatchId } from "./approval-ask-machine.js";
|
|
@@ -145,7 +145,21 @@ export function parseToolApprovalResponse(body) {
|
|
|
145
145
|
// [1458] ctrl+g 编辑放行:allow 族可带 updatedInput(编辑后的 args 整体替换形,durable /decide 腿
|
|
146
146
|
// 同名位既有语义);deny 带=无意义,忽略不 400(宽收)。
|
|
147
147
|
const u = body.updatedInput;
|
|
148
|
-
|
|
148
|
+
// #154 车二:「不再询问」的回决位。**只带候选的文本**,不是自由规则 —— 服务端拿它在引擎铸出的
|
|
149
|
+
// 候选表里定位下标(对不上就拒),于是「点一次某条无害命令换来一条别的规则」在这条路上不可拼写。
|
|
150
|
+
// deny 带 = 无意义(人拒了这次操作,谈不上持久化同意),忽略不 400(同 updatedInput 的宽收姿势)。
|
|
151
|
+
const pr = body.persistRule;
|
|
152
|
+
let persistRule;
|
|
153
|
+
if (d !== "deny" && pr !== undefined) {
|
|
154
|
+
if (pr === null || typeof pr !== "object" || Array.isArray(pr))
|
|
155
|
+
return { ok: false, error: "persistRule must be an object" };
|
|
156
|
+
const rule = pr.rule;
|
|
157
|
+
if (typeof rule !== "string" || rule === "" || rule.length > MAX_RULE_TEXT_CHARS) {
|
|
158
|
+
return { ok: false, error: `persistRule.rule must be a non-empty string of at most ${MAX_RULE_TEXT_CHARS} characters` };
|
|
159
|
+
}
|
|
160
|
+
persistRule = rule;
|
|
161
|
+
}
|
|
162
|
+
return { ok: true, value: d, ...(d !== "deny" && u !== undefined ? { updatedInput: u } : {}), ...(persistRule !== undefined ? { persistRule } : {}) };
|
|
149
163
|
}
|
|
150
164
|
return { ok: false, error: 'decision must be one of "allow" | "allow_session" | "deny"' };
|
|
151
165
|
}
|
|
@@ -349,7 +363,12 @@ export class ToolApprovalCoordinator {
|
|
|
349
363
|
* 缺席(生产形)⇒ 每条 run 腿由 {@link runWithContext} 现铸一张、经 ALS 与写侧共享;在场 ⇒ 测试注入的
|
|
350
364
|
* 固定表(此时不进 ALS 作用域,读写都走这一张)。作用域理由见 `governance-ask-marks.ts` 顶注。 */
|
|
351
365
|
governanceAskMarks;
|
|
366
|
+
/** #154 车二:持久化权限规则的**同意车道**。在场 ⇔ 规则店真装配(main.ts 与 core 的
|
|
367
|
+
* `RunnerDeps.permissionRuleStore` 同源于一个对象)⇒ ①ask 帧投 `ruleSuggestions`;②回决带
|
|
368
|
+
* `persistRule` 时兑付进店。缺席 ⇒ 两件都不做(诚实缺席,不发无处可兑的候选)。 */
|
|
369
|
+
ruleConsent;
|
|
352
370
|
constructor(opts) {
|
|
371
|
+
this.ruleConsent = opts?.ruleConsent;
|
|
353
372
|
this.governanceAskMarks = opts?.governanceAskMarks;
|
|
354
373
|
this.ttlMs = opts?.ttlMs ?? DEFAULT_APPROVAL_TTL_MS;
|
|
355
374
|
this.askStore = opts?.askStore;
|
|
@@ -404,6 +423,33 @@ export class ToolApprovalCoordinator {
|
|
|
404
423
|
this.admitByOwner.set(ownerKey, o);
|
|
405
424
|
};
|
|
406
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* #154 车二:一只 ask 的**规则车道素材**,或 `undefined`(= 本 ask 不投候选、不接受 `persistRule`)。
|
|
428
|
+
*
|
|
429
|
+
* 三个合取项(缺一即 `undefined`,理由逐字见调用点上方注):①店在场 ②引擎铸了候选 ③命令原字节可读。
|
|
430
|
+
* `req.args` 是 `unknown` ⇒ 窄读,**禁裸 as-cast**(宪法 [2704]:一个形状漂了的 args 若被 cast,
|
|
431
|
+
* 会把 `undefined` 当命令送进 `prepareCardApproval`,那是放宽面上的静默垃圾)。
|
|
432
|
+
*/
|
|
433
|
+
buildRuleLaneMaterial(req) {
|
|
434
|
+
if (this.ruleConsent === undefined)
|
|
435
|
+
return undefined;
|
|
436
|
+
const suggestions = req.ruleSuggestions;
|
|
437
|
+
if (!Array.isArray(suggestions) || suggestions.length === 0)
|
|
438
|
+
return undefined;
|
|
439
|
+
const args = req.args;
|
|
440
|
+
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
441
|
+
return undefined;
|
|
442
|
+
const command = args.command;
|
|
443
|
+
if (typeof command !== "string" || command === "")
|
|
444
|
+
return undefined;
|
|
445
|
+
const boundInputHash = readBoundInputHash(req);
|
|
446
|
+
return {
|
|
447
|
+
command,
|
|
448
|
+
suggestions,
|
|
449
|
+
...(typeof req.toolCallId === "string" && req.toolCallId !== "" ? { toolCallId: req.toolCallId } : {}),
|
|
450
|
+
...(boundInputHash !== null ? { boundInputHash } : {}),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
407
453
|
/** 测试/可观测性钩子(X-2):某 (taskId) 维当前占用的准入名额数。 */
|
|
408
454
|
admittedCount(taskId) {
|
|
409
455
|
return this.admitByTask.get(taskId) ?? 0;
|
|
@@ -704,6 +750,12 @@ export class ToolApprovalCoordinator {
|
|
|
704
750
|
const id = uuidv7();
|
|
705
751
|
const child = isChildAsk(req);
|
|
706
752
|
const bounded = boundArgs(req.args);
|
|
753
|
+
// #154 车二:规则候选的投影素材。**三个合取项**,缺一不投:
|
|
754
|
+
// ① 规则店真装配(`this.ruleConsent` 在场)—— 店缺席仍投 = 无处可兑的「不再询问」= wire 谎言;
|
|
755
|
+
// ② 引擎真铸了候选(`req.ruleSuggestions` 非空)—— 命令不可匹配(复合/重定向/替换)时 core 交空数组;
|
|
756
|
+
// ③ 命令原字节读得出(回决时**引擎**要拿它重铸候选;读不出就没有可兑付的路,投候选等于骗人)。
|
|
757
|
+
// 读 `req.args.command` 走窄读:`args` 是 `unknown`,禁裸 as-cast(宪法 [2704])。
|
|
758
|
+
const ruleLaneMaterial = this.buildRuleLaneMaterial(req);
|
|
707
759
|
const frame = {
|
|
708
760
|
type: "tool_approval",
|
|
709
761
|
approvalId: id,
|
|
@@ -736,6 +788,8 @@ export class ToolApprovalCoordinator {
|
|
|
736
788
|
...((this.governanceAskMarks ?? governanceAskMarksFor(this.streamKey(primary.owner, primary.sessionId, origin.taskId))).isMarked(req.toolCallId)
|
|
737
789
|
? { governanceForced: true }
|
|
738
790
|
: {}),
|
|
791
|
+
// #154 车二:候选逐字透传(条件见 `ruleLaneMaterial` 上方注)。
|
|
792
|
+
...(ruleLaneMaterial !== undefined ? { ruleSuggestions: ruleLaneMaterial.suggestions } : {}),
|
|
739
793
|
...(bounded.omitted ? { argsOmitted: true } : { args: bounded.args }),
|
|
740
794
|
};
|
|
741
795
|
// #151 车2(design/172 §3.3 D3 窗长三元):有效窗——legDeadlineMonotonic 缺席时退化成 ttlMs(D1 零
|
|
@@ -1237,6 +1291,9 @@ export class ToolApprovalCoordinator {
|
|
|
1237
1291
|
...(child ? { originTaskId: req.sourceTaskId } : { targetCtxs: new Set(aliveCtxs), onTargetGone }),
|
|
1238
1292
|
...(primary.sessionId ? { sessionId: primary.sessionId } : {}),
|
|
1239
1293
|
toolName: req.toolName,
|
|
1294
|
+
// #154 车二:规则车道素材 —— 与帧上的 `ruleSuggestions` **同一个条件**(店在场 ∧ 引擎真铸了候选 ∧
|
|
1295
|
+
// 命令原字节读得出)。三者任一不成立 ⇒ 不登记,回决带 `persistRule` 时如实拒(不猜命令)。
|
|
1296
|
+
...(ruleLaneMaterial !== undefined ? { ruleLane: ruleLaneMaterial } : {}),
|
|
1240
1297
|
...(askId !== undefined && batchId !== undefined ? { askId, batchId } : {}),
|
|
1241
1298
|
};
|
|
1242
1299
|
selfEntry = pendingEntry; // 供 settle() 结算时把自己从 pendingByAskId 的 Set 里摘除(上方顶注)
|
|
@@ -1418,10 +1475,72 @@ export class ToolApprovalCoordinator {
|
|
|
1418
1475
|
// #151 车2 D2(回决竞争者):askStore 缺席 ⇒ finishRespond 同步收尾,逐字不变(D1)。在场 ⇒ 先打一把
|
|
1419
1476
|
// decideAsk CAS,赢了才 finishRespond;输(某个其他竞争者已经决定了这只 ask 的终局)⇒ 不 settle,照旧
|
|
1420
1477
|
// 回现有 404 形(响应体的 409/410 分化是车4 的活,本车只保证「输者绝不覆盖赢者」)。
|
|
1421
|
-
|
|
1422
|
-
|
|
1478
|
+
const settled = this.askStore && entry.askId !== undefined && entry.batchId !== undefined
|
|
1479
|
+
? this.respondWithCas(this.askStore, entry.askId, entry.batchId, id, entry, parsed)
|
|
1480
|
+
: this.finishRespond(id, entry, parsed);
|
|
1481
|
+
// #154 车二:「不再询问」的兑付口。**在裁决落定之后**(顺序是硬的:规则是「这次同意的持久化」,
|
|
1482
|
+
// 一次没有生效的裁决不该留下一条永久规则),且只在 200 那一支 —— 404(CAS 输/条目已被别人结算)
|
|
1483
|
+
// 意味着这次回决不是有效终局,连带的规则确认也就不成立。
|
|
1484
|
+
if (parsed.persistRule === undefined)
|
|
1485
|
+
return settled;
|
|
1486
|
+
return this.persistRuleAfterDecision(settled, entry, parsed.persistRule, parsed.updatedInput !== undefined);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* #154 车二:回决携规则确认 ⇒ prepare→confirm→redeem(实现在 `rules-consent.ts`,本方法只做门与回显)。
|
|
1490
|
+
*
|
|
1491
|
+
* 三道门,每道都是**拒绝**而不是降级:
|
|
1492
|
+
* ① 裁决必须真落定(200)—— 见调用点注;
|
|
1493
|
+
* ② 必须有已验明的 owner —— 规则是**某个人**的持久同意,一条 `owner === null` 的 ask 没有归属人可写;
|
|
1494
|
+
* ③ 本 ask 必须登记过规则车道素材(店在场 ∧ 引擎铸过候选 ∧ 命令可读)。
|
|
1495
|
+
*
|
|
1496
|
+
* 失败**不翻转裁决**:人的「允许这次」已经生效并且已经交给引擎了,把整个回决改判成 4xx 会让一次真实的
|
|
1497
|
+
* 放行凭空消失。诚实的形是 200 + `rulePersisted:false`(壳据它告诉人「这次放行了,但『不再询问』没存上」)。
|
|
1498
|
+
*/
|
|
1499
|
+
async persistRuleAfterDecision(settled, entry, ruleText, inputWasEdited) {
|
|
1500
|
+
const result = await settled;
|
|
1501
|
+
if (result.status !== 200)
|
|
1502
|
+
return result;
|
|
1503
|
+
const lane = this.ruleConsent;
|
|
1504
|
+
const material = entry.ruleLane;
|
|
1505
|
+
const owner = entry.owner;
|
|
1506
|
+
const withFlag = (rulePersisted, ruleRefusal) => ({
|
|
1507
|
+
status: result.status,
|
|
1508
|
+
body: { ...result.body, rulePersisted, ...(ruleRefusal !== undefined ? { ruleRefusal } : {}) },
|
|
1509
|
+
});
|
|
1510
|
+
if (lane === undefined || material === undefined || owner === null)
|
|
1511
|
+
return withFlag(false, "rule_lane_unavailable");
|
|
1512
|
+
// 🔴 codex 交叉复审 round3 [high] 二(验真后修,**真扩权面**):`updatedInput`(ctrl+g 编辑放行)
|
|
1513
|
+
// 与 `persistRule` 同时在场时,**生效的**调用是编辑后的那一份,而候选是引擎从**原始**命令铸的 ——
|
|
1514
|
+
// 于是「把一条危险命令改安全 + 勾上不再询问」会给原始那条危险命令装上一条常驻放行规则。
|
|
1515
|
+
// v1 处置 = **拒绝这次持久化**(裁决本身照常生效;人只是这一次没能顺手存下规则)。
|
|
1516
|
+
// 不选「从编辑后的输入重铸候选」:`updatedInput` 是 `unknown` 的整体替换形,它与这只 ask 上引擎
|
|
1517
|
+
// 铸的候选之间没有任何等式可对 —— 那条路要先给编辑面立形,是另一件事(登记为后续件)。
|
|
1518
|
+
if (inputWasEdited)
|
|
1519
|
+
return withFlag(false, "rule_input_edited");
|
|
1520
|
+
// 报出的文本必须**在引擎铸的候选里**(等式一道)。对不上 ⇒ 拒,不猜:一条不在候选里的文本要么是
|
|
1521
|
+
// 旧卡的残留,要么是有人在试着自铸规则,两种都不该落库。
|
|
1522
|
+
if (!material.suggestions.some((s) => s.rule === ruleText))
|
|
1523
|
+
return withFlag(false, "rule_not_offered");
|
|
1524
|
+
try {
|
|
1525
|
+
const persisted = await lane.persistCardRule({
|
|
1526
|
+
principal: owner,
|
|
1527
|
+
toolName: entry.toolName,
|
|
1528
|
+
command: material.command,
|
|
1529
|
+
ruleText,
|
|
1530
|
+
...(material.toolCallId !== undefined ? { toolCallId: material.toolCallId } : {}),
|
|
1531
|
+
...(material.boundInputHash !== undefined ? { boundInputHash: material.boundInputHash } : {}),
|
|
1532
|
+
});
|
|
1533
|
+
if (!persisted.ok) {
|
|
1534
|
+
defaultLogger.warn("permission rule was not persisted after an approval", { reason: persisted.reason, detail: persisted.detail });
|
|
1535
|
+
return withFlag(false, persisted.reason);
|
|
1536
|
+
}
|
|
1537
|
+
return withFlag(true);
|
|
1538
|
+
}
|
|
1539
|
+
catch (err) {
|
|
1540
|
+
// D5 同族姿势:店抖动不改判人的裁决,只如实报「没存上」。
|
|
1541
|
+
this.noteStoreError(err, "persistCardRule");
|
|
1542
|
+
return withFlag(false, "rule_store_error");
|
|
1423
1543
|
}
|
|
1424
|
-
return this.finishRespond(id, entry, parsed);
|
|
1425
1544
|
}
|
|
1426
1545
|
/** store 在场时的回决 CAS 门(D2)——`respond()` 的异步分支,拆出以保持 `respond()` 本身在 store 缺席
|
|
1427
1546
|
* 时仍是纯同步函数(D1)。`askStore`/`askId`/`batchId` 由调用方在已窄化的分支里传入(避免非空断言)。 */
|
|
@@ -26,8 +26,8 @@ type BgNotifExcluded = "kind" | "sessionScoped" | "owner" | "scope" | "descripti
|
|
|
26
26
|
type _GuardBgNotif = AssertAllKeysHandled<Exclude<keyof BackgroundChildEvent, BgNotifProjected | BgNotifExcluded>>;
|
|
27
27
|
type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
|
|
28
28
|
type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
|
|
29
|
-
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation";
|
|
30
|
-
type AskExcluded = "preview" | "principal" | "requiresRealApproval" | "riskAxes" | "boundInputHash"
|
|
29
|
+
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleSuggestions";
|
|
30
|
+
type AskExcluded = "preview" | "principal" | "requiresRealApproval" | "riskAxes" | "boundInputHash";
|
|
31
31
|
type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
|
|
32
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";
|
|
33
33
|
type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
|
package/dist/trace/project.d.ts
CHANGED
package/dist/trace/project.js
CHANGED
|
@@ -561,6 +561,7 @@ export function brainStatusEventData(ev) {
|
|
|
561
561
|
...(num(ev.attempt) ? { attempt: ev.attempt } : {}),
|
|
562
562
|
...(num(ev.maxRetries) ? { maxRetries: ev.maxRetries } : {}),
|
|
563
563
|
...(num(ev.retryInMs) ? { retryInMs: ev.retryInMs } : {}),
|
|
564
|
+
...(typeof ev.errClass === "string" && ev.errClass.length > 0 ? { errClass: ev.errClass } : {}),
|
|
564
565
|
...identityFields(ev),
|
|
565
566
|
};
|
|
566
567
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.11.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.22.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.13.0",
|
|
73
73
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
74
74
|
"@types/node": "22.10.2",
|
|
75
75
|
"@types/pg": "^8.20.0",
|