@sema-agent/server 7.119.0 → 7.120.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/MIGRATION.md +54 -0
- package/USAGE.md +5 -3
- package/dist/boot/config-center.js +5 -5
- package/dist/boot/execution-env.js +3 -2
- package/dist/boot/fatal-exit.d.ts +7 -1
- package/dist/boot/fatal-exit.js +2 -1
- package/dist/boot/runner-deps.d.ts +4 -3
- package/dist/boot/runner-deps.js +7 -3
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/capabilities/builtin-tools.d.ts +31 -8
- package/dist/capabilities/builtin-tools.js +22 -1
- package/dist/capabilities/scenarios.d.ts +8 -6
- package/dist/capabilities/scenarios.js +3 -3
- package/dist/capabilities/send-user-file-tool.d.ts +6 -3
- package/dist/capabilities/send-user-file-tool.js +1 -1
- package/dist/config-catalog.js +2 -2
- package/dist/config-lkg.d.ts +33 -5
- package/dist/config-lkg.js +55 -11
- package/dist/config-types.d.ts +8 -1
- package/dist/config.js +9 -9
- package/dist/deployment-governance.d.ts +3 -1
- package/dist/deployment-governance.js +3 -1
- package/dist/device-store.d.ts +7 -0
- package/dist/device-store.js +4 -0
- package/dist/device-ws-hub.js +17 -9
- package/dist/device-ws-protocol.d.ts +15 -0
- package/dist/http/resume-legs.js +4 -0
- package/dist/http/routes/capabilities.js +1 -0
- package/dist/http/routes/devices.js +3 -3
- package/dist/http/routes/rules.js +13 -6
- package/dist/list-entry-display.d.ts +14 -0
- package/dist/list-entry-display.js +14 -0
- package/dist/namespaced-rule-name.js +2 -6
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/operator-ask.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +8 -0
- package/dist/plugins/one-time-migrations.js +72 -3
- package/dist/plugins/permission-rule-store-file.js +2 -6
- package/dist/plugins/permission-rule-store-sql.d.ts +64 -6
- package/dist/plugins/permission-rule-store-sql.js +166 -59
- package/dist/plugins/remote-env-device.d.ts +12 -1
- package/dist/plugins/remote-env-device.js +13 -5
- package/dist/plugins/scheduler-support.d.ts +8 -4
- package/dist/plugins/scheduler-support.js +5 -5
- package/dist/rule-entry-list.d.ts +0 -1
- package/dist/rule-entry-list.js +4 -9
- package/dist/rules-consent.d.ts +12 -3
- package/dist/rules-consent.js +5 -4
- package/dist/run-local.js +2 -0
- package/dist/sema-config-dir.d.ts +19 -0
- package/dist/sema-config-dir.js +33 -0
- package/dist/session-policy-wire.js +5 -4
- package/dist/spec-fields.d.ts +5 -4
- package/dist/task-settings.d.ts +3 -3
- package/dist/trace/engine-notice-wire.d.ts +6 -4
- package/dist/trace/engine-notice-wire.js +16 -9
- package/dist/trace/self-minted-wire.d.ts +14 -0
- package/dist/trace/self-minted-wire.js +2 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
- package/package.json +2 -2
package/dist/rules-consent.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createLogger } from "./observability/logger.js";
|
|
|
5
5
|
import { MAX_CWD_CHARS } from "./task-cwd.js";
|
|
6
6
|
import { isDirectWritableRuleBehavior } from "./permission-rule-vocab.js";
|
|
7
7
|
import { CONTROL_AND_BIDI_CHARS, hasBidiControls } from "./text-bidi.js";
|
|
8
|
-
import { confirmRuleApproval, RULE_BEHAVIORS, parseRuleText, ruleReachesProgramRun, ruleToolGrammarOf, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, addPersistedRule, removePersistedRule, effectiveOrThrow, readRowBehavior, } from "@sema-agent/core";
|
|
8
|
+
import { confirmRuleApproval, RULE_BEHAVIORS, parseRuleText, ruleReachesProgramRun, ruleToolGrammarOf, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, addPersistedRule, removePersistedRule, isValidDurableScope, effectiveOrThrow, readRowBehavior, } from "@sema-agent/core";
|
|
9
9
|
import { buildRulePayloadHash } from "./plugins/permission-rule-store-sql.js";
|
|
10
10
|
export const RULE_IMPORT_TICKET_TTL_MS = 10 * 60_000;
|
|
11
11
|
export const RULE_IMPORT_RETRY_AFTER_SEC = 2;
|
|
@@ -57,9 +57,10 @@ export function parseRuleScope(text) {
|
|
|
57
57
|
return { kind: "global" };
|
|
58
58
|
if (!text.startsWith("project:"))
|
|
59
59
|
return undefined;
|
|
60
|
-
const
|
|
61
|
-
return
|
|
60
|
+
const scope = { kind: "project", root: text.slice("project:".length) };
|
|
61
|
+
return isValidDurableScope(scope) ? scope : undefined;
|
|
62
62
|
}
|
|
63
|
+
export const RULE_SCOPE_REFUSAL_SENTENCE = "scope must be 'global' or 'project:<root>' with an absolute root";
|
|
63
64
|
export function sameWireRuleIdentity(row, identity) {
|
|
64
65
|
return row.behavior === identity.behavior && row.rule === identity.rule && row.scope === identity.scope;
|
|
65
66
|
}
|
|
@@ -163,7 +164,7 @@ function anchorBatchMembers(members, displayed) {
|
|
|
163
164
|
function screenLocalImportRow(row) {
|
|
164
165
|
const scope = parseRuleScope(row.scope);
|
|
165
166
|
if (scope === undefined) {
|
|
166
|
-
return { reason: "scope_unreadable", detail:
|
|
167
|
+
return { reason: "scope_unreadable", detail: RULE_SCOPE_REFUSAL_SENTENCE };
|
|
167
168
|
}
|
|
168
169
|
if (scope.kind === "project" && hasBidiControls(scope.root)) {
|
|
169
170
|
return {
|
package/dist/run-local.js
CHANGED
|
@@ -56,6 +56,7 @@ import { installSecretEnvScrubRecorder } from "./observability/secret-env-scrub.
|
|
|
56
56
|
import { createHostLspManager } from "./host-lsp-manager.js";
|
|
57
57
|
import { buildDisplaySafeUrl } from "./trace/redact.js";
|
|
58
58
|
import { installOutboundDispatcher } from "./outbound-dispatcher.js";
|
|
59
|
+
import { provisionSemaConfigDir } from "./sema-config-dir.js";
|
|
59
60
|
export function parseArgs(argv) {
|
|
60
61
|
let objective;
|
|
61
62
|
let root;
|
|
@@ -325,6 +326,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
325
326
|
const memoryEngineIncident = createMemoryEngineIncidentSink({ logger, metrics });
|
|
326
327
|
installSecretEnvScrubRecorder({ logger, metrics });
|
|
327
328
|
logConfigDiagnostics(logger);
|
|
329
|
+
provisionSemaConfigDir(config, logger);
|
|
328
330
|
installOutboundDispatcher(logger);
|
|
329
331
|
const providerInput = { provider: config.configProvider ?? "local", localDir: root };
|
|
330
332
|
try {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Logger } from "./observability/logger.js";
|
|
2
|
+
/** 部署声明的配置根(绝对路径);没声明 ⇒ `undefined`;空串 ⇒ 抛(起服拒,句子点名变量与出路)。纯读 env,不碰文件。**只有 `loadConfig` 调它**
|
|
3
|
+
* (结果 = 配置席 `agentConfigDir`);别的读者读那一席。 */
|
|
4
|
+
export declare function declaredSemaConfigDir(env?: NodeJS.ProcessEnv): string | undefined;
|
|
5
|
+
/** 配置根:配置席 `agentConfigDir`(`loadConfig` 铸,声明了的根)在 ⇒ 它;缺席 ⇒ `~/.sema`(与引擎读字面 `~/.sema` 同一处)。不读 env。 */
|
|
6
|
+
export declare function configRootOf(config: {
|
|
7
|
+
readonly agentConfigDir?: string;
|
|
8
|
+
}): string;
|
|
9
|
+
/**
|
|
10
|
+
* 起服的**供给动作**(每个入口一次:HTTP 服务第 1 段、`run-local`,都在 logger 建起来之后、任何 Runner 之前):声明了配置根 ⇒ 确保目录在
|
|
11
|
+
* (不在就建,`0700` —— 本服务本来就往里写 LKG / 缓存 / 店)。没声明 ⇒ 什么都不做(`~/.sema` 由各写者首次写时自建,同 7.119.x)。
|
|
12
|
+
*
|
|
13
|
+
* **建不出来不在这里拒**:能不能用由引擎判(`new Runner` 对不是已存在目录的根拒 `config.agent_config_dir_invalid`,起服随之拒),本服务
|
|
14
|
+
* 只把建目录这件事的失败如实说一行(`sema_config_dir_unprovisioned`,点名变量与 errno)—— 运维在引擎的拒句之前就看到是哪根旋钮、为什么建不出。
|
|
15
|
+
*/
|
|
16
|
+
export declare function provisionSemaConfigDir(config: {
|
|
17
|
+
readonly agentConfigDir?: string;
|
|
18
|
+
}, logger: Pick<Logger, "warn">): void;
|
|
19
|
+
//# sourceMappingURL=sema-config-dir.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
export function declaredSemaConfigDir(env = process.env) {
|
|
5
|
+
const raw = env.SEMA_CONFIG_DIR;
|
|
6
|
+
if (raw === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (raw === "") {
|
|
9
|
+
throw new Error('env SEMA_CONFIG_DIR="" is empty — refusing to start rather than moving the configuration files: up to 7.119.x an empty value meant the process ' +
|
|
10
|
+
"working directory (the last-known-good config, the skill cache and the scheduler store were kept there), and reading it as unset would move them " +
|
|
11
|
+
"to ~/.sema without a word. Unset the variable to use ~/.sema, or set it to the directory you mean (to keep the old place, the working directory's path).");
|
|
12
|
+
}
|
|
13
|
+
return resolve(raw);
|
|
14
|
+
}
|
|
15
|
+
export function configRootOf(config) {
|
|
16
|
+
return config.agentConfigDir ?? join(homedir(), ".sema");
|
|
17
|
+
}
|
|
18
|
+
export function provisionSemaConfigDir(config, logger) {
|
|
19
|
+
const dir = config.agentConfigDir;
|
|
20
|
+
if (dir === undefined)
|
|
21
|
+
return;
|
|
22
|
+
try {
|
|
23
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
logger.warn("sema_config_dir_unprovisioned", {
|
|
27
|
+
dir,
|
|
28
|
+
err: e instanceof Error ? e.message : String(e),
|
|
29
|
+
note: "SEMA_CONFIG_DIR names a directory this server could not create — the engine refuses a configuration root that is not an existing directory when the first Runner is built, so the boot stops there with the engine's refusal: create the directory, point the variable at one, or unset it (the default is ~/.sema)",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=sema-config-dir.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PROTOCOL_TABLE, SessionPolicyError, assertSessionRuleNamesCurrent } from "@sema-agent/core";
|
|
2
|
+
import { displayListEntry } from "./list-entry-display.js";
|
|
2
3
|
import { describeEmptyToolSegment, emptyToolSegmentOf } from "./namespaced-rule-name.js";
|
|
3
4
|
const RULE_FIELDS = {
|
|
4
5
|
toolAllow: true,
|
|
@@ -99,17 +100,17 @@ export function describeSessionPermissionRulesProblem(p) {
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
function describeUnreachableSessionRuleName(u) {
|
|
102
|
-
return `${u.field} names
|
|
103
|
+
return `${u.field} names ${displayListEntry(u.name)}, which can never match a tool (${describeEmptyToolSegment(u.ns)}), so nothing was written`;
|
|
103
104
|
}
|
|
104
105
|
function describeLegacyToolName(r) {
|
|
105
106
|
const why = r.guidance !== undefined ? ` (${r.guidance})` : "";
|
|
106
107
|
switch (r.verdict) {
|
|
107
108
|
case "retired":
|
|
108
|
-
return `${r.field} names
|
|
109
|
+
return `${r.field} names ${displayListEntry(r.name)}, a retired tool name${why}; with that rule in place this session could not start another run, so nothing was written — use the current tool name${r.currentName !== undefined ? ` (${displayListEntry(r.currentName)})` : ""}`;
|
|
109
110
|
case "unprefixed_namespace":
|
|
110
|
-
return `${r.field} names
|
|
111
|
+
return `${r.field} names ${displayListEntry(r.name)}, which contains "__" but no protocol prefix; with that rule in place this session could not start another run, so nothing was written — a protocol tool is named ${protocolToolNameShapes()}`;
|
|
111
112
|
case "unmintable_segment":
|
|
112
|
-
return `${r.field} names
|
|
113
|
+
return `${r.field} names ${displayListEntry(r.name)}, which can never match a tool${why}, so nothing was written`;
|
|
113
114
|
default: {
|
|
114
115
|
const unhandled = r.verdict;
|
|
115
116
|
throw new Error(`unhandled legacy tool name verdict: ${JSON.stringify(unhandled)}`);
|
package/dist/spec-fields.d.ts
CHANGED
|
@@ -67,10 +67,11 @@ export declare function skillToolListRefusal(skill: Record<string, unknown>): st
|
|
|
67
67
|
* 白名单外键 fail-loud —— 与 resilience/limits 的「容忍未知键」相反:agents 是能力定义,静默丢字段 =
|
|
68
68
|
* 定义被悄悄削(一个丢掉的 isolation 就是放宽),比 400 更危险,故整条拒。`allowTools` / `denyTools` 本门只判形与条数(`toolList`),
|
|
69
69
|
* **名字归引擎一只判**(core 7.45.0 #1377 / #1385,开跑前,三来源同判):没有工具能是的拼法(带空白 / 括号形坏 / 不可见字符)、非字符串条目 ⇒ 整 run 拒
|
|
70
|
-
* `config.agent.unknown_tool`;无人挂的名 ⇒ 不拒、经 `onError`(phase `"config"`)告一次(本仓汇点 `runner_config_advisory`);大小写孪生 / 退役名(`bash`)
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* `denyTools`
|
|
70
|
+
* `config.agent.unknown_tool`;无人挂的名 ⇒ 不拒、经 `onError`(phase `"config"`)告一次(本仓汇点 `runner_config_advisory`);大小写孪生 / 退役名(`bash` / `BASH`)
|
|
71
|
+
* ⇒ 开跑前拒 `config.agent.unknown_tool` —— 主车道的委派工具带 extraTools 工厂,本仓在同一处声明它的名册(core 7.46.0 `extraToolNames`;工厂与名册读同一批工具对象,
|
|
72
|
+
* `capabilities/builtin-tools.ts` `createSubagentExtraTools`),引擎因此列得出子代工具池。两张表管子代手里的**全部**工具:委派池与 hands 内建(`Read` / `Edit` / `Write` /
|
|
73
|
+
* `Bash` …,core #1430)—— `denyTools:["Write"]` ⇒ 子代没有 `Write`;`allowTools` 只点了调用方工具 ⇒ 子代一只 hands 工具都没有。hands 行之外的引擎工具
|
|
74
|
+
* (`Monitor` 等)仍不受这两张表管(引擎已知限制)。`denyTools` 的 `Tool(content)` ⇒ 整只工具对该子代藏起并告。本仓不在受理口另做名字对照(那会是同一问的第二个判官)。
|
|
74
75
|
* ⚠️ 键判断必须 Object.hasOwn:`in` 走原型链,JSON 里的 "toString"/"constructor"/"__proto__" 键会被
|
|
75
76
|
* 误判为白名单字段,随后 SHAPES[k].ok 不是函数 ⇒ TypeError(fresh 500 / resume 砖;cross-review 复现)。 */
|
|
76
77
|
export declare function taskAgentIssue(item: unknown): string | null;
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -373,9 +373,9 @@ export interface FsWriteGateWiring {
|
|
|
373
373
|
* 存不存在 ⇒ 把哨兵递给它,`deny Edit(src/**)` 会编译成功、指向一个谁也不会写的目录,对真实工作区
|
|
374
374
|
* 的 `<工作区>/src/secret.ts` 这类目标判 allow —— 一条运维亲手写下的收紧规则**静默失效**,方向与写门那条正相反。
|
|
375
375
|
*
|
|
376
|
-
* ⇒ 本字段**只在真任务根已知时**在场。缺席 ⇒
|
|
377
|
-
* (
|
|
378
|
-
*
|
|
376
|
+
* ⇒ 本字段**只在真任务根已知时**在场。缺席 ⇒ 不声明根 ⇒ 需要该基的拼法(`Edit(src/**)` / `Edit(/etc/**)`)锚在**装入这只策略的那条腿**
|
|
377
|
+
* 的真根(core 7.46.0 #1383 车 B:锚装一次、在被装入处;`unsupported.path_base` 只剩「声明了一个用不了的基」)—— 哨兵从不被声明成基。
|
|
378
|
+
* 修前(core ≤7.45)缺席 ⇒ 整表拒(→ 422):沙箱车道上相对 / `/x` 形规则恒 422。绝对形(`//abs`)不需要基;`~/` 形见下一字段。
|
|
379
379
|
*/
|
|
380
380
|
taskRoot?: string;
|
|
381
381
|
/**
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
|
|
17
17
|
* 刚立,立完就在下一次加码时自己咬住了)。
|
|
18
18
|
*/
|
|
19
|
-
export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "instructions.source_changed", "session.records_skipped", "server.model_unreachable", "classifier.unavailable", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.allowed_tool_not_granted", "task.tool_turn_incomplete"];
|
|
19
|
+
export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "instructions.source_changed", "session.records_skipped", "server.model_unreachable", "classifier.unavailable", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.rule_row_dropped", "permission.allowed_tool_not_granted", "task.tool_turn_incomplete"];
|
|
20
20
|
export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
|
|
21
21
|
/** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */
|
|
22
22
|
export declare function isEngineNoticeWireCode(code: string): code is EngineNoticeWireCode;
|
|
@@ -63,10 +63,12 @@ export interface EngineNoticeWireRow {
|
|
|
63
63
|
*/
|
|
64
64
|
export declare function sanitizeNoticeDetail(detail: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
65
65
|
/**
|
|
66
|
-
* 通告 `detail`
|
|
67
|
-
* 与运维日志行(`boot/runner-deps.ts`
|
|
66
|
+
* 通告 `detail` 里的**规则文本**换成条目的脱敏原值({@link redactListEntry}:凭据换记号,不加引号、不转义 —— 机读位不是显示面,显示形只属于人读句)。
|
|
67
|
+
* 会话流行(经 {@link engineNoticeEventData},之后照常过 {@link sanitizeNoticeDetail})与运维日志行(`boot/runner-deps.ts` 的转发席,其余字段逐字)
|
|
68
|
+
* 同调这一只 ⇒ 两条出路上同一条规则文本逐字节同(会话流行另有 detail 串的 {@link NOTICE_TEXT_MAX} 字上限,超长的那一截只在日志行上),两只码里同一条
|
|
69
|
+
* 规则文本也同(引擎交来的两形本身一致时 —— `rule_row_dropped` 交的是引擎中和过的形,空白 / 超长的条目两只码今天不同,上游缝)。其余字段、其余码原样。
|
|
68
70
|
*/
|
|
69
|
-
export declare function
|
|
71
|
+
export declare function redactNoticeRuleText(code: string, detail: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
|
|
70
72
|
export declare function engineNoticeEventData(notice: {
|
|
71
73
|
code: string;
|
|
72
74
|
message: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redactSecrets } from "./redact.js";
|
|
2
|
-
import {
|
|
2
|
+
import { redactListEntry } from "../list-entry-display.js";
|
|
3
3
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
4
4
|
export const ENGINE_NOTICE_WIRE_CODES = [
|
|
5
5
|
"memory.session_polluted",
|
|
@@ -34,6 +34,7 @@ export const ENGINE_NOTICE_WIRE_CODES = [
|
|
|
34
34
|
"permission.ask_released_by_mode",
|
|
35
35
|
"permission.read_deny_unapplied",
|
|
36
36
|
"permission.rule_store_unavailable",
|
|
37
|
+
"permission.rule_row_dropped",
|
|
37
38
|
"permission.allowed_tool_not_granted",
|
|
38
39
|
"task.tool_turn_incomplete",
|
|
39
40
|
];
|
|
@@ -92,22 +93,28 @@ function sanitizeValue(v, depth) {
|
|
|
92
93
|
}
|
|
93
94
|
return undefined;
|
|
94
95
|
}
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
const RULE_TEXT_DETAIL_FIELDS = {
|
|
97
|
+
"permission.allowed_tool_not_granted": { array: "entries", field: "entry" },
|
|
98
|
+
"permission.rule_row_dropped": { array: "rows", field: "rule" },
|
|
99
|
+
};
|
|
100
|
+
export function redactNoticeRuleText(code, detail) {
|
|
101
|
+
const at = Object.hasOwn(RULE_TEXT_DETAIL_FIELDS, code) ? RULE_TEXT_DETAIL_FIELDS[code] : undefined;
|
|
102
|
+
if (at === undefined || detail === undefined)
|
|
99
103
|
return detail;
|
|
100
|
-
const list = detail[
|
|
104
|
+
const list = detail[at.array];
|
|
101
105
|
if (!Array.isArray(list))
|
|
102
106
|
return detail;
|
|
103
|
-
const
|
|
104
|
-
|
|
107
|
+
const redacted = list.map((item) => {
|
|
108
|
+
const text = item !== null && typeof item === "object" ? item[at.field] : undefined;
|
|
109
|
+
return typeof text === "string" ? { ...item, [at.field]: redactListEntry(text) } : item;
|
|
110
|
+
});
|
|
111
|
+
return { ...detail, [at.array]: redacted };
|
|
105
112
|
}
|
|
106
113
|
export function engineNoticeEventData(notice, sessionId, nowMs) {
|
|
107
114
|
return {
|
|
108
115
|
code: notice.code,
|
|
109
116
|
message: redactSecrets(String(notice.message)).slice(0, NOTICE_TEXT_MAX),
|
|
110
|
-
detail: sanitizeNoticeDetail(
|
|
117
|
+
detail: sanitizeNoticeDetail(redactNoticeRuleText(notice.code, notice.detail)),
|
|
111
118
|
sessionId,
|
|
112
119
|
ts: nowMs,
|
|
113
120
|
};
|
|
@@ -183,6 +183,20 @@ export declare const SELF_MINTED_WIRE: {
|
|
|
183
183
|
readonly attr: "redactErrorMessage";
|
|
184
184
|
readonly since: "7.108.0";
|
|
185
185
|
};
|
|
186
|
+
/**
|
|
187
|
+
* 7.120.0:通告 `detail` 里的规则文本 = 条目的脱敏原值(`list-entry-display.ts` `redactListEntry`,经 `engine-notice-wire.ts` 的
|
|
188
|
+
* `redactNoticeRuleText`;会话流行与运维日志行同一只;不加引号、不转义 —— 显示形只在人读句里)。修前(7.119.0):本仓手抄的呈现形
|
|
189
|
+
* (剥 C0 / 双向控制符、超过 160 字截断并标原长),运维日志行不脱敏。钉 test/notice-entry-presentation.test.ts。
|
|
190
|
+
*/
|
|
191
|
+
readonly "engine_notice permission.allowed_tool_not_granted#detail.entries[].entry": {
|
|
192
|
+
readonly attr: "redactListEntry";
|
|
193
|
+
readonly since: "7.120.0";
|
|
194
|
+
};
|
|
195
|
+
/** 7.120.0:同上一行同一只写法(码本版入册;引擎交来的是它中和过的规则文本,本仓再过脱敏读口)。钉同上「两只码」组。 */
|
|
196
|
+
readonly "engine_notice permission.rule_row_dropped#detail.rows[].rule": {
|
|
197
|
+
readonly attr: "redactListEntry";
|
|
198
|
+
readonly since: "7.120.0";
|
|
199
|
+
};
|
|
186
200
|
};
|
|
187
201
|
};
|
|
188
202
|
};
|
|
@@ -65,6 +65,8 @@ export const SELF_MINTED_WIRE = {
|
|
|
65
65
|
members: {
|
|
66
66
|
"GET /v1/runs/:id#result.errorMessage": { attr: "redactErrorMessage", since: "7.57.0" },
|
|
67
67
|
"POST /v1/side-query#errorMessage": { attr: "redactErrorMessage", since: "7.108.0" },
|
|
68
|
+
"engine_notice permission.allowed_tool_not_granted#detail.entries[].entry": { attr: "redactListEntry", since: "7.120.0" },
|
|
69
|
+
"engine_notice permission.rule_row_dropped#detail.rows[].rule": { attr: "redactListEntry", since: "7.120.0" },
|
|
68
70
|
},
|
|
69
71
|
},
|
|
70
72
|
};
|
|
@@ -46,7 +46,9 @@ which version the server speaks before you spend a handshake finding out.
|
|
|
46
46
|
acquire the connection lease
|
|
47
47
|
◀── helloAck { gen, heartbeatIntervalMs,
|
|
48
48
|
maxInflight, pendingInstructions,
|
|
49
|
-
chunkWatermarks
|
|
49
|
+
chunkWatermarks, reconnectGraceMs,
|
|
50
|
+
cancelGraceMs, chunkGapTimeoutMs,
|
|
51
|
+
executionCeilingGraceMs }
|
|
50
52
|
… or helloReject { code }
|
|
51
53
|
fence: kill/drain everything from older generations
|
|
52
54
|
generationAck { gen } ───────────────────────▶ now (and only now) the
|
|
@@ -79,7 +81,7 @@ which version the server speaks before you spend a handshake finding out.
|
|
|
79
81
|
| `platformOs` | yes | closed set `darwin` \| `linux` (anything else is a `400`; there is no third value) |
|
|
80
82
|
| `platformArch` | no | ≤ 32 bytes |
|
|
81
83
|
| `displayName` | no | ≤ 255 bytes |
|
|
82
|
-
| `workspaceRoot` | yes |
|
|
84
|
+
| `workspaceRoot` | yes | ≤ 1024 bytes, **an absolute POSIX path** — the device-side root that path resolution and `exec` `cwd` fall back to. Registration refuses anything else (empty, relative, `~/…`) with `400 request.field_invalid`; the same check runs again when a session's execution environment is created, so a device registered with a non-absolute root by an older server is refused there |
|
|
83
85
|
|
|
84
86
|
`deviceId` and the owner are **not** inputs: the server mints `dev_<26-char ULID>` and takes the owner
|
|
85
87
|
from the verified credential. The response is `200 { deviceId }` and contains no secret of any kind.
|
|
@@ -251,6 +253,17 @@ backoff, exponential to a 60 s ceiling, jittered — except for `revoked`, which
|
|
|
251
253
|
| `gen` | **canonical decimal string** (`"7"`, never `7`, never `"07"`) | the connection generation. Stamp it on every frame you send |
|
|
252
254
|
| `pendingInstructions` | `string[]` | instruction ids the server still considers in flight for you — see §5 R2 |
|
|
253
255
|
| `chunkWatermarks` | `{ [instructionId]: number }` | per instruction, the highest **contiguous** `streamSeq` the server has already accepted. `-1` means "nothing accepted yet" |
|
|
256
|
+
| `reconnectGraceMs` | integer ms | the reconnect grace **in force on this server** (R4): how long in-flight work survives a dropped socket. Deployment-tunable, default 120000 |
|
|
257
|
+
| `cancelGraceMs` | integer ms | the cancel grace in force (R11): how long after a `cancel` is delivered the server waits for your terminal `result`. Default 10000 |
|
|
258
|
+
| `chunkGapTimeoutMs` | integer ms | the chunk-gap timeout in force (R8): how long the server waits for a missing `streamSeq` before failing the instruction. Default 30000 |
|
|
259
|
+
| `executionCeilingGraceMs` | integer ms | the grace added to an instruction's `timeoutMs` before the server gives up and settles it as outcome-unknown (R12: the ceiling is `timeoutMs + executionCeilingGraceMs`). Default 30000 |
|
|
260
|
+
|
|
261
|
+
The four grace fields are the values the server's own timers are armed with — not documentation defaults. Size
|
|
262
|
+
your reconnect backoff ceiling, your `cancel` answer deadline, your retransmission cadence and your own
|
|
263
|
+
`timeoutMs` enforcement from them. A server that predates them omits all four; treat absence as "not told" and
|
|
264
|
+
fall back to the defaults in the table above, never as zero. For the same reason the published TypeScript type
|
|
265
|
+
(`DeviceHelloAckFrame` from `@sema-agent/server/device-protocol`) declares the four as optional; a server from
|
|
266
|
+
7.120.0 on always sends all four.
|
|
254
267
|
|
|
255
268
|
`gen` is a string because it is a 64-bit database value; parse it as an integer only if your language can
|
|
256
269
|
hold it, and always **echo the string you were given** rather than re-formatting a number.
|
|
@@ -276,7 +289,7 @@ fenced — that half is yours, and it is the reason a device-level single-instan
|
|
|
276
289
|
|
|
277
290
|
Send `goodbye { gen, reason, graceMs? }`. The server stops *starting* new dispatches for this connection
|
|
278
291
|
and keeps collecting results for the ones already in flight, then closes the socket after `graceMs` (if you
|
|
279
|
-
omit it, the deployment's reconnect grace — default 120000 ms — is used). Sending `goodbye` does not settle
|
|
292
|
+
omit it, the deployment's reconnect grace — `helloAck.reconnectGraceMs`, default 120000 ms — is used). Sending `goodbye` does not settle
|
|
280
293
|
anything: finish or fail your in-flight instructions properly before you exit.
|
|
281
294
|
|
|
282
295
|
⚠️ **`goodbye` is not a hard barrier.** The check happens when a dispatch *enters* the pipeline, and a
|
|
@@ -341,9 +354,9 @@ zeros, a plus sign, whitespace or a float form are rejected as malformed.
|
|
|
341
354
|
| `t` | Fields | Notes |
|
|
342
355
|
|---|---|---|
|
|
343
356
|
| `challenge` | `nonce` string, `protocolVersion` int | first frame the server sends, immediately on open |
|
|
344
|
-
| `helloAck` | `ok`, `heartbeatIntervalMs`, `maxInflight`, `gen`, `pendingInstructions`, `chunkWatermarks` | §1.5 |
|
|
357
|
+
| `helloAck` | `ok`, `heartbeatIntervalMs`, `maxInflight`, `gen`, `pendingInstructions`, `chunkWatermarks`, `reconnectGraceMs`, `cancelGraceMs`, `chunkGapTimeoutMs`, `executionCeilingGraceMs` | §1.5 |
|
|
345
358
|
| `helloReject` | `code`, `minProtocolVersion?` | §1.4; the socket closes right after |
|
|
346
|
-
| `instruction` | `gen`, `instructionId`, `rootSessionId`, `issuedSeq` int, `kind`, `args`, `cwd?`, `shellEnv?`, `timeoutMs` int | one unit of work (§3) |
|
|
359
|
+
| `instruction` | `gen`, `instructionId`, `rootSessionId`, `issuedSeq` int, `kind`, `args`, `cwd?`, `shellEnv?`, `timeoutMs` int | one unit of work (§3). `cwd` appears on `exec` / `execStream` only, and always there (§3.1) |
|
|
347
360
|
| `payload` | `gen`, `instructionId`, `part` (`begin` \| `chunk` \| `commit`), `offset?`, `dataB64?`, `totalLen?`, `sha256?` | the bytes for a write-family instruction (§3.4) |
|
|
348
361
|
| `cancel` | `gen`, `instructionId` | idempotent; answer with a terminal `result` (§5 R11) |
|
|
349
362
|
| `resultAck` | `gen`, `instructionId` | you may release that instruction's buffers |
|
|
@@ -416,8 +429,10 @@ silently forgiven: it becomes a gap or a missing terminal, and the instruction e
|
|
|
416
429
|
### 3.1 The closed set, its arguments, and the success outcome each one owes
|
|
417
430
|
|
|
418
431
|
Every `path` in `args` arrives **already absolute and POSIX-normalized** — the server resolved it against
|
|
419
|
-
the session's working directory before sending. `
|
|
420
|
-
|
|
432
|
+
the session's working directory before sending. `exec` and `execStream` additionally carry `cwd`, which is
|
|
433
|
+
**always present and always absolute**: run the command in exactly that directory. There is no implicit
|
|
434
|
+
base to fall back to, so an executor needs no branch for a missing or relative `cwd`. Every other kind
|
|
435
|
+
carries no `cwd` — its paths are already absolute.
|
|
421
436
|
|
|
422
437
|
<!-- closed-set: DEVICE_INSTRUCTION_KINDS -->
|
|
423
438
|
|
|
@@ -551,10 +566,37 @@ payload { part: "commit", instructionId, totalLen, sha256 }
|
|
|
551
566
|
2. on `commit`, verify the length **and** the digest;
|
|
552
567
|
3. only then rename into place — atomically, honouring the instruction's semantics (exclusive create,
|
|
553
568
|
guarded expectation, append);
|
|
554
|
-
4. any failure at any step fails the **whole instruction** and leaves **no half-written file
|
|
569
|
+
4. any failure at any step fails the **whole instruction** and leaves **no half-written file** — with one
|
|
570
|
+
exception, `appendFile`, which is stated next.
|
|
555
571
|
|
|
556
572
|
`defaultPayloadPartBytes` is 262144, but do not assume it: use `offset` and `totalLen`.
|
|
557
573
|
|
|
574
|
+
**`appendFile` is the one write that cannot be all-or-nothing.** Steps 1–3 give `writeFile`,
|
|
575
|
+
`writeFileExclusive` and `writeFileGuarded` their guarantee by building the complete file beside the target
|
|
576
|
+
and swapping it in. An append has no such swap: it extends the existing target **in place**, so there is a
|
|
577
|
+
moment after which the target has changed and the instruction has not finished. What POSIX guarantees for an
|
|
578
|
+
`O_APPEND` write is that the position is taken at end-of-file atomically; it does not promise that the payload
|
|
579
|
+
lands in one piece. This protocol therefore asks you to treat `PIPE_BUF` bytes (the POSIX minimum is 512; Linux
|
|
580
|
+
uses 4096) as the largest append you may regard as indivisible — the conservative bound, borrowed from the
|
|
581
|
+
rule POSIX states for pipe writes — and to assume that anything larger can reach the file in several writes,
|
|
582
|
+
interleave with another writer's appends, or stop part-way. So what you answer depends on *when* you fail:
|
|
583
|
+
|
|
584
|
+
- **The target cannot have been touched yet** — the payload failed its length or digest check, the target
|
|
585
|
+
could not be opened, your own containment policy refused the path. Answer the ordinary code
|
|
586
|
+
(`permission_denied`, `not_found`, `is_directory`, `invalid`, …). Nothing changed, so the caller may safely
|
|
587
|
+
retry.
|
|
588
|
+
- **The target may already have been changed** — a short write, an I/O error part-way through, or the process
|
|
589
|
+
or the machine going away while the append was running, so that you cannot say how much landed. Answer
|
|
590
|
+
`outcome_unknown` (§3.3). Never answer `timeout` or `transport_lost` here: both are retryable, and a retry
|
|
591
|
+
appends the content a second time.
|
|
592
|
+
- **The whole payload was appended.** Answer `{ok:true, kind:"void"}`. Do not report success for a partial
|
|
593
|
+
append.
|
|
594
|
+
|
|
595
|
+
The server settles an executor-reported `outcome_unknown` exactly like one it reaches by its own deadlines:
|
|
596
|
+
the caller gets the code `outcome_unknown`, which nothing upstream retries, and your message is kept and
|
|
597
|
+
followed by a fixed sentence telling the model to verify the file's contents before running the append again.
|
|
598
|
+
So make your message specific about what you know (bytes written, where it stopped).
|
|
599
|
+
|
|
558
600
|
### 3.5 `writeFileGuarded` and its `expect`
|
|
559
601
|
|
|
560
602
|
`expect` is `{ canonicalPath: string, fileId?: string, exclusive?: boolean, noFollow?: boolean }`:
|
|
@@ -605,7 +647,7 @@ server delivers in order and buffers out-of-order chunks; it acks with `chunkAck
|
|
|
605
647
|
**cumulative** watermark meaning "everything up to and including this is accepted" (`-1` = nothing yet).
|
|
606
648
|
You may release buffers at or below the watermark.
|
|
607
649
|
|
|
608
|
-
- A gap that is not filled within the deployment's gap timeout (default 30 s) **fails the instruction**
|
|
650
|
+
- A gap that is not filled within the deployment's gap timeout (`helloAck.chunkGapTimeoutMs`, default 30 s) **fails the instruction**
|
|
609
651
|
with `outcome_unknown` — output is never silently truncated.
|
|
610
652
|
- The reorder buffer is bounded (default 64 entries / 2 MiB decoded). Running far ahead of the watermark
|
|
611
653
|
overflows it and also fails the instruction.
|
|
@@ -711,7 +753,7 @@ process — that work is accounted for as "may have run".
|
|
|
711
753
|
|
|
712
754
|
**R4 — no reconnect within the grace ⇒ the books close.**
|
|
713
755
|
*Given* the socket dropped with `I` in flight, *when* no connection for this device is established within
|
|
714
|
-
the reconnect grace (default 120 s), *then* `I` settles `outcome_unknown`; a later reconnect does not list
|
|
756
|
+
the reconnect grace (`helloAck.reconnectGraceMs`, default 120 s), *then* `I` settles `outcome_unknown`; a later reconnect does not list
|
|
715
757
|
it, and its `result` is either refused or memo-acked without being believed (§2.4).
|
|
716
758
|
*Executor:* reconnect promptly. If you were offline longer than the grace, your results are no longer
|
|
717
759
|
deliverable — that is not a licence to re-run anything.
|
|
@@ -742,7 +784,7 @@ as the original — the first copy the server buffered is the one it will delive
|
|
|
742
784
|
|
|
743
785
|
**R8 — a gap is a failure, not a truncation.**
|
|
744
786
|
*Given* the server is waiting for `streamSeq k`, *when* `k` does not arrive within the gap timeout
|
|
745
|
-
(default 30 s) or the reorder buffer overflows (default 64 entries / 2 MiB), *then* `I` fails with
|
|
787
|
+
(`helloAck.chunkGapTimeoutMs`, default 30 s) or the reorder buffer overflows (default 64 entries / 2 MiB), *then* `I` fails with
|
|
746
788
|
`outcome_unknown` — the command already ran, so the result cannot be faithfully reported.
|
|
747
789
|
*Executor:* never skip a `streamSeq`, never renumber after a reconnect, never "compact" a stream.
|
|
748
790
|
|
|
@@ -766,18 +808,18 @@ machine from taking over the lease mid-command; only you can.
|
|
|
766
808
|
|
|
767
809
|
**R11 — `cancel` demands a terminal, within a grace.**
|
|
768
810
|
*Given* the server sent `cancel` for `I`, *when* you do not send a terminal `result` within the cancel
|
|
769
|
-
grace (default 10 s, restarted each time the cancel is delivered), *then* `I` settles `outcome_unknown`.
|
|
811
|
+
grace (`helloAck.cancelGraceMs`, default 10 s, restarted each time the cancel is delivered), *then* `I` settles `outcome_unknown`.
|
|
770
812
|
`cancel` is idempotent, is delivered at most once per generation, and **is replayed on the first active
|
|
771
813
|
tick after a same-epoch reconnect** (a cancel written to a socket that then dropped may never have
|
|
772
814
|
arrived).
|
|
773
815
|
*Executor:* treat a repeated `cancel` for the same `I` as a no-op. Answer exactly one terminal:
|
|
774
816
|
`{ok:true, kind:"cancelled", state:"not_started"}` if the work had not begun, `state:"killed"` if you
|
|
775
|
-
killed it, or the work's ordinary outcome if it had already finished. Answer promptly — 10 s is the budget.
|
|
817
|
+
killed it, or the work's ordinary outcome if it had already finished. Answer promptly — `cancelGraceMs` (default 10 s) is the budget.
|
|
776
818
|
|
|
777
819
|
**R12 — two-layer timeout: yours is authoritative, the server's is a backstop.**
|
|
778
820
|
*Given* an instruction carrying `timeoutMs`, *when* that budget elapses, *then* you must kill the work and
|
|
779
821
|
answer `{ok:false, errorCode:"timeout"}` — that is the only path to the retryable `timeout` word. *When*
|
|
780
|
-
you answer nothing, the server gives up at `timeoutMs` +
|
|
822
|
+
you answer nothing, the server gives up at `timeoutMs` + `helloAck.executionCeilingGraceMs` (default 30 s) and settles
|
|
781
823
|
`outcome_unknown`; a later result for `I` is then undeliverable.
|
|
782
824
|
*Executor:* enforce `timeoutMs` yourself, and prefer answering late-but-honestly over not answering.
|
|
783
825
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.120.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",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@sema-agent/core": "7.
|
|
67
|
+
"@sema-agent/core": "7.46.0",
|
|
68
68
|
"@sema-agent/settings-schema": "8.0.0",
|
|
69
69
|
"e2b": "^2.28.0",
|
|
70
70
|
"libsodium-wrappers": "^0.8.4",
|