@fitlab-ai/agent-infra 0.9.7 → 0.9.9
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +11 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/codex-lifecycle.js +19 -5
- package/dist/lib/internal/codex-sandbox-controller.js +6 -13
- package/dist/lib/internal/git-workflow.js +5 -7
- package/dist/lib/internal/platform-pr.js +20 -7
- package/dist/lib/internal/release-workflow.js +3 -4
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-ledger.js +59 -5
- package/dist/lib/internal/task-lifecycle.js +59 -8
- package/dist/lib/internal/task-orchestration.js +161 -288
- package/dist/lib/internal/task-override.js +131 -0
- package/dist/lib/internal/task-review.js +64 -6
- package/dist/lib/internal/task-verify.js +65 -3
- package/dist/lib/internal/task-warning.js +57 -4
- package/dist/lib/platform/pr-summary.js +53 -13
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +145 -5
- package/dist/lib/sandbox/control/lifecycle.js +395 -73
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/codex-orchestration.js +158 -33
- package/dist/lib/task/commands/status.js +59 -1
- package/dist/lib/task/commit-operation.js +335 -0
- package/dist/lib/task/commit-policy.js +18 -0
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +279 -17
- package/dist/lib/task/events.js +9 -5
- package/dist/lib/task/finalization.js +567 -0
- package/dist/lib/task/guard-override.js +31 -0
- package/dist/lib/task/human-override.js +1240 -0
- package/dist/lib/task/ledger-intents.js +6 -3
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +199 -719
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/short-id.js +59 -1
- package/dist/lib/task/task-execution-lock.js +6 -1
- package/dist/lib/task/verification-engine.js +138 -59
- package/dist/lib/task/verification.js +15 -2
- package/dist/lib/task/workflow-warning-intents.js +54 -3
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/git/workflow.ts +11 -2
- package/lib/init.ts +1 -1
- package/lib/internal/codex-lifecycle.ts +18 -5
- package/lib/internal/codex-sandbox-controller.ts +6 -12
- package/lib/internal/git-workflow.ts +5 -6
- package/lib/internal/platform-pr.ts +14 -7
- package/lib/internal/release-workflow.ts +3 -4
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +32 -107
- package/lib/internal/task-override.ts +138 -0
- package/lib/internal/task-review.ts +51 -6
- package/lib/internal/task-verify.ts +52 -3
- package/lib/internal/task-warning.ts +44 -3
- package/lib/platform/pr-summary.ts +65 -14
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +172 -6
- package/lib/sandbox/control/lifecycle.ts +465 -77
- package/lib/sandbox/control/protocol.ts +85 -9
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +73 -43
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/codex-orchestration.ts +167 -33
- package/lib/task/commands/status.ts +78 -1
- package/lib/task/commit-operation.ts +430 -0
- package/lib/task/commit-policy.ts +30 -0
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +304 -19
- package/lib/task/events.ts +10 -5
- package/lib/task/finalization.ts +719 -0
- package/lib/task/guard-override.ts +50 -0
- package/lib/task/human-override.ts +1503 -0
- package/lib/task/ledger-intents.ts +6 -3
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +239 -922
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +145 -64
- package/lib/task/verification.ts +18 -3
- package/lib/task/workflow-warning-intents.ts +61 -4
- package/lib/task/write.ts +4 -1
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +2 -2
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/commit/SKILL.en.md +41 -125
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
- package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.codex/hooks.json +4 -4
- package/dist/lib/task/commit-finalization.js +0 -305
- package/dist/lib/task/commit-intent.js +0 -165
- package/lib/task/commit-finalization.ts +0 -419
- package/lib/task/commit-intent.ts +0 -230
|
@@ -38,6 +38,7 @@ export function sandboxControlPaths(params) {
|
|
|
38
38
|
channelDir: path.join(root, 'channel'),
|
|
39
39
|
statusDir: path.join(root, 'public'),
|
|
40
40
|
processingDir: path.join(root, 'processing'),
|
|
41
|
+
runtimeDir: path.join(root, 'runtime'),
|
|
41
42
|
manifestPath: path.join(root, 'manifest.json')
|
|
42
43
|
};
|
|
43
44
|
}
|
|
@@ -50,6 +51,29 @@ function assertSafeDirectory(directory, expectedBase) {
|
|
|
50
51
|
throw new Error(`Sandbox workspace view must not be a symbolic link: ${directory}`);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
function assertSafeMountTargetPath(target, worktreeRoot, canonicalWorktreeRoot) {
|
|
55
|
+
const relative = path.relative(worktreeRoot, target);
|
|
56
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
57
|
+
throw new Error(`Sandbox workspace mount target escapes its worktree: ${target}`);
|
|
58
|
+
}
|
|
59
|
+
let current = worktreeRoot;
|
|
60
|
+
for (const segment of ['', ...relative.split(path.sep).filter(Boolean)]) {
|
|
61
|
+
current = segment ? path.join(current, segment) : current;
|
|
62
|
+
try {
|
|
63
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
64
|
+
throw new Error(`Sandbox workspace mount target must not contain a symbolic link: ${current}`);
|
|
65
|
+
}
|
|
66
|
+
const canonicalRelative = path.relative(canonicalWorktreeRoot, fs.realpathSync.native(current));
|
|
67
|
+
if (canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {
|
|
68
|
+
throw new Error(`Sandbox workspace mount target escapes its real worktree: ${current}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.code !== 'ENOENT')
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
53
77
|
export function assertSandboxTaskSource(repoRoot, taskId) {
|
|
54
78
|
if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
|
|
55
79
|
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
@@ -65,42 +89,22 @@ export function assertSandboxTaskSource(repoRoot, taskId) {
|
|
|
65
89
|
}
|
|
66
90
|
return canonical;
|
|
67
91
|
}
|
|
68
|
-
export function
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
fs.mkdirSync(agentsRoot, { mode: 0o700 });
|
|
80
|
-
}
|
|
81
|
-
const canonicalAgents = fs.realpathSync.native(agentsRoot);
|
|
82
|
-
if (path.dirname(canonicalAgents) !== canonicalWorktree) {
|
|
83
|
-
throw new Error(`Sandbox workspace mount ancestor escapes the worktree: ${agentsRoot}`);
|
|
92
|
+
export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
|
|
93
|
+
const resolvedWorktreeRoot = path.resolve(worktreeRoot);
|
|
94
|
+
const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
|
|
95
|
+
const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
|
|
96
|
+
const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
|
|
97
|
+
const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
|
|
98
|
+
for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget]) {
|
|
99
|
+
assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
|
|
84
100
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
throw new Error(`Sandbox workspace mount target must not be a symbolic link: ${target}`);
|
|
90
|
-
}
|
|
91
|
-
if (!stat.isDirectory()) {
|
|
92
|
-
throw new Error(`Sandbox workspace mount target must be a directory: ${target}`);
|
|
93
|
-
}
|
|
94
|
-
if (path.dirname(fs.realpathSync.native(target)) !== canonicalAgents) {
|
|
95
|
-
throw new Error(`Sandbox workspace mount target escapes the worktree: ${target}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
fs.mkdirSync(target, { mode: 0o700 });
|
|
101
|
+
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
|
|
102
|
+
for (const { hostPath } of statePaths) {
|
|
103
|
+
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
104
|
+
fs.chmodSync(hostPath, 0o700);
|
|
100
105
|
}
|
|
101
|
-
fs.chmodSync(
|
|
102
|
-
fs.
|
|
103
|
-
return target;
|
|
106
|
+
fs.chmodSync(workspaceRoot, 0o700);
|
|
107
|
+
fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
|
|
104
108
|
}
|
|
105
109
|
export function materializeSandboxWorkspaceView(params) {
|
|
106
110
|
const projectRoot = path.resolve(params.base, params.project);
|
|
@@ -131,15 +135,32 @@ export function materializeSandboxWorkspaceView(params) {
|
|
|
131
135
|
return { root, taskMountPath };
|
|
132
136
|
}
|
|
133
137
|
export function materializeSandboxControl(params) {
|
|
134
|
-
const { root, channelDir, statusDir, processingDir, manifestPath } = sandboxControlPaths(params);
|
|
138
|
+
const { root, channelDir, statusDir, processingDir, runtimeDir, manifestPath } = sandboxControlPaths(params);
|
|
135
139
|
assertSafeDirectory(root, path.resolve(params.base));
|
|
140
|
+
if (fs.existsSync(root)) {
|
|
141
|
+
if (!params.replacementLease || path.resolve(params.replacementLease.root) !== root) {
|
|
142
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_REQUIRED');
|
|
143
|
+
}
|
|
144
|
+
params.replacementLease.assertOwned();
|
|
145
|
+
}
|
|
136
146
|
const consumedDir = path.join(root, 'consumed');
|
|
137
147
|
assertSafeDirectory(consumedDir, root);
|
|
138
148
|
fs.rmSync(consumedDir, { recursive: true, force: true });
|
|
139
149
|
fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
|
|
140
|
-
for (const directory of [statusDir, processingDir]) {
|
|
150
|
+
for (const directory of [statusDir, processingDir, runtimeDir]) {
|
|
141
151
|
assertSafeDirectory(directory, root);
|
|
152
|
+
if (directory === runtimeDir)
|
|
153
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
154
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
155
|
+
}
|
|
156
|
+
for (const directory of [
|
|
157
|
+
path.join(runtimeDir, 'clients'),
|
|
158
|
+
path.join(runtimeDir, 'clients', 'codex', 'capabilities'),
|
|
159
|
+
path.join(runtimeDir, 'clients', 'codex', 'lifecycle')
|
|
160
|
+
]) {
|
|
161
|
+
assertSafeDirectory(directory, runtimeDir);
|
|
142
162
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
163
|
+
fs.chmodSync(directory, 0o700);
|
|
143
164
|
}
|
|
144
165
|
for (const queue of ['requests', 'responses']) {
|
|
145
166
|
const directory = path.join(channelDir, queue);
|
|
@@ -151,7 +172,7 @@ export function materializeSandboxControl(params) {
|
|
|
151
172
|
const generation = randomBytes(16).toString('hex');
|
|
152
173
|
const repoRoot = fs.realpathSync.native(params.repoRoot);
|
|
153
174
|
const manifest = {
|
|
154
|
-
version:
|
|
175
|
+
version: 5,
|
|
155
176
|
engine: params.engine ?? 'docker',
|
|
156
177
|
repoRoot,
|
|
157
178
|
worktreeRoot: fs.realpathSync.native(params.worktreeRoot),
|
|
@@ -165,16 +186,17 @@ export function materializeSandboxControl(params) {
|
|
|
165
186
|
generation,
|
|
166
187
|
channelDir,
|
|
167
188
|
publicStatusDir: statusDir,
|
|
168
|
-
processingDir
|
|
189
|
+
processingDir,
|
|
190
|
+
runtimeDir
|
|
169
191
|
};
|
|
170
192
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
|
|
171
|
-
return { root, channelDir, statusDir, manifestPath, token, generation };
|
|
193
|
+
return { root, channelDir, statusDir, runtimeDir, manifestPath, token, generation };
|
|
172
194
|
}
|
|
173
195
|
export function finalizeSandboxControlManifest(setup, identity) {
|
|
174
196
|
if (!identity.engine || !identity.id)
|
|
175
197
|
throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
|
|
176
198
|
const manifest = JSON.parse(fs.readFileSync(setup.manifestPath, 'utf8'));
|
|
177
|
-
manifest.version =
|
|
199
|
+
manifest.version = 5;
|
|
178
200
|
manifest.engine = identity.engine;
|
|
179
201
|
manifest.containerIdentity = { id: identity.id, labels: { ...identity.labels } };
|
|
180
202
|
const temporary = `${setup.manifestPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { runProbe } from './shell.js';
|
|
6
|
+
import { resolveSandboxTarget } from './workspace-identity.js';
|
|
5
7
|
function probeText(probe, cwd, args) {
|
|
6
8
|
const result = probe('git', ['-C', cwd, ...args], { encoding: 'utf8' });
|
|
7
9
|
if (result.error)
|
|
@@ -138,6 +140,197 @@ function captureOnce(worktree, probe) {
|
|
|
138
140
|
}
|
|
139
141
|
return { worktree: resolvedWorktree, branch, head, changes, identity: hash.digest('hex') };
|
|
140
142
|
}
|
|
143
|
+
function probeTextWithEnv(probe, repoRoot, args, env) {
|
|
144
|
+
const result = probe('git', args, { cwd: repoRoot, encoding: 'utf8', env });
|
|
145
|
+
if (result.error)
|
|
146
|
+
throw result.error;
|
|
147
|
+
if (result.status !== 0) {
|
|
148
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : result.stderr.toString('utf8').trim();
|
|
149
|
+
throw new Error(stderr || `git ${args[0] ?? 'command'} failed with exit code ${result.status ?? 'unknown'}`);
|
|
150
|
+
}
|
|
151
|
+
return typeof result.stdout === 'string' ? result.stdout : result.stdout.toString('utf8');
|
|
152
|
+
}
|
|
153
|
+
function probeMetadataCommand(probe, repoRoot, args, env) {
|
|
154
|
+
const result = probe('git', args, { cwd: repoRoot, encoding: 'utf8', env });
|
|
155
|
+
if (result.error)
|
|
156
|
+
throw result.error;
|
|
157
|
+
return result.status === 0;
|
|
158
|
+
}
|
|
159
|
+
function hasValidHeadContent(content) {
|
|
160
|
+
return /^(?:ref:\s+refs\/\S+|[0-9a-f]{40}|[0-9a-f]{64})$/i.test(content.trim());
|
|
161
|
+
}
|
|
162
|
+
function assertRecoverableMetadata(worktree, repoRoot, probe) {
|
|
163
|
+
const dotGit = path.join(worktree, '.git');
|
|
164
|
+
let dotGitStat;
|
|
165
|
+
try {
|
|
166
|
+
dotGitStat = fs.lstatSync(dotGit);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error.code === 'ENOENT')
|
|
170
|
+
return;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
if (dotGitStat.isSymbolicLink() || dotGitStat.isDirectory()) {
|
|
174
|
+
throw new Error('WORKTREE_RECOVERY_METADATA_NOT_ELIGIBLE');
|
|
175
|
+
}
|
|
176
|
+
if (!dotGitStat.isFile())
|
|
177
|
+
throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
|
|
178
|
+
const content = fs.readFileSync(dotGit, 'utf8').trim();
|
|
179
|
+
const match = /^gitdir:\s*(.+)$/i.exec(content);
|
|
180
|
+
if (!match)
|
|
181
|
+
throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
|
|
182
|
+
const commonDir = fs.realpathSync.native(path.resolve(repoRoot, probeText(probe, repoRoot, [
|
|
183
|
+
'rev-parse', '--git-common-dir'
|
|
184
|
+
]).trim()));
|
|
185
|
+
const adminRoot = path.join(commonDir, 'worktrees');
|
|
186
|
+
const adminPath = path.resolve(path.dirname(dotGit), match[1]);
|
|
187
|
+
const relative = path.relative(adminRoot, adminPath);
|
|
188
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative) || relative.includes(path.sep)) {
|
|
189
|
+
throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
|
|
190
|
+
}
|
|
191
|
+
if (!fs.existsSync(adminPath))
|
|
192
|
+
return;
|
|
193
|
+
const adminStat = fs.lstatSync(adminPath);
|
|
194
|
+
if (adminStat.isSymbolicLink())
|
|
195
|
+
throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
|
|
196
|
+
if (!adminStat.isDirectory())
|
|
197
|
+
return;
|
|
198
|
+
const requiredFiles = ['HEAD', 'gitdir', 'commondir', 'index'];
|
|
199
|
+
if (requiredFiles.some((name) => !fs.existsSync(path.join(adminPath, name))))
|
|
200
|
+
return;
|
|
201
|
+
for (const name of requiredFiles) {
|
|
202
|
+
const filePath = path.join(adminPath, name);
|
|
203
|
+
const stat = fs.lstatSync(filePath);
|
|
204
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
205
|
+
throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
|
|
206
|
+
}
|
|
207
|
+
const adminGitdir = fs.readFileSync(path.join(adminPath, 'gitdir'), 'utf8').trim();
|
|
208
|
+
const adminCommonDir = fs.readFileSync(path.join(adminPath, 'commondir'), 'utf8').trim();
|
|
209
|
+
const adminGitdirPath = adminGitdir
|
|
210
|
+
? fs.realpathSync.native(path.resolve(adminPath, adminGitdir))
|
|
211
|
+
: '';
|
|
212
|
+
const canonicalDotGit = fs.realpathSync.native(dotGit);
|
|
213
|
+
const adminCommonDirPath = adminCommonDir
|
|
214
|
+
? fs.realpathSync.native(path.resolve(adminPath, adminCommonDir))
|
|
215
|
+
: '';
|
|
216
|
+
if (!adminGitdir || adminGitdirPath !== canonicalDotGit
|
|
217
|
+
|| !adminCommonDir || adminCommonDirPath !== commonDir) {
|
|
218
|
+
throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
|
|
219
|
+
}
|
|
220
|
+
const headContent = fs.readFileSync(path.join(adminPath, 'HEAD'), 'utf8');
|
|
221
|
+
const indexContent = fs.readFileSync(path.join(adminPath, 'index'));
|
|
222
|
+
if (!hasValidHeadContent(headContent) || indexContent.subarray(0, 4).toString('ascii') !== 'DIRC')
|
|
223
|
+
return;
|
|
224
|
+
const validationEnv = { ...process.env };
|
|
225
|
+
for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE', 'GIT_WORK_TREE', 'GIT_PREFIX']) {
|
|
226
|
+
delete validationEnv[key];
|
|
227
|
+
}
|
|
228
|
+
validationEnv.GIT_DIR = adminPath;
|
|
229
|
+
validationEnv.GIT_WORK_TREE = path.resolve(worktree);
|
|
230
|
+
if (!probeMetadataCommand(probe, repoRoot, ['rev-parse', '--verify', 'HEAD^{commit}'], validationEnv))
|
|
231
|
+
return;
|
|
232
|
+
if (!probeMetadataCommand(probe, repoRoot, ['ls-files', '--stage', '-z'], validationEnv))
|
|
233
|
+
return;
|
|
234
|
+
throw new Error('WORKTREE_RECOVERY_METADATA_REGISTERED');
|
|
235
|
+
}
|
|
236
|
+
function parseRecoveredStatus(raw) {
|
|
237
|
+
const records = raw.split('\0');
|
|
238
|
+
const changes = [];
|
|
239
|
+
for (let index = 0; index < records.length;) {
|
|
240
|
+
const status = records[index++] ?? '';
|
|
241
|
+
if (!status)
|
|
242
|
+
continue;
|
|
243
|
+
const code = status[0] ?? '';
|
|
244
|
+
if (code === 'R' || code === 'C') {
|
|
245
|
+
const originalPath = records[index++];
|
|
246
|
+
const nextPath = records[index++];
|
|
247
|
+
if (!originalPath || !nextPath)
|
|
248
|
+
throw new Error('Unable to parse recovered Git rename record');
|
|
249
|
+
changes.push({ indexStatus: code, worktreeStatus: ' ', path: nextPath, originalPath });
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const filePath = records[index++];
|
|
253
|
+
if (!filePath)
|
|
254
|
+
throw new Error('Unable to parse recovered Git status record');
|
|
255
|
+
changes.push({
|
|
256
|
+
indexStatus: code === 'D' ? ' ' : code,
|
|
257
|
+
worktreeStatus: code === 'D' ? 'D' : ' ',
|
|
258
|
+
path: filePath
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return changes;
|
|
262
|
+
}
|
|
263
|
+
function captureRecoveredOnce(worktree, recovery, probe) {
|
|
264
|
+
const resolvedWorktree = path.resolve(worktree);
|
|
265
|
+
const resolvedBase = path.resolve(recovery.worktreeBase);
|
|
266
|
+
const relative = path.relative(resolvedBase, resolvedWorktree);
|
|
267
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
268
|
+
throw new Error(`WORKTREE_RECOVERY_PATH_INVALID: ${resolvedWorktree}`);
|
|
269
|
+
}
|
|
270
|
+
const stat = fs.lstatSync(resolvedWorktree);
|
|
271
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
272
|
+
throw new Error('WORKTREE_RECOVERY_PATH_INVALID');
|
|
273
|
+
const canonicalBase = fs.realpathSync.native(resolvedBase);
|
|
274
|
+
const canonicalWorktree = fs.realpathSync.native(resolvedWorktree);
|
|
275
|
+
const canonicalRelative = path.relative(canonicalBase, canonicalWorktree);
|
|
276
|
+
if (!canonicalRelative || canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {
|
|
277
|
+
throw new Error('WORKTREE_RECOVERY_PATH_INVALID');
|
|
278
|
+
}
|
|
279
|
+
const resolvedWorkspace = resolveSandboxTarget(recovery.branch, recovery.repoRoot).workspace;
|
|
280
|
+
if (recovery.identitySource === 'branch-only' && resolvedWorkspace.mode !== 'branch-only') {
|
|
281
|
+
throw new Error('WORKTREE_RECOVERY_IDENTITY_CHANGED');
|
|
282
|
+
}
|
|
283
|
+
if (recovery.identitySource === 'task-bound'
|
|
284
|
+
&& (resolvedWorkspace.mode !== 'task-bound' || resolvedWorkspace.taskId !== recovery.taskId)) {
|
|
285
|
+
throw new Error('WORKTREE_RECOVERY_IDENTITY_CHANGED');
|
|
286
|
+
}
|
|
287
|
+
assertRecoverableMetadata(resolvedWorktree, recovery.repoRoot, probe);
|
|
288
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-recovered-index-'));
|
|
289
|
+
const env = { ...process.env };
|
|
290
|
+
for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_PREFIX'])
|
|
291
|
+
delete env[key];
|
|
292
|
+
env.GIT_INDEX_FILE = path.join(tempDir, 'index');
|
|
293
|
+
env.GIT_WORK_TREE = resolvedWorktree;
|
|
294
|
+
try {
|
|
295
|
+
const baseline = probeTextWithEnv(probe, recovery.repoRoot, [
|
|
296
|
+
'rev-parse', '--verify', `refs/heads/${recovery.branch}^{commit}`
|
|
297
|
+
], env).trim();
|
|
298
|
+
if (!baseline)
|
|
299
|
+
throw new Error(`WORKTREE_RECOVERY_BRANCH_INVALID: ${recovery.branch}`);
|
|
300
|
+
probeTextWithEnv(probe, recovery.repoRoot, ['read-tree', baseline], env);
|
|
301
|
+
probeTextWithEnv(probe, recovery.repoRoot, ['add', '-A', '--', '.'], env);
|
|
302
|
+
const status = probeTextWithEnv(probe, recovery.repoRoot, [
|
|
303
|
+
'diff', '--cached', '--name-status', '--find-renames', '-z', '--', '.'
|
|
304
|
+
], env);
|
|
305
|
+
const changes = parseRecoveredStatus(status);
|
|
306
|
+
const hash = createHash('sha256');
|
|
307
|
+
updatePart(hash, 'source', 'recovered');
|
|
308
|
+
updatePart(hash, 'worktree', resolvedWorktree);
|
|
309
|
+
updatePart(hash, 'repo-root', path.resolve(recovery.repoRoot));
|
|
310
|
+
updatePart(hash, 'branch', recovery.branch);
|
|
311
|
+
updatePart(hash, 'baseline', baseline);
|
|
312
|
+
updatePart(hash, 'identity-source', recovery.identitySource);
|
|
313
|
+
updatePart(hash, 'task-id', recovery.taskId ?? '');
|
|
314
|
+
updatePart(hash, 'status', status);
|
|
315
|
+
for (const change of changes) {
|
|
316
|
+
hashPath(hash, resolvedWorktree, change.path, probe);
|
|
317
|
+
if (change.originalPath)
|
|
318
|
+
hashPath(hash, resolvedWorktree, change.originalPath, probe);
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
worktree: resolvedWorktree,
|
|
322
|
+
branch: recovery.branch,
|
|
323
|
+
head: baseline,
|
|
324
|
+
changes,
|
|
325
|
+
identity: hash.digest('hex'),
|
|
326
|
+
source: 'recovered',
|
|
327
|
+
recovery: { ...recovery }
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
141
334
|
function inspectWorktree(worktree, { probe = runProbe } = {}) {
|
|
142
335
|
const resolvedWorktree = path.resolve(worktree);
|
|
143
336
|
try {
|
|
@@ -156,6 +349,24 @@ function inspectWorktree(worktree, { probe = runProbe } = {}) {
|
|
|
156
349
|
};
|
|
157
350
|
}
|
|
158
351
|
}
|
|
352
|
+
function inspectRecoveredWorktree(worktree, recovery, { probe = runProbe } = {}) {
|
|
353
|
+
const resolvedWorktree = path.resolve(worktree);
|
|
354
|
+
try {
|
|
355
|
+
const first = captureRecoveredOnce(resolvedWorktree, recovery, probe);
|
|
356
|
+
const second = captureRecoveredOnce(resolvedWorktree, recovery, probe);
|
|
357
|
+
if (first.identity !== second.identity) {
|
|
358
|
+
return { status: 'failed', worktree: resolvedWorktree, message: 'Snapshot changed while recovering worktree' };
|
|
359
|
+
}
|
|
360
|
+
return { status: second.changes.length === 0 ? 'clean' : 'dirty', snapshot: second };
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
return {
|
|
364
|
+
status: 'failed',
|
|
365
|
+
worktree: resolvedWorktree,
|
|
366
|
+
message: error instanceof Error ? error.message : String(error)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
159
370
|
function inspectWorktrees(worktrees) {
|
|
160
371
|
return [...new Set(worktrees.map((worktree) => path.resolve(worktree)))].sort()
|
|
161
372
|
.map((worktree) => inspectWorktree(worktree));
|
|
@@ -171,7 +382,9 @@ function createDiscardPermit(snapshot) {
|
|
|
171
382
|
return { mode: 'discard', snapshot };
|
|
172
383
|
}
|
|
173
384
|
function verifyWorktreePermit(permit, options = {}) {
|
|
174
|
-
const current =
|
|
385
|
+
const current = permit.snapshot.source === 'recovered' && permit.snapshot.recovery
|
|
386
|
+
? inspectRecoveredWorktree(permit.snapshot.worktree, permit.snapshot.recovery, options)
|
|
387
|
+
: inspectWorktree(permit.snapshot.worktree, options);
|
|
175
388
|
if (current.status === 'failed')
|
|
176
389
|
throw new Error(`Unable to verify worktree permit: ${current.message}`);
|
|
177
390
|
if (current.snapshot.identity !== permit.snapshot.identity || (permit.mode === 'clean' && current.status !== 'clean')) {
|
|
@@ -192,5 +405,5 @@ function formatWorktreeSnapshot(snapshot) {
|
|
|
192
405
|
`Snapshot identity: ${snapshot.identity}`
|
|
193
406
|
].join('\n');
|
|
194
407
|
}
|
|
195
|
-
export { createCleanPermit, createDiscardPermit, formatWorktreeSnapshot, inspectWorktree, inspectWorktrees, verifyWorktreePermit };
|
|
408
|
+
export { createCleanPermit, createDiscardPermit, formatWorktreeSnapshot, inspectRecoveredWorktree, inspectWorktree, inspectWorktrees, verifyWorktreePermit };
|
|
196
409
|
//# sourceMappingURL=worktree-safety.js.map
|
|
@@ -97,6 +97,56 @@ export function getProcessStartTime(pid, platform = process.platform) {
|
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
export function getProcessIdentityState(identity, platform = process.platform) {
|
|
101
|
+
if (!Number.isInteger(identity.pid) || identity.pid <= 0 || !Number.isSafeInteger(identity.startTime)) {
|
|
102
|
+
return 'dead';
|
|
103
|
+
}
|
|
104
|
+
if (platform === 'linux') {
|
|
105
|
+
try {
|
|
106
|
+
const stat = parseLinuxProcessStat(fs.readFileSync(`/proc/${identity.pid}/stat`, 'utf8'));
|
|
107
|
+
if (stat === null)
|
|
108
|
+
return 'unknown';
|
|
109
|
+
if (stat.state === 'Z')
|
|
110
|
+
return 'dead';
|
|
111
|
+
return stat.startTime === identity.startTime ? 'alive' : 'dead';
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return error.code === 'ENOENT' ? 'dead' : 'unknown';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
process.kill(identity.pid, 0);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
const code = error.code;
|
|
122
|
+
if (code === 'ESRCH')
|
|
123
|
+
return 'dead';
|
|
124
|
+
if (code !== 'EPERM')
|
|
125
|
+
return 'unknown';
|
|
126
|
+
}
|
|
127
|
+
const query = buildProcessStartTimeQuery(identity.pid, platform);
|
|
128
|
+
if (query === null)
|
|
129
|
+
return 'unknown';
|
|
130
|
+
try {
|
|
131
|
+
const output = execFileSync(query.command, query.args, {
|
|
132
|
+
encoding: 'utf8',
|
|
133
|
+
env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
|
|
134
|
+
}).trim();
|
|
135
|
+
if (output.length === 0)
|
|
136
|
+
return 'unknown';
|
|
137
|
+
const startTime = platform === 'win32'
|
|
138
|
+
? new Date(output).getTime()
|
|
139
|
+
: platform === 'darwin'
|
|
140
|
+
? parseDarwinStartTime(output)
|
|
141
|
+
: null;
|
|
142
|
+
if (startTime === null || Number.isNaN(startTime))
|
|
143
|
+
return 'unknown';
|
|
144
|
+
return startTime === identity.startTime ? 'alive' : 'dead';
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return 'unknown';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
100
150
|
function parsePidRecord(raw) {
|
|
101
151
|
const trimmed = raw.trim();
|
|
102
152
|
if (/^[1-9]\d*$/.test(trimmed)) {
|
|
@@ -200,6 +250,6 @@ export function removePidRecordIfMatches(pidFile, expected) {
|
|
|
200
250
|
return removePidFileIfMatches(pidFile, state.snapshot);
|
|
201
251
|
}
|
|
202
252
|
export function processIdentityMatches(identity) {
|
|
203
|
-
return
|
|
253
|
+
return getProcessIdentityState(identity) === 'alive';
|
|
204
254
|
}
|
|
205
255
|
//# sourceMappingURL=process-state.js.map
|
|
@@ -5,6 +5,7 @@ import { locateActivityLog, appendActivityEntry } from './activity-log.js';
|
|
|
5
5
|
import { assertWritableInventory, buildArtifactLinkSection, inspectTaskArtifacts, parseArtifactName } from './artifact-lifecycle.js';
|
|
6
6
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
7
7
|
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
8
|
+
import { allowsManualOverride } from './guard-override.js';
|
|
8
9
|
const HEAD_RE = /^[0-9a-f]{40}$/;
|
|
9
10
|
const REVIEW_ACTION_RE = /^Review PR \(Round ([1-9]\d*)\)( \[started\])?$/;
|
|
10
11
|
const VERDICTS = new Set(['approved', 'changes-requested', 'commented']);
|
|
@@ -292,9 +293,11 @@ export function applyPrReviewActivityIntent(intent, options = {}) {
|
|
|
292
293
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
293
294
|
if (!resolved.ok)
|
|
294
295
|
return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
295
|
-
if (resolved.state !== 'active') {
|
|
296
|
+
if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'activity-intent', 'TASK_STATE_MISMATCH')) {
|
|
296
297
|
return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
297
298
|
}
|
|
299
|
+
if (options.lockAlreadyHeld)
|
|
300
|
+
return applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });
|
|
298
301
|
try {
|
|
299
302
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-activity:${intent.kind}`, () => applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot }), options.lockOptions);
|
|
300
303
|
}
|