@fitlab-ai/agent-infra 0.11.13 → 0.11.14
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/dist/bin/internal-cli.js +2 -7
- package/dist/lib/agent-clients/adapter.js +0 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
- package/dist/lib/agent-clients/adapters/codex.js +0 -2
- package/dist/lib/internal/cli-route-inventory.js +0 -1
- package/dist/lib/internal/task-finalization.js +6 -4
- package/dist/lib/internal/task-lifecycle.js +3 -1
- package/dist/lib/internal/task-operation-registry.js +1 -3
- package/dist/lib/internal/task-validate.js +14 -124
- package/dist/lib/platform/release-notes.js +2 -2
- package/dist/lib/sandbox/commands/control-status.js +0 -1
- package/dist/lib/sandbox/commands/create.js +5 -4
- package/dist/lib/sandbox/config.js +12 -0
- package/dist/lib/sandbox/control/audit.js +1 -2
- package/dist/lib/sandbox/control/client.js +12 -47
- package/dist/lib/sandbox/control/executor.js +6 -10
- package/dist/lib/sandbox/control/host-recovery.js +89 -27
- package/dist/lib/sandbox/control/lifecycle.js +20 -16
- package/dist/lib/sandbox/control/protocol.js +8 -36
- package/dist/lib/sandbox/control/server.js +2 -21
- package/dist/lib/sandbox/environment.js +7 -8
- package/dist/lib/sandbox/mounts.js +71 -59
- package/dist/lib/sandbox/recovery.js +2 -3
- package/dist/lib/sandbox/removal.js +19 -4
- package/dist/lib/sandbox/workspace-view.js +41 -4
- package/dist/lib/task/control-authority.js +39 -17
- package/dist/lib/task/control-recovery.js +1 -2
- package/dist/lib/task/finalization.js +13 -2
- package/dist/lib/task/lifecycle.js +92 -33
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +0 -27
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -4
- package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
- package/lib/agent-clients/adapters/codex.ts +0 -2
- package/lib/internal/cli-route-inventory.ts +0 -1
- package/lib/internal/task-finalization.ts +6 -4
- package/lib/internal/task-lifecycle.ts +3 -1
- package/lib/internal/task-operation-registry.ts +1 -3
- package/lib/internal/task-validate.ts +13 -127
- package/lib/platform/release-notes.ts +2 -2
- package/lib/sandbox/commands/control-status.ts +0 -1
- package/lib/sandbox/commands/create.ts +5 -4
- package/lib/sandbox/config.ts +13 -0
- package/lib/sandbox/control/audit.ts +1 -2
- package/lib/sandbox/control/client.ts +13 -61
- package/lib/sandbox/control/executor.ts +7 -13
- package/lib/sandbox/control/host-recovery.ts +94 -21
- package/lib/sandbox/control/lifecycle.ts +18 -16
- package/lib/sandbox/control/protocol.ts +9 -45
- package/lib/sandbox/control/server.ts +2 -17
- package/lib/sandbox/environment.ts +7 -8
- package/lib/sandbox/mounts.ts +92 -75
- package/lib/sandbox/recovery.ts +2 -3
- package/lib/sandbox/removal.ts +16 -4
- package/lib/sandbox/workspace-view.ts +40 -5
- package/lib/task/control-authority.ts +40 -26
- package/lib/task/control-recovery.ts +2 -3
- package/lib/task/finalization.ts +15 -2
- package/lib/task/lifecycle.ts +103 -31
- package/package.json +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
- package/templates/.agents/rules/task-short-id.en.md +18 -12
- package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
- package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
- package/dist/lib/sandbox/control/agent-operation.js +0 -34
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
- package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
- package/lib/sandbox/control/agent-operation.ts +0 -49
|
@@ -12,14 +12,13 @@ export const SANDBOX_CONTROL_STATUS_MOUNT = '/run/agent-infra/control-status';
|
|
|
12
12
|
const TASK_MARKER_KEYS = [
|
|
13
13
|
'AGENT_INFRA_SANDBOX',
|
|
14
14
|
'AGENT_INFRA_TASK_ID',
|
|
15
|
-
'AGENT_INFRA_CONTROL_TOKEN',
|
|
16
15
|
'AGENT_INFRA_CONTROL_GENERATION',
|
|
17
16
|
'AGENT_INFRA_CONTROL_DIR',
|
|
17
|
+
'AGENT_INFRA_CONTROL_ROOT_DIR',
|
|
18
18
|
'AGENT_INFRA_CONTROL_STATUS_DIR',
|
|
19
19
|
'AGENT_INFRA_RUNTIME_DIR'
|
|
20
20
|
] as const;
|
|
21
21
|
const TASK_CONTROL_MARKER_KEYS = [
|
|
22
|
-
'AGENT_INFRA_CONTROL_TOKEN',
|
|
23
22
|
'AGENT_INFRA_CONTROL_GENERATION',
|
|
24
23
|
'AGENT_INFRA_CONTROL_DIR',
|
|
25
24
|
'AGENT_INFRA_CONTROL_STATUS_DIR'
|
|
@@ -88,7 +87,6 @@ export function resolveSandboxControlTransport(
|
|
|
88
87
|
): SandboxControlTransportDecision {
|
|
89
88
|
const markerState = sandboxTaskMarkerState(env);
|
|
90
89
|
const hasAnyMarker = markerState !== 'none'
|
|
91
|
-
|| Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
|
|
92
90
|
|| Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
|
|
93
91
|
const fixedStatusDir = options.statusMountPath ?? SANDBOX_CONTROL_STATUS_MOUNT;
|
|
94
92
|
const fixedStatusProbe = path.isAbsolute(fixedStatusDir)
|
|
@@ -109,6 +107,10 @@ export function resolveSandboxControlTransport(
|
|
|
109
107
|
if (!hasCompleteConfig) {
|
|
110
108
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
111
109
|
}
|
|
110
|
+
if (markerState === 'task-bound'
|
|
111
|
+
&& (!env.AGENT_INFRA_CONTROL_ROOT_DIR || !path.isAbsolute(env.AGENT_INFRA_CONTROL_ROOT_DIR))) {
|
|
112
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
113
|
+
}
|
|
112
114
|
const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
113
115
|
const configuredStatusProbe = configuredStatusDir && path.isAbsolute(configuredStatusDir)
|
|
114
116
|
? nativeDirectoryProbe(configuredStatusDir) : 'absent';
|
|
@@ -119,7 +121,7 @@ export function resolveSandboxControlTransport(
|
|
|
119
121
|
? configuredStatusProbe === 'present' ? configuredStatusDir : null
|
|
120
122
|
: fixedStatusMounted ? fixedStatusDir : null;
|
|
121
123
|
const statusMounted = statusDir !== null;
|
|
122
|
-
if (env.AGENT_INFRA_EXECUTOR_MANIFEST
|
|
124
|
+
if (env.AGENT_INFRA_EXECUTOR_MANIFEST) {
|
|
123
125
|
return { kind: 'fail-closed', reasonCode: 'TASK_CONTROL_TRANSPORT_INVALID' };
|
|
124
126
|
}
|
|
125
127
|
const taskBound = Boolean(env.AGENT_INFRA_TASK_ID);
|
|
@@ -134,9 +136,6 @@ export function resolveSandboxControlTransport(
|
|
|
134
136
|
? 'SANDBOX_CONTROL_IDENTITY_MISSING'
|
|
135
137
|
: 'SANDBOX_CONTROL_IDENTITY_MALFORMED' };
|
|
136
138
|
}
|
|
137
|
-
if (sentinel.generation !== env.AGENT_INFRA_CONTROL_GENERATION) {
|
|
138
|
-
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH' };
|
|
139
|
-
}
|
|
140
139
|
if (sentinel.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
|
|
141
140
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH' };
|
|
142
141
|
}
|
|
@@ -144,7 +143,7 @@ export function resolveSandboxControlTransport(
|
|
|
144
143
|
&& (sentinel.taskId !== env.AGENT_INFRA_TASK_ID || !runtime)) {
|
|
145
144
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
|
|
146
145
|
}
|
|
147
|
-
if (sentinel.mode === 'branch-only' && (taskBound || runtime)) {
|
|
146
|
+
if (sentinel.mode === 'branch-only' && (taskBound || runtime || Boolean(env.AGENT_INFRA_CONTROL_ROOT_DIR))) {
|
|
148
147
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
|
|
149
148
|
}
|
|
150
149
|
return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
|
package/lib/sandbox/mounts.ts
CHANGED
|
@@ -15,6 +15,89 @@ export type SandboxBindMountDeclaration = {
|
|
|
15
15
|
readOnly: boolean;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
function workspaceStateMounts(workspaceViewRoot: string, taskBound: boolean): SandboxBindMountDeclaration[] {
|
|
19
|
+
return sandboxWorkspaceViewStatePaths(workspaceViewRoot)
|
|
20
|
+
// The writable completed-task mount below is nested under this path. Keep
|
|
21
|
+
// the worktree target visible so Docker can mount it without creating a
|
|
22
|
+
// mountpoint inside a read-only parent bind mount.
|
|
23
|
+
.filter(({ state }) => !(taskBound && state === 'completed'))
|
|
24
|
+
.map(({ state, hostPath }) => taskBound && state === 'active'
|
|
25
|
+
? {
|
|
26
|
+
hostPaths: [path.join(hostPath, '.short-ids.json')],
|
|
27
|
+
containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
|
|
28
|
+
readOnly: true
|
|
29
|
+
}
|
|
30
|
+
: {
|
|
31
|
+
hostPaths: [hostPath],
|
|
32
|
+
containerPath: path.posix.join('/workspace/.agents/workspace', state),
|
|
33
|
+
readOnly: true
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertGitMetadataPair(overrides: {
|
|
39
|
+
gitCommonDir?: string;
|
|
40
|
+
worktreeGitFile?: string;
|
|
41
|
+
}): void {
|
|
42
|
+
if (Boolean(overrides.gitCommonDir) !== Boolean(overrides.worktreeGitFile)) {
|
|
43
|
+
throw new Error('SANDBOX_GIT_METADATA_INCOMPLETE');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function gitCommonMount(gitCommonDir?: string): SandboxBindMountDeclaration[] {
|
|
48
|
+
return gitCommonDir ? [{ hostPaths: [gitCommonDir], containerPath: SANDBOX_GIT_COMMON_DIR, readOnly: false }] : [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function worktreeGitFileMount(worktreeGitFile?: string): SandboxBindMountDeclaration[] {
|
|
52
|
+
return worktreeGitFile ? [{ hostPaths: [worktreeGitFile], containerPath: '/workspace/.git', readOnly: true }] : [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function taskWorkspaceMounts(taskSources: string[], taskBound: boolean, overrides: {
|
|
56
|
+
taskId?: string;
|
|
57
|
+
completedTaskSource?: string;
|
|
58
|
+
}): SandboxBindMountDeclaration[] {
|
|
59
|
+
if (!taskBound) return [];
|
|
60
|
+
const taskId = overrides.taskId!;
|
|
61
|
+
const mounts: SandboxBindMountDeclaration[] = [{
|
|
62
|
+
hostPaths: taskSources,
|
|
63
|
+
containerPath: path.posix.join('/workspace/.agents/workspace/active', taskId),
|
|
64
|
+
readOnly: false
|
|
65
|
+
}];
|
|
66
|
+
if (overrides.completedTaskSource) mounts.push({
|
|
67
|
+
hostPaths: [overrides.completedTaskSource],
|
|
68
|
+
containerPath: path.posix.join('/workspace/.agents/workspace/completed', taskId),
|
|
69
|
+
readOnly: false
|
|
70
|
+
});
|
|
71
|
+
return mounts;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sharedMounts(config: Pick<SandboxConfig, 'repoRoot' | 'worktreeBase' | 'shareBase' | 'shellConfigBase'>, branch: string,
|
|
75
|
+
shellConfigHostDir?: string): SandboxBindMountDeclaration[] {
|
|
76
|
+
return [{
|
|
77
|
+
hostPaths: [shareCommonDir(config)], containerPath: '/share/common', readOnly: false
|
|
78
|
+
}, {
|
|
79
|
+
hostPaths: [shareBranchDir(config, branch)], containerPath: '/share/branch', readOnly: false
|
|
80
|
+
}, {
|
|
81
|
+
hostPaths: shellConfigHostDir ? [shellConfigHostDir] : shellConfigDirCandidates(config, branch),
|
|
82
|
+
containerPath: '/home/devuser/.host-shell-config', readOnly: true
|
|
83
|
+
}];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function controlMounts(overrides: { controlDir: string; controlRootDir?: string; controlStatusDir: string; runtimeDir?: string }, taskBound: boolean): SandboxBindMountDeclaration[] {
|
|
87
|
+
const mounts: SandboxBindMountDeclaration[] = [{
|
|
88
|
+
hostPaths: [overrides.controlStatusDir], containerPath: '/run/agent-infra/control-status', readOnly: true
|
|
89
|
+
}, {
|
|
90
|
+
hostPaths: [overrides.controlDir], containerPath: '/run/agent-infra/control', readOnly: false
|
|
91
|
+
}];
|
|
92
|
+
if (taskBound && overrides.runtimeDir) mounts.push({
|
|
93
|
+
hostPaths: [overrides.runtimeDir], containerPath: '/run/agent-infra/runtime', readOnly: false
|
|
94
|
+
});
|
|
95
|
+
if (taskBound && overrides.controlRootDir) mounts.push({
|
|
96
|
+
hostPaths: [overrides.controlRootDir], containerPath: '/run/agent-infra/control-root', readOnly: false
|
|
97
|
+
});
|
|
98
|
+
return mounts;
|
|
99
|
+
}
|
|
100
|
+
|
|
18
101
|
export function sandboxCoreBindMounts(
|
|
19
102
|
config: Pick<
|
|
20
103
|
SandboxConfig,
|
|
@@ -26,6 +109,7 @@ export function sandboxCoreBindMounts(
|
|
|
26
109
|
shellConfigHostDir?: string;
|
|
27
110
|
workspaceViewRoot: string;
|
|
28
111
|
controlDir: string;
|
|
112
|
+
controlRootDir?: string;
|
|
29
113
|
controlStatusDir: string;
|
|
30
114
|
gitCommonDir?: string;
|
|
31
115
|
worktreeGitFile?: string;
|
|
@@ -37,30 +121,10 @@ export function sandboxCoreBindMounts(
|
|
|
37
121
|
): SandboxBindMountDeclaration[] {
|
|
38
122
|
const taskSources = overrides.taskSources ?? [];
|
|
39
123
|
const taskBound = Boolean(taskSources.length > 0 && overrides.taskId);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
? {
|
|
43
|
-
hostPaths: [path.join(hostPath, '.short-ids.json')],
|
|
44
|
-
containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
|
|
45
|
-
readOnly: true
|
|
46
|
-
}
|
|
47
|
-
: {
|
|
48
|
-
hostPaths: [hostPath],
|
|
49
|
-
containerPath: path.posix.join('/workspace/.agents/workspace', state),
|
|
50
|
-
readOnly: true
|
|
51
|
-
}
|
|
52
|
-
);
|
|
53
|
-
if (Boolean(overrides.gitCommonDir) !== Boolean(overrides.worktreeGitFile)) {
|
|
54
|
-
throw new Error('SANDBOX_GIT_METADATA_INCOMPLETE');
|
|
55
|
-
}
|
|
56
|
-
const gitCommonMount = overrides.gitCommonDir
|
|
57
|
-
? { hostPaths: [overrides.gitCommonDir], containerPath: SANDBOX_GIT_COMMON_DIR, readOnly: false }
|
|
58
|
-
: null;
|
|
59
|
-
const worktreeGitFileMount = overrides.worktreeGitFile
|
|
60
|
-
? { hostPaths: [overrides.worktreeGitFile], containerPath: '/workspace/.git', readOnly: true }
|
|
61
|
-
: null;
|
|
124
|
+
assertGitMetadataPair(overrides);
|
|
125
|
+
if (taskBound && !overrides.controlRootDir) throw new Error('SANDBOX_CONTROL_ROOT_MISSING');
|
|
62
126
|
const mounts: SandboxBindMountDeclaration[] = [
|
|
63
|
-
...(
|
|
127
|
+
...gitCommonMount(overrides.gitCommonDir),
|
|
64
128
|
{
|
|
65
129
|
hostPaths: overrides.worktree
|
|
66
130
|
? [overrides.worktree]
|
|
@@ -68,58 +132,11 @@ export function sandboxCoreBindMounts(
|
|
|
68
132
|
containerPath: '/workspace',
|
|
69
133
|
readOnly: false
|
|
70
134
|
},
|
|
71
|
-
...(
|
|
72
|
-
...
|
|
135
|
+
...worktreeGitFileMount(overrides.worktreeGitFile),
|
|
136
|
+
...workspaceStateMounts(overrides.workspaceViewRoot, taskBound),
|
|
137
|
+
...taskWorkspaceMounts(taskSources, taskBound, overrides),
|
|
138
|
+
...sharedMounts(config, branch, overrides.shellConfigHostDir),
|
|
139
|
+
...controlMounts(overrides, taskBound)
|
|
73
140
|
];
|
|
74
|
-
if (taskBound) {
|
|
75
|
-
const taskMountPath = path.posix.join('/workspace/.agents/workspace/active', overrides.taskId!);
|
|
76
|
-
mounts.push({
|
|
77
|
-
hostPaths: taskSources,
|
|
78
|
-
containerPath: taskMountPath,
|
|
79
|
-
readOnly: false
|
|
80
|
-
});
|
|
81
|
-
if (overrides.completedTaskSource) {
|
|
82
|
-
mounts.push({
|
|
83
|
-
hostPaths: [overrides.completedTaskSource],
|
|
84
|
-
containerPath: path.posix.join('/workspace/.agents/workspace/completed', overrides.taskId!),
|
|
85
|
-
readOnly: false
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
mounts.push(
|
|
90
|
-
{
|
|
91
|
-
hostPaths: [shareCommonDir(config)],
|
|
92
|
-
containerPath: '/share/common',
|
|
93
|
-
readOnly: false
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
hostPaths: [shareBranchDir(config, branch)],
|
|
97
|
-
containerPath: '/share/branch',
|
|
98
|
-
readOnly: false
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
hostPaths: overrides.shellConfigHostDir
|
|
102
|
-
? [overrides.shellConfigHostDir]
|
|
103
|
-
: shellConfigDirCandidates(config, branch),
|
|
104
|
-
containerPath: '/home/devuser/.host-shell-config',
|
|
105
|
-
readOnly: true
|
|
106
|
-
}
|
|
107
|
-
);
|
|
108
|
-
mounts.push({
|
|
109
|
-
hostPaths: [overrides.controlStatusDir],
|
|
110
|
-
containerPath: '/run/agent-infra/control-status',
|
|
111
|
-
readOnly: true
|
|
112
|
-
}, {
|
|
113
|
-
hostPaths: [overrides.controlDir],
|
|
114
|
-
containerPath: '/run/agent-infra/control',
|
|
115
|
-
readOnly: false
|
|
116
|
-
});
|
|
117
|
-
if (taskBound && overrides.runtimeDir) {
|
|
118
|
-
mounts.push({
|
|
119
|
-
hostPaths: [overrides.runtimeDir],
|
|
120
|
-
containerPath: '/run/agent-infra/runtime',
|
|
121
|
-
readOnly: false
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
141
|
return mounts;
|
|
125
142
|
}
|
package/lib/sandbox/recovery.ts
CHANGED
|
@@ -342,7 +342,6 @@ async function ensureSandboxControlBroker(params: {
|
|
|
342
342
|
}): Promise<void> {
|
|
343
343
|
const control = sandboxControlPaths({
|
|
344
344
|
base: params.config.controlBase ?? path.join(params.config.home, '.agent-infra', 'sandbox-control'),
|
|
345
|
-
repoRoot: params.config.repoRoot,
|
|
346
345
|
project: params.config.project,
|
|
347
346
|
container: params.container,
|
|
348
347
|
identity: params.workspace
|
|
@@ -753,7 +752,6 @@ function expectedMounts(params: {
|
|
|
753
752
|
});
|
|
754
753
|
const control = sandboxControlPaths({
|
|
755
754
|
base: config.controlBase ?? path.join(config.home, '.agent-infra', 'sandbox-control'),
|
|
756
|
-
repoRoot: config.repoRoot,
|
|
757
755
|
project: config.project,
|
|
758
756
|
container: params.container,
|
|
759
757
|
identity: params.workspace
|
|
@@ -778,6 +776,7 @@ function expectedMounts(params: {
|
|
|
778
776
|
}),
|
|
779
777
|
workspaceViewRoot: view.root,
|
|
780
778
|
controlDir: control.channelDir,
|
|
779
|
+
...(params.workspace.mode === 'task-bound' ? { controlRootDir: control.root } : {}),
|
|
781
780
|
controlStatusDir: control.statusDir,
|
|
782
781
|
...(taskSources === null
|
|
783
782
|
? {}
|
|
@@ -1296,7 +1295,7 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
|
|
|
1296
1295
|
let completedReentry: { manifest: SandboxControlManifest; evidence: Awaited<ReturnType<typeof prepareCompletedReentry>> } | null = null;
|
|
1297
1296
|
if (params.reentry === 'completed' && params.workspace?.mode === 'task-bound') {
|
|
1298
1297
|
const control = sandboxControlPaths({
|
|
1299
|
-
base: params.config.controlBase,
|
|
1298
|
+
base: params.config.controlBase, project: params.config.project,
|
|
1300
1299
|
container: params.row.name, identity: params.workspace
|
|
1301
1300
|
});
|
|
1302
1301
|
const manifest = readSandboxControlManifest(control.manifestPath);
|
package/lib/sandbox/removal.ts
CHANGED
|
@@ -829,7 +829,7 @@ function resolveRmTarget(
|
|
|
829
829
|
? matchedContainers
|
|
830
830
|
: [...new Set([...containerNameCandidates(config, effectiveBranch), ...matchedContainers])];
|
|
831
831
|
const controlRoots = containers.flatMap((container) => identities.map((identity) => sandboxControlPaths({
|
|
832
|
-
base: config.controlBase,
|
|
832
|
+
base: config.controlBase, project: config.project, container, identity
|
|
833
833
|
}).root));
|
|
834
834
|
const workspaceViewRoots = containers.flatMap((container) => identities.map((identity) => sandboxWorkspaceViewPaths({
|
|
835
835
|
base: config.workspaceViewBase, project: config.project, container, identity
|
|
@@ -1083,6 +1083,19 @@ async function rmPurgeCore(
|
|
|
1083
1083
|
const containers = runEngine(engine, 'docker', [
|
|
1084
1084
|
'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
|
|
1085
1085
|
]).split('\n').filter(Boolean);
|
|
1086
|
+
const controlRoots = rows.flatMap((row) => {
|
|
1087
|
+
if (!row.branch) return [];
|
|
1088
|
+
const identity = row.workspaceMode === 'task-bound' && row.taskId
|
|
1089
|
+
? { mode: 'task-bound' as const, taskId: row.taskId }
|
|
1090
|
+
: row.workspaceMode === 'branch-only'
|
|
1091
|
+
? { mode: 'branch-only' as const }
|
|
1092
|
+
: null;
|
|
1093
|
+
if (!identity) return [];
|
|
1094
|
+
return [sandboxControlPaths({
|
|
1095
|
+
base: config.controlBase, project: config.project,
|
|
1096
|
+
container: row.name, identity
|
|
1097
|
+
}).root];
|
|
1098
|
+
});
|
|
1086
1099
|
const completedTaskIds = [...new Set(rows
|
|
1087
1100
|
.filter((row) => row.workspaceMode === 'task-bound' && row.taskId)
|
|
1088
1101
|
.map((row) => row.taskId!))];
|
|
@@ -1115,9 +1128,8 @@ async function rmPurgeCore(
|
|
|
1115
1128
|
if (!isCancel(selected) && selected) fs.rmSync(config.shareBase, { recursive: true, force: true });
|
|
1116
1129
|
else cleanupCompleted = false;
|
|
1117
1130
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1131
|
+
fs.rmSync(path.join(config.workspaceViewBase, config.project), { recursive: true, force: true });
|
|
1132
|
+
for (const root of new Set(controlRoots)) fs.rmSync(root, { recursive: true, force: true });
|
|
1121
1133
|
if (cleanupCompleted) {
|
|
1122
1134
|
for (const taskId of completedTaskIds) releaseStaleShortIdRegistry(config.repoRoot, taskId);
|
|
1123
1135
|
}
|
|
@@ -4,7 +4,6 @@ import path from 'node:path';
|
|
|
4
4
|
import type { SandboxWorkspaceIdentity, SandboxWorkspaceKey } from './workspace-identity.ts';
|
|
5
5
|
import { resolveTaskWorkspace } from './task-resolver.ts';
|
|
6
6
|
import type { SandboxControlManifest } from './control/protocol.ts';
|
|
7
|
-
import { resolveTaskRuntimeRoot } from '../task/runtime-paths.ts';
|
|
8
7
|
import {
|
|
9
8
|
createSandboxControlIdentitySentinel,
|
|
10
9
|
writeSandboxControlIdentitySentinel
|
|
@@ -64,13 +63,21 @@ export type SandboxControlManifestDraft = Readonly<Omit<SandboxControlManifest,
|
|
|
64
63
|
|
|
65
64
|
export function sandboxControlPaths(params: Readonly<{
|
|
66
65
|
base: string;
|
|
67
|
-
repoRoot?: string;
|
|
68
66
|
project: string;
|
|
69
67
|
container: string;
|
|
70
68
|
identity: SandboxWorkspaceIdentity | SandboxWorkspaceKey;
|
|
71
69
|
}>): Readonly<{ root: string; channelDir: string; statusDir: string; processingDir: string; runtimeDir: string; manifestPath: string }> {
|
|
72
70
|
const root = params.identity.mode === 'task-bound'
|
|
73
|
-
?
|
|
71
|
+
? (() => {
|
|
72
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(params.identity.taskId)
|
|
73
|
+
|| !params.project || path.basename(params.project) !== params.project || params.project === '.' || params.project === '..') {
|
|
74
|
+
throw new Error('SANDBOX_CONTROL_ROOT_INVALID');
|
|
75
|
+
}
|
|
76
|
+
const candidate = path.resolve(params.base, params.project, 'tasks', params.identity.taskId);
|
|
77
|
+
const relative = path.relative(path.resolve(params.base), candidate);
|
|
78
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('SANDBOX_CONTROL_ROOT_INVALID');
|
|
79
|
+
return candidate;
|
|
80
|
+
})()
|
|
74
81
|
: (() => {
|
|
75
82
|
const digest = createHash('sha256').update('branch-only').digest('hex').slice(0, 16);
|
|
76
83
|
return path.resolve(params.base, params.project, params.container, digest);
|
|
@@ -95,6 +102,23 @@ function assertSafeDirectory(directory: string, expectedBase: string): void {
|
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
function assertSafeTaskBoundControlRoot(basePath: string, root: string): void {
|
|
106
|
+
const relative = path.relative(basePath, root);
|
|
107
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
108
|
+
throw new Error(`Sandbox control root escapes its configured base: ${root}`);
|
|
109
|
+
}
|
|
110
|
+
if (fs.existsSync(basePath) && fs.lstatSync(basePath).isSymbolicLink()) {
|
|
111
|
+
throw new Error(`Sandbox control base must not be a symbolic link: ${basePath}`);
|
|
112
|
+
}
|
|
113
|
+
let current = basePath;
|
|
114
|
+
for (const segment of relative.split(path.sep)) {
|
|
115
|
+
current = path.join(current, segment);
|
|
116
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
|
|
117
|
+
throw new Error(`Sandbox control root must not contain a symbolic link: ${current}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
function assertSafeMountTargetPath(
|
|
99
123
|
target: string,
|
|
100
124
|
worktreeRoot: string,
|
|
@@ -263,7 +287,11 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
263
287
|
}>;
|
|
264
288
|
}>): SandboxControlSetup {
|
|
265
289
|
const { root, channelDir, statusDir, processingDir, runtimeDir, manifestPath } = sandboxControlPaths(params);
|
|
266
|
-
|
|
290
|
+
if (params.identity.mode === 'task-bound') {
|
|
291
|
+
assertSafeTaskBoundControlRoot(path.resolve(params.base), root);
|
|
292
|
+
} else {
|
|
293
|
+
assertSafeDirectory(root, path.resolve(params.base));
|
|
294
|
+
}
|
|
267
295
|
if (fs.existsSync(root)) {
|
|
268
296
|
if (!params.replacementLease || path.resolve(params.replacementLease.root) !== root) {
|
|
269
297
|
throw new Error('SANDBOX_CONTROL_REPLACEMENT_REQUIRED');
|
|
@@ -329,9 +357,16 @@ export function finalizeSandboxControlManifest(
|
|
|
329
357
|
containerIdentity: { id: identity.id, labels: { ...identity.labels } },
|
|
330
358
|
authorityEvidence: identity.authorityEvidence
|
|
331
359
|
};
|
|
360
|
+
const persistedManifest = {
|
|
361
|
+
...manifest,
|
|
362
|
+
channelDir: 'channel',
|
|
363
|
+
publicStatusDir: 'public',
|
|
364
|
+
processingDir: 'processing',
|
|
365
|
+
runtimeDir: 'runtime'
|
|
366
|
+
};
|
|
332
367
|
const temporary = `${setup.manifestPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
333
368
|
try {
|
|
334
|
-
fs.writeFileSync(temporary, `${JSON.stringify(
|
|
369
|
+
fs.writeFileSync(temporary, `${JSON.stringify(persistedManifest)}\n`, { mode: 0o600, flag: 'wx' });
|
|
335
370
|
fs.renameSync(temporary, setup.manifestPath);
|
|
336
371
|
} finally {
|
|
337
372
|
fs.rmSync(temporary, { force: true });
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
4
4
|
import { isAgentClientId } from '../agent-clients/types.ts';
|
|
5
5
|
import type { AgentClientId } from '../agent-clients/types.ts';
|
|
6
|
+
import { sandboxControlPaths } from '../sandbox/workspace-view.ts';
|
|
6
7
|
import { getAgentClientAdapter } from '../agent-clients/registry.ts';
|
|
7
8
|
import {
|
|
8
9
|
activateMatchingOrchestrationDelegation,
|
|
@@ -41,18 +42,14 @@ import { resolveTaskContext, resolveTaskRef, TASK_ID_RE } from './resolve-ref.ts
|
|
|
41
42
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
42
43
|
import { verifyTaskEvent } from './verification.ts';
|
|
43
44
|
|
|
44
|
-
export type TaskControlControllerBinding = Readonly<{
|
|
45
|
-
instanceDigest: string;
|
|
46
|
-
controlGeneration: string;
|
|
47
|
-
}>;
|
|
48
|
-
|
|
49
45
|
export type TaskControlExecutionContext =
|
|
50
46
|
| Readonly<{
|
|
51
47
|
source: 'direct-host';
|
|
52
48
|
mode: 'direct-host';
|
|
53
49
|
repoRoot: string;
|
|
54
50
|
runtimeDir?: string;
|
|
55
|
-
|
|
51
|
+
controlBase?: string;
|
|
52
|
+
project?: string;
|
|
56
53
|
}>
|
|
57
54
|
| Readonly<{
|
|
58
55
|
source: 'sandbox-executor';
|
|
@@ -64,7 +61,6 @@ export type TaskControlExecutionContext =
|
|
|
64
61
|
generation: string;
|
|
65
62
|
manifestPath: string;
|
|
66
63
|
requestId: string;
|
|
67
|
-
controllerBinding?: TaskControlControllerBinding | null;
|
|
68
64
|
}>;
|
|
69
65
|
|
|
70
66
|
export type TaskControlOrchestrationIntent =
|
|
@@ -112,25 +108,19 @@ function absolute(name: string, value: string): string {
|
|
|
112
108
|
return path.resolve(value);
|
|
113
109
|
}
|
|
114
110
|
|
|
115
|
-
function binding(value: TaskControlControllerBinding | null | undefined): TaskControlControllerBinding | null | undefined {
|
|
116
|
-
if (value === null || value === undefined) return value;
|
|
117
|
-
if (!/^[a-f0-9]{64}$/u.test(value.instanceDigest) || !value.controlGeneration) {
|
|
118
|
-
invalidContext('controller binding is invalid');
|
|
119
|
-
}
|
|
120
|
-
return value;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
111
|
export function createDirectHostExecutionContext(params: Readonly<{
|
|
124
112
|
repoRoot: string;
|
|
125
113
|
runtimeDir?: string;
|
|
126
|
-
|
|
114
|
+
controlBase?: string;
|
|
115
|
+
project?: string;
|
|
127
116
|
}>): TaskControlExecutionContext {
|
|
128
117
|
return {
|
|
129
118
|
source: 'direct-host',
|
|
130
119
|
mode: 'direct-host',
|
|
131
120
|
repoRoot: absolute('repoRoot', params.repoRoot),
|
|
132
|
-
...(params.
|
|
133
|
-
...(params.
|
|
121
|
+
...(params.controlBase === undefined ? {} : { controlBase: absolute('controlBase', params.controlBase) }),
|
|
122
|
+
...(params.project === undefined ? {} : { project: params.project }),
|
|
123
|
+
...(params.runtimeDir === undefined ? {} : { runtimeDir: absolute('runtimeDir', params.runtimeDir) })
|
|
134
124
|
};
|
|
135
125
|
}
|
|
136
126
|
|
|
@@ -142,7 +132,6 @@ export function createSandboxExecutorExecutionContext(params: Readonly<{
|
|
|
142
132
|
generation: string;
|
|
143
133
|
manifestPath: string;
|
|
144
134
|
requestId: string;
|
|
145
|
-
controllerBinding?: TaskControlControllerBinding | null;
|
|
146
135
|
}>): TaskControlExecutionContext {
|
|
147
136
|
if (!/^TASK-\d{8}-\d{6}$/u.test(params.taskId) || !params.generation || !params.requestId) {
|
|
148
137
|
invalidContext('sandbox binding is incomplete');
|
|
@@ -160,8 +149,7 @@ export function createSandboxExecutorExecutionContext(params: Readonly<{
|
|
|
160
149
|
taskId: params.taskId,
|
|
161
150
|
generation: params.generation,
|
|
162
151
|
manifestPath: absolute('manifestPath', params.manifestPath),
|
|
163
|
-
requestId: params.requestId
|
|
164
|
-
...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
|
|
152
|
+
requestId: params.requestId
|
|
165
153
|
};
|
|
166
154
|
}
|
|
167
155
|
|
|
@@ -170,7 +158,8 @@ export function assertTaskControlExecutionContext(context: TaskControlExecutionC
|
|
|
170
158
|
if (context.mode !== 'direct-host') invalidContext('direct-host mode is inconsistent');
|
|
171
159
|
absolute('repoRoot', context.repoRoot);
|
|
172
160
|
if (context.runtimeDir !== undefined) absolute('runtimeDir', context.runtimeDir);
|
|
173
|
-
|
|
161
|
+
if (Boolean(context.controlBase) !== Boolean(context.project)) invalidContext('control configuration is incomplete');
|
|
162
|
+
if (context.controlBase !== undefined) absolute('controlBase', context.controlBase);
|
|
174
163
|
return;
|
|
175
164
|
}
|
|
176
165
|
if (context.mode !== 'task-bound-sandbox' || !/^TASK-\d{8}-\d{6}$/u.test(context.taskId)
|
|
@@ -184,7 +173,6 @@ export function assertTaskControlExecutionContext(context: TaskControlExecutionC
|
|
|
184
173
|
if (path.resolve(context.runtimeDir) !== path.join(path.dirname(path.resolve(context.manifestPath)), 'runtime')) {
|
|
185
174
|
invalidContext('runtimeDir is not bound to the manifest control root');
|
|
186
175
|
}
|
|
187
|
-
binding(context.controllerBinding);
|
|
188
176
|
}
|
|
189
177
|
|
|
190
178
|
function boundTaskRef(context: TaskControlExecutionContext, taskRef: string): string {
|
|
@@ -225,6 +213,8 @@ async function applyLifecycleWithAuthority(
|
|
|
225
213
|
context: TaskControlExecutionContext,
|
|
226
214
|
request: TaskLifecycleControlRequest
|
|
227
215
|
): Promise<TaskLifecycleResult | LifecycleRecoveryResult> {
|
|
216
|
+
const taskId = lifecycleTaskId(request, context.repoRoot);
|
|
217
|
+
const controlRoot = taskId ? controlRootForTask(context, taskId) : undefined;
|
|
228
218
|
const execute = async (underTaskLock = false): Promise<TaskLifecycleResult | LifecycleRecoveryResult> => {
|
|
229
219
|
if (request.intent === 'recover-started') {
|
|
230
220
|
const adapter = isAgentClientId(request.agent)
|
|
@@ -240,10 +230,17 @@ async function applyLifecycleWithAuthority(
|
|
|
240
230
|
return adapter.recoverStarted(request as LifecycleRecoveryRequest, { repoRoot: context.repoRoot });
|
|
241
231
|
}
|
|
242
232
|
return underTaskLock
|
|
243
|
-
? applyTaskLifecycleWithinTaskLock(request, {
|
|
244
|
-
|
|
233
|
+
? applyTaskLifecycleWithinTaskLock(request, {
|
|
234
|
+
repoRoot: context.repoRoot,
|
|
235
|
+
...(controlRoot ? { controlRoot } : {}),
|
|
236
|
+
...(context.source === 'direct-host' && context.project ? { controlProject: context.project } : {})
|
|
237
|
+
}, taskId!)
|
|
238
|
+
: applyTaskLifecycle(request, {
|
|
239
|
+
repoRoot: context.repoRoot,
|
|
240
|
+
...(controlRoot ? { controlRoot } : {}),
|
|
241
|
+
...(context.source === 'direct-host' && context.project ? { controlProject: context.project } : {})
|
|
242
|
+
});
|
|
245
243
|
};
|
|
246
|
-
const taskId = lifecycleTaskId(request, context.repoRoot);
|
|
247
244
|
if (!taskId) return await execute();
|
|
248
245
|
try {
|
|
249
246
|
return await withTaskExecutionLock(
|
|
@@ -266,6 +263,17 @@ async function applyLifecycleWithAuthority(
|
|
|
266
263
|
}
|
|
267
264
|
}
|
|
268
265
|
|
|
266
|
+
function controlRootForTask(context: TaskControlExecutionContext, taskId: string): string | undefined {
|
|
267
|
+
if (context.source === 'sandbox-executor') return path.dirname(context.manifestPath);
|
|
268
|
+
if (!context.controlBase || !context.project) return undefined;
|
|
269
|
+
return sandboxControlPaths({
|
|
270
|
+
base: context.controlBase,
|
|
271
|
+
project: context.project,
|
|
272
|
+
container: '',
|
|
273
|
+
identity: { mode: 'task-bound', taskId }
|
|
274
|
+
}).root;
|
|
275
|
+
}
|
|
276
|
+
|
|
269
277
|
function orchestration(
|
|
270
278
|
context: TaskControlExecutionContext,
|
|
271
279
|
operation: Extract<TaskControlOperation, { family: 'task-orchestration' }>
|
|
@@ -367,6 +375,10 @@ export function dispatchTaskControlOperation(
|
|
|
367
375
|
});
|
|
368
376
|
}
|
|
369
377
|
if (operation.family === 'task-finalization') {
|
|
378
|
+
const resolved = resolveTaskRef(boundTaskRef(context, operation.request.taskRef), { repoRoot: context.repoRoot });
|
|
379
|
+
const controlRoot = context.source === 'sandbox-executor'
|
|
380
|
+
? path.dirname(context.manifestPath)
|
|
381
|
+
: resolved.ok ? controlRootForTask(context, resolved.taskId) : undefined;
|
|
370
382
|
return applyTaskFinalization({
|
|
371
383
|
...operation.request,
|
|
372
384
|
taskRef: boundTaskRef(context, operation.request.taskRef)
|
|
@@ -376,6 +388,8 @@ export function dispatchTaskControlOperation(
|
|
|
376
388
|
controlBinding: { generation: context.generation, requestId: context.requestId },
|
|
377
389
|
manifestPath: context.manifestPath
|
|
378
390
|
} : {}),
|
|
391
|
+
...(controlRoot ? { controlRoot } : {}),
|
|
392
|
+
...(context.source === 'direct-host' && context.project ? { controlProject: context.project } : {}),
|
|
379
393
|
preflight: (request, options) => verifyTaskEvent(
|
|
380
394
|
{ ...request, event: 'complete-task.hard-preflight' }, options
|
|
381
395
|
)
|
|
@@ -13,7 +13,7 @@ export function parseControlOutput(output: string | null): Record<string, unknow
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export type ControlRecoveryOperation = Readonly<{
|
|
16
|
-
family: 'task-lifecycle' | 'task-create'
|
|
16
|
+
family: 'task-lifecycle' | 'task-create';
|
|
17
17
|
intent: string;
|
|
18
18
|
class: string;
|
|
19
19
|
}>;
|
|
@@ -54,8 +54,7 @@ export type ControlRecoveryDecision = Readonly<{
|
|
|
54
54
|
const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'] as const;
|
|
55
55
|
export const SANDBOX_CONTROL_RECOVERY_OPERATIONS: readonly ControlRecoveryOperation[] = Object.freeze([
|
|
56
56
|
...LIFECYCLE_INTENTS.map((intent) => ({ family: 'task-lifecycle' as const, intent, class: 'lifecycle-mutation' })),
|
|
57
|
-
{ family: 'task-create', intent: 'create', class: 'task-create' }
|
|
58
|
-
...(['controller.close', 'controller.verify'] as const).map((intent) => ({ family: 'agent-client' as const, intent, class: 'agent-client-operation' }))
|
|
57
|
+
{ family: 'task-create', intent: 'create', class: 'task-create' }
|
|
59
58
|
]);
|
|
60
59
|
|
|
61
60
|
export function digestControlRecoveryIntent(family: string, intent: string): string {
|
package/lib/task/finalization.ts
CHANGED
|
@@ -70,6 +70,8 @@ type TaskFinalizationRequest = Readonly<{
|
|
|
70
70
|
type TaskFinalizationOptions = Readonly<{
|
|
71
71
|
repoRoot: string;
|
|
72
72
|
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
73
|
+
controlRoot?: string;
|
|
74
|
+
controlProject?: string;
|
|
73
75
|
manifestPath?: string;
|
|
74
76
|
metadataProvider?: TaskLifecycleOptions['metadataProvider'];
|
|
75
77
|
lifecycle?: typeof applyTaskLifecycle;
|
|
@@ -1068,7 +1070,13 @@ async function prepareUnderLock(
|
|
|
1068
1070
|
} else {
|
|
1069
1071
|
const result = lifecycle(
|
|
1070
1072
|
{ taskRef: taskId, intent: 'complete', agent: request.agent },
|
|
1071
|
-
{
|
|
1073
|
+
{
|
|
1074
|
+
repoRoot,
|
|
1075
|
+
prepareOnly: true,
|
|
1076
|
+
...(options.controlRoot ? { controlRoot: options.controlRoot } : {}),
|
|
1077
|
+
...(options.controlProject ? { controlProject: options.controlProject } : {}),
|
|
1078
|
+
...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {})
|
|
1079
|
+
}
|
|
1072
1080
|
);
|
|
1073
1081
|
lifecycleResult = lifecycleStep(result);
|
|
1074
1082
|
if (result.status !== 'applied' && result.status !== 'no-op') {
|
|
@@ -1207,7 +1215,12 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1207
1215
|
});
|
|
1208
1216
|
receipt = updateReceipt(repoRoot, receipt, { verification: 'pending' });
|
|
1209
1217
|
const lifecycleRequest = { taskRef: resolved.taskId, intent: 'complete' as const, agent: request.agent };
|
|
1210
|
-
const lifecycleOptions = {
|
|
1218
|
+
const lifecycleOptions = {
|
|
1219
|
+
repoRoot,
|
|
1220
|
+
...(options.controlRoot ? { controlRoot: options.controlRoot } : {}),
|
|
1221
|
+
...(options.controlProject ? { controlProject: options.controlProject } : {}),
|
|
1222
|
+
...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {})
|
|
1223
|
+
};
|
|
1211
1224
|
const result = options.lifecycle
|
|
1212
1225
|
? options.lifecycle(lifecycleRequest, lifecycleOptions)
|
|
1213
1226
|
: applyTaskLifecycleWithinTaskLock(lifecycleRequest, lifecycleOptions, resolved.taskId);
|