@sema-agent/server 7.81.0 → 7.82.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 +27 -0
- package/dist/boot/memory-consolidation.d.ts +38 -0
- package/dist/boot/memory-consolidation.js +14 -0
- package/dist/boot/runner-deps.d.ts +11 -0
- package/dist/boot/runner-deps.js +15 -0
- package/dist/boot/stage-06-runners.js +13 -8
- package/dist/config-catalog.js +1 -0
- package/dist/config-types.d.ts +26 -0
- package/dist/config.js +9 -1
- package/dist/observability/metrics.js +1 -0
- package/dist/plugins/posix-shell-fs.d.ts +2 -1
- package/dist/plugins/posix-shell-fs.js +4 -1
- package/dist/plugins/remote-env-adb.d.ts +5 -1
- package/dist/plugins/remote-env-adb.js +3 -0
- package/dist/plugins/remote-env-e2b.d.ts +8 -1
- package/dist/plugins/remote-env-e2b.js +4 -1
- package/dist/plugins/remote-env-host.d.ts +15 -1
- package/dist/plugins/remote-env-host.js +37 -1
- package/dist/plugins/remote-env-k8s.d.ts +5 -1
- package/dist/plugins/remote-env-k8s.js +3 -0
- package/dist/plugins/remote-env-local-docker.d.ts +5 -1
- package/dist/plugins/remote-env-local-docker.js +3 -0
- package/dist/plugins/remote-env-ssh.d.ts +5 -1
- package/dist/plugins/remote-env-ssh.js +3 -0
- package/dist/plugins/remote-shell.d.ts +78 -1
- package/dist/plugins/remote-shell.js +35 -0
- package/dist/trace/core-keyset-guard.d.ts +6 -1
- package/dist/trace/project.d.ts +18 -0
- package/dist/trace/project.js +11 -0
- package/package.json +2 -2
package/USAGE.md
CHANGED
|
@@ -1137,6 +1137,33 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
|
|
|
1137
1137
|
> ④**模型座位解析不出来**——既没有显式 chat 席,也没有 `roles.consolidate` / `roles.summarize`(含档位表的
|
|
1138
1138
|
> `flash` 绑定)。core **刻意不回落主模型**(整库蒸馏不该悄悄骑最贵的席位),本服务照抄那条判断,不在下游
|
|
1139
1139
|
> 补一个兜底席。
|
|
1140
|
+
> - `MEMORY_AUTO_CONSOLIDATION=on|off`(**未设 = 读引擎缺省**,见下):记忆**自动整理**的部署席
|
|
1141
|
+
> (core 7.21.0 的 `RunnerDeps.autoRunOnRecommendation` 席)。武装 ⇒ **每个产生了整理建议的任务
|
|
1142
|
+
> 之后**,为该 scope 起**一次**整库整理跑 —— 🔴 **记忆内容自动外流到配置的整理模型**。
|
|
1143
|
+
> **三态,别读成布尔**:
|
|
1144
|
+
> - **未设** = 不写引擎那个键 ⇒ 读引擎的具名缺省,而**本版装的引擎那个缺省是「开」**。
|
|
1145
|
+
> ⇒ 🔴 **不配这根旋钮 = 武装**(在下面那条前置满足时)。缺省的属主在引擎侧、会随提货漂,所以
|
|
1146
|
+
> 「什么都不配」这一态的含义**不是稳定的**;要一个稳定的答案就显式写一个词。
|
|
1147
|
+
> - `off` = **显式关,胜过缺省** —— 这是**关闭口**。不想让记忆自动外流的部署,靠它在 core 把缺省翻开
|
|
1148
|
+
> 之后仍然保持关闭。**这是本旋钮存在的理由。**
|
|
1149
|
+
> - `on` = 武装。**设之前请先读**「这是出口决定不是性能旋钮」那一句:一轮 consolidation 读**全库**
|
|
1150
|
+
> (~1e5 prompt tokens)并把内容交给整理模型,既是钱更是**数据出境**。
|
|
1151
|
+
> ⚠️ **闭集两词,别的一律拒启**(与上一根同一条纪律,方向在这根上更要紧:一个被读成「未设」的拼错关词
|
|
1152
|
+
> 会让那台部署跟着 core 的新缺省**静默武装**,而运维手里握着一份自以为关掉了的配置)。
|
|
1153
|
+
> 🔴 **前置 = 上一根旋钮**:引擎面的整理两席(协议席 + 模型席)在场 **iff** `MEMORY_CONSOLIDATION_DRIVER=on`
|
|
1154
|
+
> 且记忆引擎接了线 —— 两席与那两个 operator 口**共用同一只已解析座**(不做第二份装配)。
|
|
1155
|
+
> · 阀门关着 ⇒ 两席都不写键,引擎那半场**逐字节不变**;此时 `MEMORY_AUTO_CONSOLIDATION=on`
|
|
1156
|
+
> **启动期响亮拒**(core 对「武装但没接整理席」是**每一次 prepare 都抛** `config.auto_consolidation`,
|
|
1157
|
+
> 留到运行期就是一台**启动成功、每个任务都死**的机器)。
|
|
1158
|
+
> · 阀门开着 ⇒ 两席齐 ⇒ 缺省(开)当场生效。
|
|
1159
|
+
> 🔴🔴 **升级影响**:**升级前就配了 `MEMORY_CONSOLIDATION_DRIVER=on` 的部署,升到本版之后每个产生了
|
|
1160
|
+
> 整理建议的任务之后会自动跑一轮整库整理**(升级前那是只能由人按的手动阀门)。不想要就**升级前**显式写
|
|
1161
|
+
> `MEMORY_AUTO_CONSOLIDATION=off` —— **不配不等于关**。启动日志 `memory_consolidation_driver_armed`
|
|
1162
|
+
> 带一位 `autoRunOnRecommendation`(三态原样),用它核这台机器是哪一态。
|
|
1163
|
+
> **两根的分工**:上一根决定**有没有**整理能力,这一根决定那个能力**要不要自动触发**;关掉自动整理,
|
|
1164
|
+
> 手动阀门照常可用。
|
|
1165
|
+
> 未武装的部署,`wiring_manifest` 与它的 `configFingerprint` 对 7.20.1 **逐字节相同**;武装的部署,新段
|
|
1166
|
+
> `autoConsolidation: { onRecommendation: true }` **只出现在 operator 帧上**(契约附录 G.13)。
|
|
1140
1167
|
> - `MEMORY_CONSOLIDATION_SCOPES=user:local[,org:acme]`:阀门的 scope 表 = 这台 worker **声明**它会折叠哪些库。
|
|
1141
1168
|
> **恰好一条**时它是两个口省略 `scope` 的唯一缺省;零条或多条时省略 `scope` 一律 400(在两个库之间替人挑
|
|
1142
1169
|
> 一个去花全库模型钱是本设计要消灭的形)。**表非空时显式 scope 必须在表里**,否则 400 —— 一个陈旧或敲错
|
|
@@ -138,6 +138,44 @@ export declare function buildStopDetailAudit(input: {
|
|
|
138
138
|
outcome: string;
|
|
139
139
|
untrustedDetail: string;
|
|
140
140
|
};
|
|
141
|
+
/**
|
|
142
|
+
* S-362 / S-399 —— **`RunnerDeps.autoRunOnRecommendation` 武装席的装配不变量**(D1 的姊妹门)。
|
|
143
|
+
*
|
|
144
|
+
* 与 {@link assertConsolidationDriverWirable} 刻意**分家**而不是加第四臂:那道门问「阀门这根旋钮
|
|
145
|
+
* (`MEMORY_CONSOLIDATION_DRIVER`)开着而这台机器结构上跑不了」,本门问「武装这根旋钮
|
|
146
|
+
* (`MEMORY_AUTO_CONSOLIDATION`)开着而引擎那半场没有整理席」。两句诊断指向的动作不同,合成一道门就会
|
|
147
|
+
* 指错方向。
|
|
148
|
+
*
|
|
149
|
+
* ## 为什么是**拒启**而不是让 core 在 prepare 抛
|
|
150
|
+
*
|
|
151
|
+
* core 对「`autoRunOnRecommendation: true` 但没接整理席」的处置是**每一次 prepare 都抛**
|
|
152
|
+
* `config.auto_consolidation`(带码错,不是通知;逐字见 core `RunnerDeps.autoRunOnRecommendation` 的 d.ts)。
|
|
153
|
+
* 留到运行期买到的是一台**启动成功、每一个任务都死**的 worker —— 比阀门那三条(「启动成功、什么都没
|
|
154
|
+
* 折叠」)更坏。所以按 D1 的同一条(**安全/成本控件不得半开**)提前到启动期,并把**该去拧哪根**写进句子。
|
|
155
|
+
*
|
|
156
|
+
* ## 判据取的是**结构事实**,不是一个手抄的布尔
|
|
157
|
+
*
|
|
158
|
+
* `memoryConsolidationWired` 由调用点从**它刚刚铸出来的那只 `RunnerDeps`** 上读(`deps.memoryConsolidation
|
|
159
|
+
* !== undefined`),不是在这里写一个常量 —— S-399 把两席接上之后,本门**自动**从「恒拒」变成「阀门开着
|
|
160
|
+
* 就放行」,没有人需要记得回来改一个数。
|
|
161
|
+
*
|
|
162
|
+
* 🔴 **core 的拒绝谓词是两个析取项,本门只覆盖第一个**(对抗复审 F5 [low],验真后如实登记):
|
|
163
|
+
* core d.ts 逐字「`true` with nothing to run (**no `RunnerDeps.memoryConsolidation`, or no resolvable
|
|
164
|
+
* driver model seat**), or a non-boolean, is refused at prepare」。第二项(模型席解不出)本门不判,
|
|
165
|
+
* 而这在本仓**结构上不可达**:两席是**同一只已解析座**一起写上去的(`boot/runner-deps.ts` 的
|
|
166
|
+
* `ctx.consolidationSeat` 那一段)——座解析不出来时 {@link resolveConsolidationDriverSeat} 早在
|
|
167
|
+
* D1 那道门上就**拒启**了,轮不到这里。哪天有人把两席拆开各走各的路,这条「不可达」就失效,那时
|
|
168
|
+
* 本门要补第二项(core 侧的判据是 `consolidationSeatResolvable`,**不从包根导出**,届时要么向 core
|
|
169
|
+
* 要导出,要么在本层复现 —— 这一句留在这里就是为了那一天不必重新推一遍)。
|
|
170
|
+
*
|
|
171
|
+
* 未武装(`undefined` / `false`)⇒ 整条门不判。
|
|
172
|
+
*/
|
|
173
|
+
export declare function assertAutoConsolidationWirable(input: {
|
|
174
|
+
/** 部署席的三态读数(`config.memoryConsolidationDriver.autoRunOnRecommendation`)。 */
|
|
175
|
+
armed: boolean | undefined;
|
|
176
|
+
/** 铸好的 `RunnerDeps` 上**真的**有整理席吗(`deps.memoryConsolidation !== undefined`)。 */
|
|
177
|
+
memoryConsolidationWired: boolean;
|
|
178
|
+
}): void;
|
|
141
179
|
/** 两个 admin 口消费的窄能力面(`ServiceDeps.memoryConsolidation` 的实参来源)。 */
|
|
142
180
|
export interface MemoryConsolidationFaces {
|
|
143
181
|
/** D3 的座位自证(投影用;boot 期解析一次,restart-to-apply)。 */
|
|
@@ -107,6 +107,20 @@ export function buildStopDetailAudit(input) {
|
|
|
107
107
|
untrustedDetail: redactSecrets(input.stopDetail).slice(0, STOP_DETAIL_LOG_MAX),
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
+
export function assertAutoConsolidationWirable(input) {
|
|
111
|
+
if (input.armed !== true)
|
|
112
|
+
return;
|
|
113
|
+
if (input.memoryConsolidationWired)
|
|
114
|
+
return;
|
|
115
|
+
throw new Error(`MEMORY_AUTO_CONSOLIDATION=on arms automatic memory consolidation (a whole-library fold after every task that ` +
|
|
116
|
+
`recommended one — memory content EGRESSES to the configured consolidation model), but this deployment wires NO ` +
|
|
117
|
+
`engine-side consolidation seat (RunnerDeps.memoryConsolidation). The engine refuses that combination at EVERY ` +
|
|
118
|
+
`prepare (config.auto_consolidation), so this worker would boot fine and then fail every single task. ` +
|
|
119
|
+
`The engine-side seats are wired IFF MEMORY_CONSOLIDATION_DRIVER=on and a memory engine is wired — so either ` +
|
|
120
|
+
`set MEMORY_CONSOLIDATION_DRIVER=on (which also mounts the manual valve, and note that BOTH knobs then bill ` +
|
|
121
|
+
`whole-library model calls), or set MEMORY_AUTO_CONSOLIDATION=off (unsetting it is NOT the same: the engine's ` +
|
|
122
|
+
`named default is ON as of core 7.21.1).`);
|
|
123
|
+
}
|
|
110
124
|
export function createMemoryConsolidationFaces(store, input) {
|
|
111
125
|
const engine = new MemoryEngine({
|
|
112
126
|
backend: store.backend,
|
|
@@ -17,6 +17,7 @@ import type { ElicitationCoordinator } from "../elicitation.js";
|
|
|
17
17
|
import { FleetEventBus } from "../fleet/fleet-bus.js";
|
|
18
18
|
import type { Logger } from "../observability/logger.js";
|
|
19
19
|
import type { Metrics } from "../observability/metrics.js";
|
|
20
|
+
import { type ConsolidationDriverSeat } from "./memory-consolidation.js";
|
|
20
21
|
import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
|
|
21
22
|
import { type WorkflowCompletionInbox } from "../orchestration/workflow-completion-inbox.js";
|
|
22
23
|
import { type EngineNoticeRouter } from "../trace/engine-notice-wire.js";
|
|
@@ -148,6 +149,16 @@ export interface RunnerDepsCtx {
|
|
|
148
149
|
backend: MemoryBackend;
|
|
149
150
|
root: string;
|
|
150
151
|
} | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* S-399(core 7.21.1)—— 记忆整理的**已解析座**(`boot/memory-consolidation.ts` 的
|
|
154
|
+
* `resolveConsolidationDriverSeat` 产物),在场 **iff** `MEMORY_CONSOLIDATION_DRIVER=on` 且记忆引擎接了线。
|
|
155
|
+
*
|
|
156
|
+
* 🔴 **一座两用,不是两次装配**:同一只座同时喂 admin 阀门的操作面引擎与这里的引擎面两席
|
|
157
|
+
* (`RunnerDeps.memoryConsolidation` + `memoryConsolidationDriver`)。第二次解析会让「谁在花钱」
|
|
158
|
+
* 在两条腿上各答一遍,而座是 boot 期定的(restart-to-apply,理由在该模块顶注)。
|
|
159
|
+
* 缺席 ⇒ 两席都**不写键** ⇒ 引擎那半场对 7.20.x 逐字节不变。
|
|
160
|
+
*/
|
|
161
|
+
consolidationSeat: ConsolidationDriverSeat | undefined;
|
|
151
162
|
/** [ref]②([ref] §2.1b,core 7.0.2 [ref] 件2)/ S-215:会话 capture opt-out 记录的**按平面取用源**
|
|
152
163
|
* (`backend.sessionCaptureRecords()`,main.ts 取**一次**、与四只 operator 面引擎同一只源 —— 全站同批
|
|
153
164
|
* 注入律,机器钉 = test/memory-capture-optout-lane.test.ts ②)。在场 ⇒ `capture:"off"` 声明与中途翻转
|
package/dist/boot/runner-deps.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { listCollabWorkflows, resolveCollabWorkflow } from "../capabilities/collab-workflows.js";
|
|
4
4
|
import { fleetBackgroundChildPublisher, FleetEventBus } from "../fleet/fleet-bus.js";
|
|
5
5
|
import { createMemoryEngineIncidentSink } from "../memory-incident-sink.js";
|
|
6
|
+
import { assertAutoConsolidationWirable } from "./memory-consolidation.js";
|
|
6
7
|
import { createHardenedVmRunner } from "../orchestration/hardened-vm-runner.js";
|
|
7
8
|
import { createWorkerHardenedVmRunner } from "../orchestration/hardened-vm-worker-runner.js";
|
|
8
9
|
import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
|
|
@@ -274,9 +275,23 @@ export function createRunnerDeps(ctx) {
|
|
|
274
275
|
});
|
|
275
276
|
return;
|
|
276
277
|
}
|
|
278
|
+
metrics.inc("runner_error_total", { phase: ctx.phase });
|
|
277
279
|
logger.error("runner_error", { phase: ctx.phase, sessionId: ctx.sessionId, err: String(err) });
|
|
278
280
|
},
|
|
281
|
+
...(ctx.consolidationSeat !== undefined
|
|
282
|
+
? {
|
|
283
|
+
memoryConsolidation: {},
|
|
284
|
+
memoryConsolidationDriver: { chat: ctx.consolidationSeat.options.chat, model: ctx.consolidationSeat.options.model },
|
|
285
|
+
}
|
|
286
|
+
: {}),
|
|
287
|
+
...(config.memoryConsolidationDriver.autoRunOnRecommendation !== undefined
|
|
288
|
+
? { autoRunOnRecommendation: config.memoryConsolidationDriver.autoRunOnRecommendation }
|
|
289
|
+
: {}),
|
|
279
290
|
};
|
|
291
|
+
assertAutoConsolidationWirable({
|
|
292
|
+
armed: config.memoryConsolidationDriver.autoRunOnRecommendation,
|
|
293
|
+
memoryConsolidationWired: runnerDeps.memoryConsolidation !== undefined,
|
|
294
|
+
});
|
|
280
295
|
return runnerDeps;
|
|
281
296
|
}
|
|
282
297
|
//# sourceMappingURL=runner-deps.js.map
|
|
@@ -62,15 +62,18 @@ export function runStage06(ctx) {
|
|
|
62
62
|
memoryEngineBackend: config.memoryEngineBackend,
|
|
63
63
|
...(config.memoryProvenance !== undefined ? { provenance: config.memoryProvenance } : {}),
|
|
64
64
|
});
|
|
65
|
-
const
|
|
65
|
+
const consolidationSeat = config.memoryConsolidationDriver.enabled && memoryEngine
|
|
66
|
+
? resolveConsolidationDriverSeat({
|
|
67
|
+
brain,
|
|
68
|
+
models: config.models,
|
|
69
|
+
tiers: config.tiers,
|
|
70
|
+
roles: config.roles,
|
|
71
|
+
getApiKeyAndHeaders: createPerModelAuthSeat(() => configCenter.getKeyResolver()),
|
|
72
|
+
})
|
|
73
|
+
: undefined;
|
|
74
|
+
const memoryConsolidationFaces = consolidationSeat && memoryEngine
|
|
66
75
|
? createMemoryConsolidationFaces(memoryEngine, {
|
|
67
|
-
seat:
|
|
68
|
-
brain,
|
|
69
|
-
models: config.models,
|
|
70
|
-
tiers: config.tiers,
|
|
71
|
-
roles: config.roles,
|
|
72
|
-
getApiKeyAndHeaders: createPerModelAuthSeat(() => configCenter.getKeyResolver()),
|
|
73
|
-
}),
|
|
76
|
+
seat: consolidationSeat,
|
|
74
77
|
scopes: config.memoryConsolidationDriver.scopes,
|
|
75
78
|
...(config.memoryProvenance !== undefined ? { provenance: config.memoryProvenance } : {}),
|
|
76
79
|
onIncident: (err) => logger.warn("memory_consolidation_incident", { code: err.code ?? null, detail: err.message }),
|
|
@@ -92,6 +95,7 @@ export function runStage06(ctx) {
|
|
|
92
95
|
model: memoryConsolidationFaces.model,
|
|
93
96
|
scopes: memoryConsolidationFaces.scopes,
|
|
94
97
|
periodic: false,
|
|
98
|
+
autoRunOnRecommendation: config.memoryConsolidationDriver.autoRunOnRecommendation ?? "engine-default(on)",
|
|
95
99
|
});
|
|
96
100
|
}
|
|
97
101
|
const governanceSeams = createGovernanceSeams({
|
|
@@ -108,6 +112,7 @@ export function runStage06(ctx) {
|
|
|
108
112
|
const workflowModelAllowlistSnapshot = workflowModelAllowlistFor(config) ?? [];
|
|
109
113
|
const sessionShellGateRegistry = createSessionShellGateRegistry();
|
|
110
114
|
const runnerDeps = createRunnerDeps({
|
|
115
|
+
consolidationSeat,
|
|
111
116
|
hands: commitHands,
|
|
112
117
|
mcpRevocations,
|
|
113
118
|
sessionShellGateExplicitlyOff: sessionShellGateRegistry.explicitlyOff,
|
package/dist/config-catalog.js
CHANGED
|
@@ -213,6 +213,7 @@ export const CONFIG_CATALOG = [
|
|
|
213
213
|
r("MCP_ELICITATION_MAX_TOTAL", "approval", "number", "elicitation 每 run 总量帽", { derivedDefaultNote: "缺席 ⇒ core 出厂窗" }),
|
|
214
214
|
r("MCP_ELICITATION_MIN_INTERVAL_MS", "approval", "number", "elicitation per-server 最小间隔(ms)", { derivedDefaultNote: "缺席 ⇒ core 出厂窗" }),
|
|
215
215
|
r("MCP_ELICITATION_TTL_MS", "approval", "number", "elicitation 无人应答释放窗(ms)", { derivedDefaultNote: "缺席 ⇒ core 出厂窗" }),
|
|
216
|
+
r("MEMORY_AUTO_CONSOLIDATION", "memory", "enum", "记忆自动整理部署席(on=每个建议任务后整库整理跑,记忆内容自动外流到整理模型;off=显式关,胜过引擎缺省;未设=读引擎具名缺省)", { enumValues: ["on", "off"], derivedDefaultNote: "缺席 ⇒ 引擎具名缺省(**本版装的引擎是开**)——不配不等于关;前置=MEMORY_CONSOLIDATION_DRIVER=on", danger: SEC, resolve: (c) => c.memoryConsolidationDriver.autoRunOnRecommendation, booleanEnum: { true: "on", false: "off" } }),
|
|
216
217
|
r("MEMORY_CAPTURE_POLICY", "memory", "enum", "记忆 capture 姿态(open=声明按面值;governed=按 per-principal verdict;capture-required=恒采)", { enumValues: ["open", "governed", "capture-required"], staticDefault: "open", danger: SEC, configKey: "memoryCapturePolicy" }),
|
|
217
218
|
r("MEMORY_CONSOLIDATION_DRIVER", "memory", "enum", "记忆折叠阀门(off=零装配零探针,两 admin 口 404 族)", { enumValues: ["on", "off"], staticDefault: "off", danger: BILL, resolve: (c) => c.memoryConsolidationDriver.enabled, booleanEnum: { true: "on", false: "off" } }),
|
|
218
219
|
r("MEMORY_CONSOLIDATION_INTERVAL_SEC", "memory", "number", "折叠周期腿节律(秒;本 build 任何正值拒启 —— 周期腿未落地,恒 0)", { staticDefault: "0" }),
|
package/dist/config-types.d.ts
CHANGED
|
@@ -140,6 +140,32 @@ export interface MemoryConsolidationDriverConfig {
|
|
|
140
140
|
* `src/memory-operator-faces.ts` 的 `erasureEnvelopeRefusal` 头注),在本层拿它当白名单就是**发明
|
|
141
141
|
* 第二套授权语义**。显式传的 scope 照跑,表只管缺省。 */
|
|
142
142
|
scopes: readonly string[];
|
|
143
|
+
/**
|
|
144
|
+
* S-362 / core 7.21.0 [ref] —— **`RunnerDeps.autoRunOnRecommendation` 的部署席**
|
|
145
|
+
* (env `MEMORY_AUTO_CONSOLIDATION=on|off`;**缺席 = 不写那个键**,由 core 的具名缺省
|
|
146
|
+
* `AUTO_RUN_ON_RECOMMENDATION_DEFAULT` 作答)。
|
|
147
|
+
*
|
|
148
|
+
* 🔴 **三态,不是布尔**,而且三态各有各的意思 —— 折成布尔会当场丢掉本旋钮存在的**唯一**理由:
|
|
149
|
+
* · `undefined`(env 未设)= 不写 `RunnerDeps` 那个键 ⇒ 读 core 的具名缺省。core 7.21.0 发的是
|
|
150
|
+
* `false`,而 clay [ref] 已裁**缺省开**、core 7.21.1 翻常量 —— 所以这一格的取值**会随提货漂**,
|
|
151
|
+
* 这正是它该有的样子(缺省的属主在 core)。
|
|
152
|
+
* · `false`(`MEMORY_AUTO_CONSOLIDATION=off`)= **显式关,胜过缺省**。这是本旋钮的**关闭口**:
|
|
153
|
+
* 一台不愿意让记忆内容自动外流的部署,靠它在 core 把缺省翻开之后仍然保持关闭。
|
|
154
|
+
* · `true`(`=on`)= 武装。**武装是一条出口决定,不是性能旋钮**:每个建议任务之后会有一次整库整理跑,
|
|
155
|
+
* 记忆内容自动外流到配置的整理模型 —— 设它之前请读 `boot/memory-consolidation.ts` 的 EGRESS 段。
|
|
156
|
+
*
|
|
157
|
+
* ⚠️ **前置 = `MEMORY_CONSOLIDATION_DRIVER=on`**(且记忆引擎接了线)。阀门开着 ⇒ 引擎那半场的整理两席
|
|
158
|
+
* (`RunnerDeps.memoryConsolidation` + `memoryConsolidationDriver`)由**同一只已解析座**接上
|
|
159
|
+
* (S-399,`boot/runner-deps.ts` 的 `ctx.consolidationSeat` 那一段)⇒ 本旋钮真的生效;阀门关着 ⇒ 两席
|
|
160
|
+
* 一个键都不写,而 core 对「武装但没接整理席」的处置是**每一次 prepare 都抛** `config.auto_consolidation`
|
|
161
|
+
* —— 那会买到一台**启动成功、每个任务都死**的机器,所以本仓把那一拒提前到启动期响亮拒
|
|
162
|
+
* (`assertAutoConsolidationWirable`,读的是刚铸出来的那只 deps 而不是一个手抄的布尔),与
|
|
163
|
+
* `MEMORY_CONSOLIDATION_DRIVER` 的 D1「安全/成本控件不得半开」同一条。
|
|
164
|
+
* 🔴 **本段自 S-399 改判**:S-362 那一批这里写的是「本 build 上 `on` 一定拒启(本仓没接整理席)」——
|
|
165
|
+
* 同一批的后半场把两席接上之后,那句话成了谎。旧世界断言留在树上比欠账更危险(7.81.0 整批的教训),
|
|
166
|
+
* 所以真相变了就地改,不留「将来某一版」的口气。
|
|
167
|
+
*/
|
|
168
|
+
autoRunOnRecommendation?: boolean;
|
|
143
169
|
}
|
|
144
170
|
/** A sema-registry MCP server resolved to a core spec (env-NAME refs already → real values) plus the
|
|
145
171
|
* scenarios it applies to (empty = all). `resolveSpec` filters by scenario and passes `spec` to core. */
|
package/dist/config.js
CHANGED
|
@@ -1349,6 +1349,14 @@ function parseMemoryConsolidationDriver() {
|
|
|
1349
1349
|
`spellings to preserve, unlike MEMORY_ENGINE's two-family word table.`);
|
|
1350
1350
|
}
|
|
1351
1351
|
const enabled = word === "on";
|
|
1352
|
+
const autoWord = requireNonBlank("MEMORY_AUTO_CONSOLIDATION", process.env.MEMORY_AUTO_CONSOLIDATION);
|
|
1353
|
+
if (autoWord !== undefined && autoWord !== "on" && autoWord !== "off") {
|
|
1354
|
+
throw new Error(`MEMORY_AUTO_CONSOLIDATION must be exactly "on" or "off", got ${JSON.stringify(autoWord)} — this knob decides whether memory content ` +
|
|
1355
|
+
`EGRESSES to the consolidation model automatically after every recommending task. A misspelled word read as "unset" would hand the ` +
|
|
1356
|
+
`decision back to the engine's named default (which core is flipping to ON), so an operator who believes they turned it off would be ` +
|
|
1357
|
+
`exporting their users' memory. UNSET it to take the engine default, or spell it exactly.`);
|
|
1358
|
+
}
|
|
1359
|
+
const autoRunOnRecommendation = autoWord === undefined ? undefined : autoWord === "on";
|
|
1352
1360
|
let intervalSec = 0;
|
|
1353
1361
|
const intervalRaw = requireNonBlank("MEMORY_CONSOLIDATION_INTERVAL_SEC", process.env.MEMORY_CONSOLIDATION_INTERVAL_SEC);
|
|
1354
1362
|
if (intervalRaw !== undefined) {
|
|
@@ -1377,7 +1385,7 @@ function parseMemoryConsolidationDriver() {
|
|
|
1377
1385
|
`the consolidation valve is OFF, so neither knob reaches anything (the two admin endpoints are not even mounted). ` +
|
|
1378
1386
|
`Set MEMORY_CONSOLIDATION_DRIVER=on, or unset the other two.`);
|
|
1379
1387
|
}
|
|
1380
|
-
return { enabled, intervalSec, scopes };
|
|
1388
|
+
return { enabled, intervalSec, scopes, ...(autoRunOnRecommendation !== undefined ? { autoRunOnRecommendation } : {}) };
|
|
1381
1389
|
}
|
|
1382
1390
|
function parseMemoryDomain(ctx) {
|
|
1383
1391
|
const { requirePrincipal } = ctx;
|
|
@@ -229,6 +229,7 @@ export function createMetrics() {
|
|
|
229
229
|
m.histogram("plan_cache_qsig", "Significant-token count of task objectives (qSig length)", [1, 2, 3, 5, 10, 20]);
|
|
230
230
|
m.counter("memory_consolidation_ops_total", "Memory consolidation store ops (1.62 B-full), by op (update/delete)");
|
|
231
231
|
m.counter("mcp_server_unavailable_total", "MCP servers skipped fail-open at task start (1.68), unreachable/misconfigured");
|
|
232
|
+
m.counter("runner_error_total", "RunnerDeps.onError frames that no phase arm claimed, by core phase (hook / compaction / interrupt-reconcile / suggestions)");
|
|
232
233
|
m.counter("a2a_peer_unavailable_total", "A2A peers skipped at task start (DESIGN-269 client leg): card unreachable / spec unusable");
|
|
233
234
|
m.counter("a2a_serve_rpc_total", "A2A server-as-peer JSON-RPC calls by method and outcome (DESIGN-269 车2)");
|
|
234
235
|
m.counter("a2a_serve_rejected_total", "A2A server-as-peer requests refused before reaching a method handler, by reason (DESIGN-269 车2)");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FileError, type ExecutionError, type RemoteExecutionError, type FileInfo, type Result } from "@sema-agent/core";
|
|
1
|
+
import { FileError, type ExecutionError, type RemoteExecutionError, type FileInfo, type Result, type SymlinkChain } from "@sema-agent/core";
|
|
2
2
|
export interface ShellRunResult {
|
|
3
3
|
stdout: string;
|
|
4
4
|
stderr: string;
|
|
@@ -14,6 +14,7 @@ export interface PosixShellFsDeps {
|
|
|
14
14
|
}
|
|
15
15
|
export interface PosixShellFs {
|
|
16
16
|
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
17
|
+
canonicalChain(p: string, abortSignal?: AbortSignal): Promise<Result<SymlinkChain, FileError>>;
|
|
17
18
|
canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
18
19
|
statFileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
19
20
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { FileError, } from "@sema-agent/core";
|
|
3
|
-
import { shellQuote, kindFromMode } from "./remote-shell.js";
|
|
3
|
+
import { runSymlinkChain, shellQuote, kindFromMode } from "./remote-shell.js";
|
|
4
4
|
import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
|
|
5
5
|
const ok = (value) => ({ ok: true, value });
|
|
6
6
|
export function createPosixShellFs(deps) {
|
|
@@ -22,6 +22,9 @@ export function createPosixShellFs(deps) {
|
|
|
22
22
|
return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "not_found") };
|
|
23
23
|
return ok(r.value.stdout.trim());
|
|
24
24
|
},
|
|
25
|
+
async canonicalChain(p, abortSignal) {
|
|
26
|
+
return runSymlinkChain(resolve(p), (cmd) => run(cmd, abortSignal));
|
|
27
|
+
},
|
|
25
28
|
async canonicalPath(p, abortSignal) {
|
|
26
29
|
const abs = resolve(p);
|
|
27
30
|
const r = await run(`realpath -- ${shellQuote(abs)}`, abortSignal);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory } from "@sema-agent/core";
|
|
2
|
+
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory } from "@sema-agent/core";
|
|
3
3
|
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
4
4
|
export interface AdbEnvConfig {
|
|
5
5
|
/** Device serial or `host:port` (TCP). `host:port` triggers `adb connect` on connect()/reconnect(). */
|
|
@@ -139,6 +139,10 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
139
139
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
140
140
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
141
141
|
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
142
|
+
/** S-362 / core 7.21.0 [ref] —— **实现**(表态:本腿实现 `canonicalChain`)。一次往返里的一个 `readlink`
|
|
143
|
+
* 循环(上限 64、只锚不折、遇环即止),脚本与读法的单一属主在 `remote-shell.ts`,四条 shell 腿共用同一份。
|
|
144
|
+
* 缺席读法用不上(本腿在场);`ok:false` 由 core 读作「这一次没有跳」,永不是拒绝。 */
|
|
145
|
+
canonicalChain(p: string, abortSignal?: AbortSignal): Promise<Result<SymlinkChain, FileError>>;
|
|
142
146
|
canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
143
147
|
exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
144
148
|
createDir(p: string, options?: {
|
|
@@ -348,6 +348,9 @@ export class RemoteAdbExecutionEnv {
|
|
|
348
348
|
async readLink(p, abortSignal) {
|
|
349
349
|
return this.posixFs.readLink(p, abortSignal);
|
|
350
350
|
}
|
|
351
|
+
async canonicalChain(p, abortSignal) {
|
|
352
|
+
return this.posixFs.canonicalChain(p, abortSignal);
|
|
353
|
+
}
|
|
351
354
|
async canonicalPath(p, abortSignal) {
|
|
352
355
|
return this.posixFs.canonicalPath(p, abortSignal);
|
|
353
356
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* iterator MAY throw `RemoteExecutionError` mid-stream before an `exit` chunk (seam contract, OutputChunk).
|
|
21
21
|
*/
|
|
22
22
|
import { Sandbox, type SandboxOpts, type SandboxConnectOpts } from "e2b";
|
|
23
|
-
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type ExecutionEnvFactory, type VmLifecycleOptions, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll } from "@sema-agent/core";
|
|
23
|
+
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type ExecutionEnvFactory, type VmLifecycleOptions, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll } from "@sema-agent/core";
|
|
24
24
|
/** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
|
|
25
25
|
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
26
26
|
/** Minimal optional observability hooks (kept structural so the adapter stays standalone-testable). */
|
|
@@ -250,6 +250,13 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
|
|
|
250
250
|
* 缺它 ⇒ 合法的悬空链接让整个工具调用失败。必须返回**原始链接目标**(`readlink` 不带 `-f`)。
|
|
251
251
|
* 本腿的文件面也走 `exec`(与 canonicalPath 同通道),所以实现方式与 shell 腿一致。 */
|
|
252
252
|
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
253
|
+
/** S-362 / core 7.21.0 [ref] —— **实现**(表态:本腿实现 `canonicalChain`)。本腿的文件面走原生 SDK 的
|
|
254
|
+
* `exec`([ref] B6 的既有裁决:host 与 e2b 是原生 API 腿,**不并**进 `posix-shell-fs` 基座),但
|
|
255
|
+
* 「链怎么走、怎么读」与四条 shell 腿**是同一份**:脚本与读法的单一属主在 `remote-shell.ts`,这里只多一行
|
|
256
|
+
* 传输。一次往返、上限 64、只锚不折、遇环即止;`ok:false` 由 core 读作「这一次没有跳」,永不是拒绝。
|
|
257
|
+
* 🔴 exit≠0 归 `unknown` 而不是 `not_found`(与本腿 `readLink` 那一格刻意分家,同 `posix-shell-fs` 的理由):
|
|
258
|
+
* 「叶不是软链」与「叶不存在」在这条命令上都是 exit 0 + 空 stdout,所以 exit≠0 一定不是「文件不在」。 */
|
|
259
|
+
canonicalChain(p: string, abortSignal?: AbortSignal): Promise<Result<SymlinkChain, FileError>>;
|
|
253
260
|
canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
254
261
|
exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
255
262
|
createDir(p: string, options?: {
|
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { Sandbox, CommandExitError, FileType } from "e2b";
|
|
3
3
|
import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
4
4
|
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
5
|
-
import { numEnvOr, shellQuote } from "./remote-shell.js";
|
|
5
|
+
import { numEnvOr, runSymlinkChain, shellQuote } from "./remote-shell.js";
|
|
6
6
|
import { BackgroundShellManager } from "./background-shell-support.js";
|
|
7
7
|
const PROVIDER = "e2b";
|
|
8
8
|
const DEFAULT_MOUNT_PATH = "/home/user";
|
|
@@ -692,6 +692,9 @@ export class RemoteContainerExecutionEnv {
|
|
|
692
692
|
return { ok: false, error: new FileError("not_found", r.value.stderr.trim() || `readlink failed for ${abs}`, abs) };
|
|
693
693
|
return ok(r.value.stdout.trim());
|
|
694
694
|
}
|
|
695
|
+
async canonicalChain(p, abortSignal) {
|
|
696
|
+
return runSymlinkChain(this.resolve(p), (cmd) => this.exec(cmd, { abortSignal }));
|
|
697
|
+
}
|
|
695
698
|
async canonicalPath(p, abortSignal) {
|
|
696
699
|
const abs = this.resolve(p);
|
|
697
700
|
const r = await this.exec(`realpath -- ${shellQuote(abs)}`, { abortSignal });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FileError, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundPoll, type BackgroundSpawnOptions } from "@sema-agent/core";
|
|
1
|
+
import { FileError, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundPoll, type BackgroundSpawnOptions } from "@sema-agent/core";
|
|
2
2
|
/** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
|
|
3
3
|
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
4
4
|
/** Construction config for {@link RemoteHostExecutionEnv}. */
|
|
@@ -216,6 +216,20 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
216
216
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
217
217
|
canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
218
218
|
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
219
|
+
/**
|
|
220
|
+
* S-362 / core 7.21.0 [ref] —— **实现**(表态:本腿实现 `canonicalChain`)。host 腿是 node:fs 腿,所以按
|
|
221
|
+
* 提货单 [ref] @server 五适配器段的第一行,与 core 的 `NodeExecutionEnv.canonicalChain` **同形**:对整条
|
|
222
|
+
* 拼法 `readlink`,再对每一跳 `readlink`,**上限 64**、**只锚不折**、**遇环即止**;win32 宿主上跳到共享
|
|
223
|
+
* (UNC)时**记下该跳并停止探测**(不碰网络,CC `vr` 对网络拼法只给拼法)。
|
|
224
|
+
*
|
|
225
|
+
* 🔴 **不复用 core 的实现而是同形重写**:`NodeExecutionEnv` 是 core 的内部类,包根不导出它的这只方法;
|
|
226
|
+
* 而本腿的每一次读都必须先过 {@link fsReady}(2026-07-25 全族统一:拆除之后只读面不得与写面自相矛盾),
|
|
227
|
+
* 那道门在 core 的实现里不存在。同形的判据钉在 `test/remote-env-canonical-chain.test.ts`。
|
|
228
|
+
*
|
|
229
|
+
* 链尽的三种 errno(`EINVAL` 不是软链 / `ENOENT` 不存在 / `ENOTDIR`)⇒ `ok` + 到此为止的跳;其余 errno
|
|
230
|
+
* (`EACCES` …)⇒ `ok:false`(core 读作「这一次没有跳」,**永不是拒绝**)。非链接的叶答 `[]`。
|
|
231
|
+
*/
|
|
232
|
+
canonicalChain(p: string, abortSignal?: AbortSignal): Promise<Result<SymlinkChain, FileError>>;
|
|
219
233
|
exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
220
234
|
createDir(p: string, options?: {
|
|
221
235
|
recursive?: boolean;
|
|
@@ -5,7 +5,7 @@ import fs from "node:fs/promises";
|
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
6
|
import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, realpathSync, accessSync, constants as FS_CONST } from "node:fs";
|
|
7
7
|
import { StringDecoder } from "node:string_decoder";
|
|
8
|
-
import { armPipeDestroyGrace, numEnvOr } from "./remote-shell.js";
|
|
8
|
+
import { armPipeDestroyGrace, numEnvOr, SYMLINK_CHAIN_MAX_HOPS } from "./remote-shell.js";
|
|
9
9
|
import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
|
|
10
10
|
import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys } from "./host-platform.js";
|
|
11
11
|
import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
|
|
@@ -22,6 +22,7 @@ const HOST_BG_FILE_CAP = numEnvOr("HOST_BG_FILE_CAP", 64 * 1024 * 1024, HOST_BG_
|
|
|
22
22
|
const HOST_BG_KILL_GRACE_MS = numEnvOr("HOST_BG_KILL_GRACE_MS", 1000, 0);
|
|
23
23
|
const HOST_BG_MEM_CAP = numEnvOr("HOST_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
|
|
24
24
|
const ok = (value) => ({ ok: true, value });
|
|
25
|
+
const UNC_SPELLING = /^[\\/]{2}[^\\/]+[\\/]+[^\\/]/;
|
|
25
26
|
const unsupported = (op) => ({
|
|
26
27
|
ok: false,
|
|
27
28
|
error: new RemoteExecutionError("unsupported", `${op} is not supported on a host target (the operator's own machine — no container boundary, not snapshotable; capabilities.suspendable=false)`),
|
|
@@ -1006,6 +1007,41 @@ export class RemoteHostExecutionEnv {
|
|
|
1006
1007
|
return { ok: false, error: this.fileError(e, abs, abortSignal) };
|
|
1007
1008
|
}
|
|
1008
1009
|
}
|
|
1010
|
+
async canonicalChain(p, abortSignal) {
|
|
1011
|
+
const ready = await this.fsReady(p);
|
|
1012
|
+
if (!ready.ok)
|
|
1013
|
+
return ready;
|
|
1014
|
+
const hops = [];
|
|
1015
|
+
const seen = new Set();
|
|
1016
|
+
let cur = this.resolve(p);
|
|
1017
|
+
for (let i = 0; i < SYMLINK_CHAIN_MAX_HOPS; i += 1) {
|
|
1018
|
+
let target;
|
|
1019
|
+
try {
|
|
1020
|
+
target = await fs.readlink(cur);
|
|
1021
|
+
}
|
|
1022
|
+
catch (e) {
|
|
1023
|
+
const fe = this.fileError(e, cur, abortSignal);
|
|
1024
|
+
if (fe.code === "invalid" || fe.code === "not_found" || fe.code === "not_directory")
|
|
1025
|
+
break;
|
|
1026
|
+
return { ok: false, error: fe };
|
|
1027
|
+
}
|
|
1028
|
+
let next;
|
|
1029
|
+
if (path.isAbsolute(target))
|
|
1030
|
+
next = target;
|
|
1031
|
+
else {
|
|
1032
|
+
const dir = path.dirname(cur);
|
|
1033
|
+
next = dir.endsWith(path.sep) ? `${dir}${target}` : `${dir}${path.sep}${target}`;
|
|
1034
|
+
}
|
|
1035
|
+
if (seen.has(next))
|
|
1036
|
+
break;
|
|
1037
|
+
seen.add(next);
|
|
1038
|
+
hops.push(next);
|
|
1039
|
+
if (process.platform === "win32" && UNC_SPELLING.test(next))
|
|
1040
|
+
break;
|
|
1041
|
+
cur = next;
|
|
1042
|
+
}
|
|
1043
|
+
return ok({ hops });
|
|
1044
|
+
}
|
|
1009
1045
|
async exists(p, abortSignal) {
|
|
1010
1046
|
const ready = await this.fsReady(p);
|
|
1011
1047
|
if (!ready.ok)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll } from "@sema-agent/core";
|
|
1
|
+
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll } from "@sema-agent/core";
|
|
2
2
|
import type { PodSpecPatch } from "./k8s-exec-protocol.js";
|
|
3
3
|
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
4
4
|
export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, type ParsedBgPoll, } from "./k8s-bg-scripts.js";
|
|
@@ -310,6 +310,10 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
310
310
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
311
311
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
312
312
|
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
313
|
+
/** S-362 / core 7.21.0 [ref] —— **实现**(表态:本腿实现 `canonicalChain`)。一次往返里的一个 `readlink`
|
|
314
|
+
* 循环(上限 64、只锚不折、遇环即止),脚本与读法的单一属主在 `remote-shell.ts`,四条 shell 腿共用同一份。
|
|
315
|
+
* 缺席读法用不上(本腿在场);`ok:false` 由 core 读作「这一次没有跳」,永不是拒绝。 */
|
|
316
|
+
canonicalChain(p: string, abortSignal?: AbortSignal): Promise<Result<SymlinkChain, FileError>>;
|
|
313
317
|
canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
314
318
|
exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
315
319
|
createDir(p: string, options?: {
|
|
@@ -919,6 +919,9 @@ export class RemoteK8sExecutionEnv {
|
|
|
919
919
|
async readLink(p, abortSignal) {
|
|
920
920
|
return this.posixFs.readLink(p, abortSignal);
|
|
921
921
|
}
|
|
922
|
+
async canonicalChain(p, abortSignal) {
|
|
923
|
+
return this.posixFs.canonicalChain(p, abortSignal);
|
|
924
|
+
}
|
|
922
925
|
async canonicalPath(p, abortSignal) {
|
|
923
926
|
return this.posixFs.canonicalPath(p, abortSignal);
|
|
924
927
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory } from "@sema-agent/core";
|
|
2
|
+
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory } from "@sema-agent/core";
|
|
3
3
|
/** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
|
|
4
4
|
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
5
5
|
/** Construction config for {@link RemoteLocalDockerExecutionEnv}. */
|
|
@@ -166,6 +166,10 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
166
166
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
167
167
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
168
168
|
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
169
|
+
/** S-362 / core 7.21.0 [ref] —— **实现**(表态:本腿实现 `canonicalChain`)。一次往返里的一个 `readlink`
|
|
170
|
+
* 循环(上限 64、只锚不折、遇环即止),脚本与读法的单一属主在 `remote-shell.ts`,四条 shell 腿共用同一份。
|
|
171
|
+
* 缺席读法用不上(本腿在场);`ok:false` 由 core 读作「这一次没有跳」,永不是拒绝。 */
|
|
172
|
+
canonicalChain(p: string, abortSignal?: AbortSignal): Promise<Result<SymlinkChain, FileError>>;
|
|
169
173
|
canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
170
174
|
exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
171
175
|
createDir(p: string, options?: {
|
|
@@ -406,6 +406,9 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
406
406
|
async readLink(p, abortSignal) {
|
|
407
407
|
return this.posixFs.readLink(p, abortSignal);
|
|
408
408
|
}
|
|
409
|
+
async canonicalChain(p, abortSignal) {
|
|
410
|
+
return this.posixFs.canonicalChain(p, abortSignal);
|
|
411
|
+
}
|
|
409
412
|
async canonicalPath(p, abortSignal) {
|
|
410
413
|
return this.posixFs.canonicalPath(p, abortSignal);
|
|
411
414
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SshHostKeyPolicy } from "../ssh-host-key.js";
|
|
2
|
-
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory } from "@sema-agent/core";
|
|
2
|
+
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory } from "@sema-agent/core";
|
|
3
3
|
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
4
4
|
export interface SshEnvConfig {
|
|
5
5
|
host: string;
|
|
@@ -171,6 +171,10 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
171
171
|
private posixFsInst?;
|
|
172
172
|
private get posixFs();
|
|
173
173
|
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
174
|
+
/** S-362 / core 7.21.0 [ref] —— **实现**(表态:本腿实现 `canonicalChain`)。一次往返里的一个 `readlink`
|
|
175
|
+
* 循环(上限 64、只锚不折、遇环即止),脚本与读法的单一属主在 `remote-shell.ts`,四条 shell 腿共用同一份。
|
|
176
|
+
* 缺席读法用不上(本腿在场);`ok:false` 由 core 读作「这一次没有跳」,永不是拒绝。 */
|
|
177
|
+
canonicalChain(p: string, abortSignal?: AbortSignal): Promise<Result<SymlinkChain, FileError>>;
|
|
174
178
|
canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
175
179
|
exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
176
180
|
createDir(p: string, options?: {
|
|
@@ -548,6 +548,9 @@ export class RemoteSshExecutionEnv {
|
|
|
548
548
|
async readLink(p, abortSignal) {
|
|
549
549
|
return this.posixFs.readLink(p, abortSignal);
|
|
550
550
|
}
|
|
551
|
+
async canonicalChain(p, abortSignal) {
|
|
552
|
+
return this.posixFs.canonicalChain(p, abortSignal);
|
|
553
|
+
}
|
|
551
554
|
async canonicalPath(p, abortSignal) {
|
|
552
555
|
return this.posixFs.canonicalPath(p, abortSignal);
|
|
553
556
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared shell/path helpers for the SSH + ADB `RemoteExecutionEnv` adapters. Kept in ONE place so the
|
|
3
3
|
* security-sensitive quoting can't drift between adapters (council design/#4, #5).
|
|
4
4
|
*/
|
|
5
|
-
import type { FileInfo } from "@sema-agent/core";
|
|
5
|
+
import type { ExecutionError, FileError, FileInfo, RemoteExecutionError, Result, SymlinkChain } from "@sema-agent/core";
|
|
6
6
|
import type { ChildProcess } from "node:child_process";
|
|
7
7
|
/**
|
|
8
8
|
* D5 pipe-destroy grace (parity with core's vendored `EXEC_FORCE_SETTLE_GRACE_MS`, harness/env/nodejs.js):
|
|
@@ -32,4 +32,81 @@ export declare function drainNumEnvWarnings(): Array<{
|
|
|
32
32
|
raw: string;
|
|
33
33
|
}>;
|
|
34
34
|
export declare function numEnvOr(name: string, def: number, min: number): number;
|
|
35
|
+
export declare const SYMLINK_CHAIN_MAX_HOPS = 64;
|
|
36
|
+
/**
|
|
37
|
+
* 一次往返走完叶 symlink 链的 POSIX sh 脚本(**纯数据**:返回一个命令串 ⇒ `build*`,CLAUDE.md 工厂律)。
|
|
38
|
+
*
|
|
39
|
+
* 🔴 `readlink` **缺失**必须与「不是软链」分开:两者在 shell 里都是 exit≠0,而 `|| break` 会把「工具不在」
|
|
40
|
+
* 静默读成「零跳」—— 那是安全轴上的静默兜底(链视图整条消失,判官少一个视图)。所以脚本**先探**一次
|
|
41
|
+
* `command -v`,缺失时打一句 `readlink: not found` 到 stderr 并 exit 127 —— 那句话正好命中
|
|
42
|
+
* {@link import("./remote-env-file-error.js").classifyFsStderr} 的**命令缺失护栏**(它把命令缺失归
|
|
43
|
+
* `unknown` 而不是 `not_found`),于是调用方拿到的是 `ok:false`(core 读作「这一次没有跳」,永不是拒绝)
|
|
44
|
+
* 而不是一个伪造的空链。
|
|
45
|
+
*
|
|
46
|
+
* 🔴 **整条命令包在一个 `{ …; }` 里**,理由是**调用方会给它加前缀**:四条腿的 `withCwdEnv` 都拼
|
|
47
|
+
* `cd <dir> && <命令>`(ssh `:760` / k8s `:1418` / adb `:578` / local-docker 同形)。这里连着犯过两次错,
|
|
48
|
+
* 两次都记下来,因为它们是**同一个坑的两面**:
|
|
49
|
+
* · 写成 `command -v … || { …; }`:`||` 与前缀的 `&&` 结成一条链(`cd X && command -v … || { … }`),
|
|
50
|
+
* **`cd` 失败时跑的是探测的失败臂** —— 报「readlink 不在」而真相是「那个目录没了」,运维去修一个没坏的东西;
|
|
51
|
+
* · 改成独立语句 `if ! …; fi; c=…; while …`:前缀的 `&&` 只短路**那一条 `if`**,后面的循环照跑 ——
|
|
52
|
+
* 于是「`cd` 失败 ∧ readlink 缺失」这一格里,`|| break` 把工具缺失吞成零跳,脚本 **exit 0 + 空 stdout**,
|
|
53
|
+
* 调用方拿到一条**伪造的空链**。这正是探测本来要挡的那个静默兜底([ref] 轴),换了个入口回来了。
|
|
54
|
+
* ⇒ 正解是**让整条命令只有一个退出状态**:包进 `{ …; }`,前缀的 `&&` 要么统治全部、要么全部不跑。
|
|
55
|
+
* `cd` 失败 ⇒ 退出码来自 `cd` 自己、stderr 是它自己的话 ⇒ `ok:false`(core 读作「这一次没有跳」),
|
|
56
|
+
* 诚实且指向真因;`cd` 成功 ⇒ 探测与循环照旧。`{ }` 不是子 shell,里面的 `exit 127` 照样结束整条命令。
|
|
57
|
+
*
|
|
58
|
+
* 词法细节(逐条对着 core 的 Node 实现写,两边同形):绝对目标直接接上;相对目标用参数展开
|
|
59
|
+
* `${c%/*}` 取**当前这条链接**所在目录(不调 `dirname` —— toybox 的 `dirname` 不认 `--`,且这里零外部命令
|
|
60
|
+
* 更省一次 fork),根目录那一格(`${c%/*}` 为空)补 `/`。**全程不折叠 `..`**。
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildSymlinkChainCommand(absPath: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* {@link buildSymlinkChainCommand} 的 stdout → `SymlinkChain.hops`。
|
|
65
|
+
*
|
|
66
|
+
* 三件事**只在这里**做一次(四条 shell 腿 + e2b 共读):
|
|
67
|
+
* · **环即止** —— 出现重复跳时在**重复的那一跳之前**截断(CC `vr` 的 `d.has(u)` 同律)。
|
|
68
|
+
* 🔴 **一条如实登记的、与 core 参考实现的分歧(合并复审车GU 亲核两树,验真后**披露不修**)**:本族(六腿同形)
|
|
69
|
+
* 的 seen 表**从空开始**,而 core 的 `NodeExecutionEnv.canonicalChain` 用**被寻址的拼法**给它播种
|
|
70
|
+
* (`new Set([resolved])`)。于是**环绕回起点**的那一类链上两边差一跳:`a -> b`、`b -> a`,问 `a`,本族答
|
|
71
|
+
* `[b, a]`,core 答 `[b]`。**只在真有环时可观察**(无环链上 seen 永远碰不到起点,两边逐字节相同),而有环的
|
|
72
|
+
* 链上那次写本来就会以 `ELOOP` 失败。不修的两条理由:①方向是**多报一跳**(判官多一个视图 ⇒ 多问,从不
|
|
73
|
+
* 造成误放行),而改成与 core 同形是**摘掉**一个视图 —— 在 [ref] 轴上那是错的方向,要动得先有裁定;
|
|
74
|
+
* ②那一跳是 `b` **真的写下的**目标,契约句「不重复被寻址的拼法」两种读法都讲得通(core 读作「起点不入表」,
|
|
75
|
+
* 本族读作「每条链接写下的目标都入表」),属**待 core 裁**的一格,不是本仓单方面能定的。
|
|
76
|
+
* ⚠️ 故各腿头注里「与 core 同形 / 逐字节对得上」那句话的射程是**无环链**;有环的那一格以本段为准。
|
|
77
|
+
* 脚本侧不做:
|
|
78
|
+
* POSIX sh 里维护一张 seen 表要么靠内嵌换行的 `case` 模式(易碎),要么另起一个进程;放在这里则
|
|
79
|
+
* **一份实现**被四条腿与 e2b 共用,且与 host 腿的 Node 循环逐字节对得上。代价如实登记:链上真有环时,
|
|
80
|
+
* 远端在**那一次往返内**最多多跑到 64 次 `readlink`(有界,不是无界探测),而报出来的跳与 host 腿相同。
|
|
81
|
+
* · **上限 64** —— 脚本已经封顶,这里再切一次是对**传输层**的不信任(答多了不当真),不是第二份判据。
|
|
82
|
+
* · **逐行 trim** —— 与本族 `readLink` 的既有姿态**同一条**(基座头注:链接目标含换行是病态形,按 trim
|
|
83
|
+
* 处理,不假装支持)。残留如实写出来:目标拼法真带首尾空白时,trim 后的跳可能与真目标差几个字节 ——
|
|
84
|
+
* 方向上只会让一条跳**匹配不到**受保护行(少问,不是多放行),而终点判决(`canonicalPath`)完全不经本路。
|
|
85
|
+
*/
|
|
86
|
+
export declare function symlinkChainHopsFromStdout(stdout: string): string[];
|
|
87
|
+
/** {@link runSymlinkChain} 的传输面 —— 各腿的 `exec` 收敛成的那一个形(与 `PosixShellFs` 基座的 `run` 同形)。 */
|
|
88
|
+
export type ShellChainRun = (cmd: string) => Promise<Result<{
|
|
89
|
+
stdout: string;
|
|
90
|
+
stderr: string;
|
|
91
|
+
exitCode: number;
|
|
92
|
+
}, ExecutionError | RemoteExecutionError>>;
|
|
93
|
+
/**
|
|
94
|
+
* **shell 形 `canonicalChain` 的全部半场,一处**(对抗复审 F4 [low],验真后按类修)。
|
|
95
|
+
*
|
|
96
|
+
* 此前脚本与读法收在这里,而「怎么把它们接到传输上、exit≠0 怎么归码」在**两处**各写一遍:
|
|
97
|
+
* `posix-shell-fs.ts`(四条 shell 腿)与 `remote-env-e2b.ts`(原生 SDK 腿)。两份五行代码语义相同、
|
|
98
|
+
* 错误归类却各写各的,于是「e2b 与四条 shell 腿答得一样」只靠一句注释担保 —— 复审的换靶变异实测:
|
|
99
|
+
* 把 e2b 那五行改成 `return ok({ hops: [] })`,**全部钉照绿**。收成一只函数之后,那条担保是结构事实:
|
|
100
|
+
* 两边调的是同一个函数,分叉无处可生;基座上那一整套行为钉同时就是 e2b 的钉。
|
|
101
|
+
*
|
|
102
|
+
* 归码(两腿共用同一条,不再各写各的):
|
|
103
|
+
* · 传输自己失败 ⇒ {@link fileErrorFromExec}(`aborted` 穿透,其余诚实降级 `unknown`);
|
|
104
|
+
* · exit≠0 ⇒ {@link classifyFsStderr} 且 fallback 取 **`unknown` 而不是 `not_found`** —— 这条命令上
|
|
105
|
+
* 「叶不是软链」与「叶不存在」**都是 exit 0 + 空 stdout**(脚本里的 `|| break`),所以任何 exit≠0
|
|
106
|
+
* 都**不是**「这个文件不在」,而是解析器自己出了事(工具缺失 / cwd 没了 / 传输坏 / shell 拒)。
|
|
107
|
+
* 把它折成 `not_found` 会让运维去找一条根本没丢的路径。
|
|
108
|
+
* ⚠️ 例外由 `classifyFsStderr` 自己认:`cd` 失败那一格的 stderr 里真带「No such file」,它按**那句话**
|
|
109
|
+
* 归 `not_found` —— 那时真的有一条路径没了(工作目录),所以那个码是对的。
|
|
110
|
+
*/
|
|
111
|
+
export declare function runSymlinkChain(absPath: string, run: ShellChainRun): Promise<Result<SymlinkChain, FileError>>;
|
|
35
112
|
//# sourceMappingURL=remote-shell.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
|
|
1
2
|
export const EXEC_FORCE_SETTLE_GRACE_MS = 3000;
|
|
2
3
|
export function armPipeDestroyGrace(child) {
|
|
3
4
|
const t = setTimeout(() => {
|
|
@@ -36,4 +37,38 @@ export function numEnvOr(name, def, min) {
|
|
|
36
37
|
}
|
|
37
38
|
return Math.max(min, (Number.isFinite(n) ? n : 0) || def);
|
|
38
39
|
}
|
|
40
|
+
export const SYMLINK_CHAIN_MAX_HOPS = 64;
|
|
41
|
+
export function buildSymlinkChainCommand(absPath) {
|
|
42
|
+
return (`{ if ! command -v readlink >/dev/null 2>&1; then printf 'readlink: not found\\n' >&2; exit 127; fi; ` +
|
|
43
|
+
`c=${shellQuote(absPath)}; n=0; ` +
|
|
44
|
+
`while [ "$n" -lt ${SYMLINK_CHAIN_MAX_HOPS} ]; do ` +
|
|
45
|
+
`t=$(readlink -- "$c") || break; ` +
|
|
46
|
+
`case "$t" in "") break ;; /*) c="$t" ;; ` +
|
|
47
|
+
`*) d="${"${c%/*}"}"; case "$d" in "") c="/$t" ;; */) c="$d$t" ;; *) c="$d/$t" ;; esac ;; esac; ` +
|
|
48
|
+
`printf '%s\\n' "$c"; n=$((n+1)); done; }`);
|
|
49
|
+
}
|
|
50
|
+
export function symlinkChainHopsFromStdout(stdout) {
|
|
51
|
+
const hops = [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (const raw of stdout.split("\n")) {
|
|
54
|
+
const hop = raw.trim();
|
|
55
|
+
if (hop === "")
|
|
56
|
+
continue;
|
|
57
|
+
if (seen.has(hop))
|
|
58
|
+
break;
|
|
59
|
+
seen.add(hop);
|
|
60
|
+
hops.push(hop);
|
|
61
|
+
if (hops.length >= SYMLINK_CHAIN_MAX_HOPS)
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
return hops;
|
|
65
|
+
}
|
|
66
|
+
export async function runSymlinkChain(absPath, run) {
|
|
67
|
+
const r = await run(buildSymlinkChainCommand(absPath));
|
|
68
|
+
if (!r.ok)
|
|
69
|
+
return { ok: false, error: fileErrorFromExec(r.error, absPath) };
|
|
70
|
+
if (r.value.exitCode !== 0)
|
|
71
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, absPath, "unknown") };
|
|
72
|
+
return { ok: true, value: { hops: symlinkChainHopsFromStdout(r.value.stdout) } };
|
|
73
|
+
}
|
|
39
74
|
//# sourceMappingURL=remote-shell.js.map
|
|
@@ -60,7 +60,7 @@ type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain" | "peer
|
|
|
60
60
|
type _GuardMailboxAppend = AssertAllKeysHandled<Exclude<keyof MailboxAppendMessage, MailboxAppendProjected>>;
|
|
61
61
|
type _GuardMailboxAppendReverse = AssertAllKeysHandled<Exclude<MailboxAppendProjected, keyof MailboxAppendMessage>>;
|
|
62
62
|
type WiringSectionProjected = "schemaVersion" | "leg" | "ask" | "question" | "interaction" | "elicit" | "parkLane" | "session" | "fleet" | "permissionRules" | "modelGate" | "autoMode" | "mcp" | "tools" | "hooks" | "lsp" | "writeProtection";
|
|
63
|
-
type WiringSectionExcluded = "governance" | "configFingerprint";
|
|
63
|
+
type WiringSectionExcluded = "governance" | "configFingerprint" | "autoConsolidation";
|
|
64
64
|
type _GuardWiringSections = AssertAllKeysHandled<Exclude<keyof WiringManifest, WiringSectionProjected | WiringSectionExcluded>>;
|
|
65
65
|
type _GuardWiringSectionsReverse = AssertAllKeysHandled<Exclude<WiringSectionProjected | WiringSectionExcluded, keyof WiringManifest>>;
|
|
66
66
|
type WiringSection<K extends keyof WiringManifest> = NonNullable<WiringManifest[K]>;
|
|
@@ -103,6 +103,9 @@ type _GuardWiringRulesReverse = AssertAllKeysHandled<Exclude<WiringRulesHandled,
|
|
|
103
103
|
type WiringWriteProtectionHandled = "targetView";
|
|
104
104
|
type _GuardWiringWriteProtection = AssertAllKeysHandled<Exclude<keyof WiringSection<"writeProtection">, WiringWriteProtectionHandled>>;
|
|
105
105
|
type _GuardWiringWriteProtectionReverse = AssertAllKeysHandled<Exclude<WiringWriteProtectionHandled, keyof WiringSection<"writeProtection">>>;
|
|
106
|
+
type WiringAutoConsolidationHandled = "onRecommendation";
|
|
107
|
+
type _GuardWiringAutoConsolidation = AssertAllKeysHandled<Exclude<keyof WiringSection<"autoConsolidation">, WiringAutoConsolidationHandled>>;
|
|
108
|
+
type _GuardWiringAutoConsolidationReverse = AssertAllKeysHandled<Exclude<WiringAutoConsolidationHandled, keyof WiringSection<"autoConsolidation">>>;
|
|
106
109
|
type WiringModelGateHandled = "class" | "removed" | "restore";
|
|
107
110
|
type _GuardWiringModelGate = AssertAllKeysHandled<Exclude<keyof WiringSection<"modelGate">, WiringModelGateHandled>>;
|
|
108
111
|
type _GuardWiringModelGateReverse = AssertAllKeysHandled<Exclude<WiringModelGateHandled, keyof WiringSection<"modelGate">>>;
|
|
@@ -185,6 +188,8 @@ export type CoreKeysetGuards = [
|
|
|
185
188
|
_GuardWiringSections,
|
|
186
189
|
_GuardWiringWriteProtection,
|
|
187
190
|
_GuardWiringWriteProtectionReverse,
|
|
191
|
+
_GuardWiringAutoConsolidation,
|
|
192
|
+
_GuardWiringAutoConsolidationReverse,
|
|
188
193
|
_GuardWiringSectionsReverse,
|
|
189
194
|
_GuardWiringLeg,
|
|
190
195
|
_GuardWiringLegReverse,
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -604,6 +604,14 @@ export declare function workspaceChangedEventData(ev: {
|
|
|
604
604
|
* ⚠️ **缺席 = 第三态**:core 的段是 **present-iff 编译出了判官**,`writeProtectedPaths: []` 的部署
|
|
605
605
|
* 整段缺席。缺席**绝不**折成 `"spelling-only"`(那是「判官在、但只认拼法」,完全不同的一件事),
|
|
606
606
|
* 也不铸 `writeProtection: {}`。词出闭集同样**不**折进缺席,走 `dropProjectedFace` 留痕(见那里)。
|
|
607
|
+
* · `autoConsolidation`(`{ onRecommendation: true }`;core 7.21.0 [ref],S-362 提货件)—— **裁:operator
|
|
608
|
+
* 受众,租户面剥**,所以它**不在本构造器里**,在 {@link wiringManifestOperatorEventData}。四条判据逐字在
|
|
609
|
+
* `core-keyset-guard.ts` ⑥b 的 `WiringSectionExcluded` 那一行(与 `governance` / `configFingerprint`
|
|
610
|
+
* 同表同处,免得同一条裁定有两个写者)。一句话:它是**出口事实**(武装 = 每个建议任务之后整库记忆自动
|
|
611
|
+
* 外流到整理模型),与 governance 四布尔同族,而不是 `writeProtection` 那种「这条腿手里有什么」的自述;
|
|
612
|
+
* 并且它对调用方**本就不可观测**(整理跑在终端 harvest 之后、fire-and-forget、宿主侧),剥掉它不收走
|
|
613
|
+
* 任何能力 —— 这正是 `writeProtection` 判据①**不成立**的那一格。
|
|
614
|
+
* ⚠️ **缺席 = 未武装**(core present-iff),不折 `false`、不读「未知」。
|
|
607
615
|
* 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
|
|
608
616
|
* (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来 —— ⚠️ **但那条钉是夹具驱动的**:它只看
|
|
609
617
|
* `deriveWiringManifest(EFFECTIVE_FACTS)` 真铸出来的段,facts 不带的可选段(147③ 的 `autoMode` 就是)它一格
|
|
@@ -662,6 +670,16 @@ export declare function wiringManifestEventData(ev: {
|
|
|
662
670
|
*
|
|
663
671
|
* governance **逐键挑**(禁 `{...governance}`):core 往这个段里加第五键时必须由人显式处置,而不是顺着
|
|
664
672
|
* spread 裸上线 —— 这个段的每一键都是部署治理面,新键的默认待遇必须是「不发」。
|
|
673
|
+
*
|
|
674
|
+
* `autoConsolidation`(core 7.21.0 [ref],S-362)与 governance 同规:租户面剥、本面发(裁定四条判据在
|
|
675
|
+
* `core-keyset-guard.ts` ⑥b `WiringSectionExcluded`)。**present-iff 武装**:
|
|
676
|
+
* · 整段缺席 ⇒ **未武装**,不铸键、不计数(正面事实,不是「未知」,更不是 `{onRecommendation:false}`);
|
|
677
|
+
* · 在场且 `onRecommendation === true` ⇒ 逐字铸出(core 的在场形只有这一个,构造期门已拒掉「武装但跑不起来」);
|
|
678
|
+
* · 在场但读不出那个形 ⇒ `dropProjectedFace` 留痕并整段缺席(与 `writeProtection` / `lsp` / `hooks` 同一条
|
|
679
|
+
* 族规:「在场但读不出 ⇒ 计一次并丢弃」;**不**自造第三态,词表/形的属主在 core)。
|
|
680
|
+
* 🔴 代价如实登记,与 `writeProtection` 那一格逐字同源:wire 上「读不出」与「真未武装」是**同一个形**,
|
|
681
|
+
* 分得开它们的只有 `server.trace.projection-input-malformed` 这只计数 —— 而这一格比那一格更该说清楚,
|
|
682
|
+
* 因为它读错的方向是**把一台正在外流记忆的部署渲染成没在外流**。故本段的读不出**必须**留痕,不许静默。
|
|
665
683
|
*/
|
|
666
684
|
export declare function wiringManifestOperatorEventData(ev: {
|
|
667
685
|
manifest?: unknown;
|
package/dist/trace/project.js
CHANGED
|
@@ -554,9 +554,20 @@ export function wiringManifestOperatorEventData(ev) {
|
|
|
554
554
|
...(bool(g.memoryAdmission) !== undefined ? { memoryAdmission: g.memoryAdmission } : {}),
|
|
555
555
|
...(bool(g.retention) !== undefined ? { retention: g.retention } : {}),
|
|
556
556
|
};
|
|
557
|
+
const autoConsolidation = (() => {
|
|
558
|
+
if (m.autoConsolidation === undefined)
|
|
559
|
+
return undefined;
|
|
560
|
+
const seg = sec(m.autoConsolidation);
|
|
561
|
+
if (seg === undefined)
|
|
562
|
+
return dropProjectedFace("wiring_manifest.autoConsolidation is not an object");
|
|
563
|
+
if (bool(seg.onRecommendation) !== true)
|
|
564
|
+
return dropProjectedFace("wiring_manifest.autoConsolidation.onRecommendation is not literal true (core's only present form)");
|
|
565
|
+
return { onRecommendation: true };
|
|
566
|
+
})();
|
|
557
567
|
return {
|
|
558
568
|
...tenant,
|
|
559
569
|
...(Object.keys(governance).length > 0 ? { governance } : {}),
|
|
570
|
+
...(autoConsolidation !== undefined ? { autoConsolidation } : {}),
|
|
560
571
|
...(str(m.configFingerprint) !== undefined ? { configFingerprint: m.configFingerprint } : {}),
|
|
561
572
|
};
|
|
562
573
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.82.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": "7.
|
|
57
|
+
"@sema-agent/core": "7.21.1",
|
|
58
58
|
"@sema-agent/settings-schema": "2.1.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|