@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
- 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;
@@ -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
- const root = result.json;
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(spec.controlRepoRoot, ensured.path)
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.startsWith("..") ? ensured.path : relative,
50
+ relativeWorktreePath: relative,
47
51
  relativeBranch: branch,
48
52
  nodeModulesLinked,
49
53
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.28.3",
3
+ "version": "0.28.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",