@tea-agent/loop-agent 0.28.4 → 0.28.6

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,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.28.6] - 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.5] - 2026-08-05
17
+
18
+ ### 重点更新
19
+
20
+ - 修复 macOS 下 /tmp 路径别名导致 Night Scheduler 工作区隔离校验误报失败的问题
21
+
22
+ ### 修复
23
+
24
+ - Night Scheduler 准入控制现使用 realpath 规范化控制目录与 worktree 路径,避免 macOS /tmp 别名引发隔离校验假失败
25
+ - Night Scheduler 准入事实仅冻结仓库内相对路径的 worktree 路径,提升隔离检查的准确性
26
+
5
27
  ## [0.28.4] - 2026-08-05
6
28
 
7
29
  ### 重点更新
@@ -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
- const workspaceRoot = path.resolve(options.workspaceRoot);
31
- const controlRoot = path.resolve(options.controlRepoRoot);
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 = path.resolve(controlRoot, admission.workspace.path);
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;
@@ -1,4 +1,4 @@
1
- import { access, symlink, unlink } from "node:fs/promises";
1
+ import { access, realpath, symlink, unlink } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { ensureOwnedWorktree, verifyOwnedWorktree, } from "../../task/worktree.js";
4
4
  import { getSchedulerRoot } from "./paths.js";
@@ -37,13 +37,22 @@ export async function prepareNightWorkspace(spec) {
37
37
  if (spec.hydrateNodeModules !== false) {
38
38
  nodeModulesLinked = await linkNodeModulesIfPresent(spec.controlRepoRoot, ensured.path);
39
39
  }
40
+ // realpath both sides so macOS /tmp ↔ /private/tmp aliases do not look like escapes.
41
+ const controlResolved = await realpath(path.resolve(spec.controlRepoRoot)).catch(() => path.resolve(spec.controlRepoRoot));
42
+ const worktreeResolved = await realpath(path.resolve(ensured.path)).catch(() => path.resolve(ensured.path));
40
43
  const relative = path
41
- .relative(spec.controlRepoRoot, ensured.path)
44
+ .relative(controlResolved, worktreeResolved)
42
45
  .split(path.sep)
43
46
  .join("/") || relativeWorktreePath;
47
+ if (relative.length === 0 ||
48
+ relative.startsWith("..") ||
49
+ path.isAbsolute(relative)) {
50
+ throw new Error(`night worktree path must stay under control repo (.worktrees): ${ensured.path}`);
51
+ }
44
52
  return {
45
53
  ...ensured,
46
- relativeWorktreePath: relative.startsWith("..") ? ensured.path : relative,
54
+ path: worktreeResolved,
55
+ relativeWorktreePath: relative,
47
56
  relativeBranch: branch,
48
57
  nodeModulesLinked,
49
58
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.28.4",
3
+ "version": "0.28.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",