@tea-agent/loop-agent 0.28.3 → 0.28.5
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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.28.5] - 2026-08-05
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- 修复 macOS 下 /tmp 路径别名导致 Night Scheduler 工作区隔离校验误报失败的问题
|
|
10
|
+
|
|
11
|
+
### 修复
|
|
12
|
+
|
|
13
|
+
- Night Scheduler 准入控制现使用 realpath 规范化控制目录与 worktree 路径,避免 macOS /tmp 别名引发隔离校验假失败
|
|
14
|
+
- Night Scheduler 准入事实仅冻结仓库内相对路径的 worktree 路径,提升隔离检查的准确性
|
|
15
|
+
|
|
16
|
+
## [0.28.4] - 2026-08-05
|
|
17
|
+
|
|
18
|
+
### 重点更新
|
|
19
|
+
|
|
20
|
+
- 修复 Night Scheduler 准入控制中 writeSet 门禁冻结的误报失败问题
|
|
21
|
+
|
|
22
|
+
### 修复
|
|
23
|
+
|
|
24
|
+
- Night Scheduler `admission prepare`:为 `task advance --json` 补充 `expectJson`,并允许从 stdout 解析 writeSet gate,修复“did not return JSON”假失败
|
|
25
|
+
|
|
5
26
|
## [0.28.3] - 2026-08-05
|
|
6
27
|
|
|
7
28
|
### 重点更新
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { access, readFile, realpath } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
|
|
5
5
|
import { buildWorkerRunId, } from "./run-task.js";
|
|
@@ -27,12 +27,13 @@ export async function executePreparedTaskSpec(options) {
|
|
|
27
27
|
throw new Error(`${identityFailure.code}: ${identityFailure.message}`);
|
|
28
28
|
}
|
|
29
29
|
// Fail closed: never run on control root when workspace differs.
|
|
30
|
-
|
|
31
|
-
const controlRoot =
|
|
30
|
+
// Resolve realpath so macOS /tmp vs /private/tmp aliases compare equal.
|
|
31
|
+
const controlRoot = await resolveExistingPath(options.controlRepoRoot);
|
|
32
|
+
const workspaceRoot = await resolveExistingPath(options.workspaceRoot);
|
|
32
33
|
if (workspaceRoot === controlRoot) {
|
|
33
34
|
throw new Error("executePreparedTask: refusing to run on controlRepoRoot (night isolation required)");
|
|
34
35
|
}
|
|
35
|
-
const expectedWorkspaceRoot =
|
|
36
|
+
const expectedWorkspaceRoot = await resolvePathAgainstRoot(controlRoot, admission.workspace.path);
|
|
36
37
|
if (workspaceRoot !== expectedWorkspaceRoot) {
|
|
37
38
|
throw new Error(`executePreparedTask: workspace binding mismatch (expected ${expectedWorkspaceRoot}, got ${workspaceRoot})`);
|
|
38
39
|
}
|
|
@@ -130,6 +131,26 @@ function assertWithinRoot(root, candidate, label) {
|
|
|
130
131
|
throw new Error(`executePreparedTask: ${label} escapes its allowed root`);
|
|
131
132
|
}
|
|
132
133
|
}
|
|
134
|
+
async function resolveExistingPath(inputPath) {
|
|
135
|
+
const resolved = path.resolve(inputPath);
|
|
136
|
+
try {
|
|
137
|
+
return await realpath(resolved);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return resolved;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function resolvePathAgainstRoot(root, relativeOrAbsolute) {
|
|
144
|
+
const joined = path.isAbsolute(relativeOrAbsolute)
|
|
145
|
+
? path.resolve(relativeOrAbsolute)
|
|
146
|
+
: path.resolve(root, relativeOrAbsolute);
|
|
147
|
+
try {
|
|
148
|
+
return await realpath(joined);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return joined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
133
154
|
function isAdvanceSuccess(result) {
|
|
134
155
|
if (result.ok)
|
|
135
156
|
return true;
|
|
@@ -476,6 +476,7 @@ async function defaultPrepareInWorkspace(input) {
|
|
|
476
476
|
], {
|
|
477
477
|
cwd: input.workspaceRoot,
|
|
478
478
|
artifactName: `admission-task-advance-${materializeManifest.harnessTaskId}`,
|
|
479
|
+
expectJson: true,
|
|
479
480
|
});
|
|
480
481
|
const gate = extractGate(advanceResult, materializeManifest.harnessTaskId);
|
|
481
482
|
const dagBytes = await readFile(getTaskPaths(input.workspaceRoot, materializeManifest.harnessTaskId)
|
|
@@ -507,7 +508,15 @@ async function defaultPrepareInWorkspace(input) {
|
|
|
507
508
|
};
|
|
508
509
|
}
|
|
509
510
|
function extractGate(result, taskId) {
|
|
510
|
-
|
|
511
|
+
let root = result.json;
|
|
512
|
+
if ((!root || typeof root !== "object") && result.stdout?.trim()) {
|
|
513
|
+
try {
|
|
514
|
+
root = JSON.parse(result.stdout);
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
// keep root undefined; error below
|
|
518
|
+
}
|
|
519
|
+
}
|
|
511
520
|
if (!root || typeof root !== "object") {
|
|
512
521
|
throw new Error(`admission prepare did not return JSON for ${taskId}; cannot open writeSet gate`);
|
|
513
522
|
}
|
|
@@ -37,13 +37,17 @@ export async function prepareNightWorkspace(spec) {
|
|
|
37
37
|
if (spec.hydrateNodeModules !== false) {
|
|
38
38
|
nodeModulesLinked = await linkNodeModulesIfPresent(spec.controlRepoRoot, ensured.path);
|
|
39
39
|
}
|
|
40
|
+
const controlResolved = path.resolve(spec.controlRepoRoot);
|
|
40
41
|
const relative = path
|
|
41
|
-
.relative(
|
|
42
|
+
.relative(controlResolved, path.resolve(ensured.path))
|
|
42
43
|
.split(path.sep)
|
|
43
44
|
.join("/") || relativeWorktreePath;
|
|
45
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
46
|
+
throw new Error(`night worktree path must stay under control repo (.worktrees): ${ensured.path}`);
|
|
47
|
+
}
|
|
44
48
|
return {
|
|
45
49
|
...ensured,
|
|
46
|
-
relativeWorktreePath: relative
|
|
50
|
+
relativeWorktreePath: relative,
|
|
47
51
|
relativeBranch: branch,
|
|
48
52
|
nodeModulesLinked,
|
|
49
53
|
};
|