@fitlab-ai/agent-infra 0.9.3 → 0.9.4
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 +2 -0
- package/README.zh-CN.md +2 -0
- package/bin/cli.ts +12 -0
- package/dist/bin/cli.js +14 -0
- package/dist/bin/internal-cli.js +134 -123
- package/dist/lib/agent-clients/adapter.js +92 -2
- package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
- package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
- package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
- package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
- package/dist/lib/agent-clients/adapters/opencode.js +15 -3
- package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/dist/lib/agent-clients/registry.js +9 -1
- package/dist/lib/internal/sandbox-control.js +24 -0
- package/dist/lib/internal/task-activity.js +45 -13
- package/dist/lib/internal/task-ledger.js +12 -3
- package/dist/lib/internal/task-lifecycle.js +19 -1
- package/dist/lib/internal/task-orchestration.js +122 -18
- package/dist/lib/platform/github-client.js +19 -2
- package/dist/lib/platform/pr-checks.js +1 -1
- package/dist/lib/platform/pull-requests.js +47 -1
- package/dist/lib/platform/releases.js +13 -5
- package/dist/lib/process-data/index.js +239 -0
- package/dist/lib/process-data/normalize.js +198 -0
- package/dist/lib/process-data/privacy.js +33 -0
- package/dist/lib/process-data/reconcile.js +95 -0
- package/dist/lib/process-data/repair.js +58 -0
- package/dist/lib/process-data/sources.js +345 -0
- package/dist/lib/process-data/store.js +282 -0
- package/dist/lib/process-data/types.js +2 -0
- package/dist/lib/run/index.js +4 -3
- package/dist/lib/run/tui.js +7 -8
- package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
- package/dist/lib/sandbox/capture.js +7 -2
- package/dist/lib/sandbox/commands/create.js +109 -228
- package/dist/lib/sandbox/commands/enter.js +15 -47
- package/dist/lib/sandbox/commands/list-running.js +16 -6
- package/dist/lib/sandbox/commands/ls.js +10 -6
- package/dist/lib/sandbox/commands/rebuild.js +4 -3
- package/dist/lib/sandbox/commands/refresh.js +4 -84
- package/dist/lib/sandbox/commands/rm.js +24 -3
- package/dist/lib/sandbox/commands/show.js +12 -3
- package/dist/lib/sandbox/commands/start.js +7 -4
- package/dist/lib/sandbox/config.js +2 -0
- package/dist/lib/sandbox/constants.js +6 -0
- package/dist/lib/sandbox/control/client.js +44 -0
- package/dist/lib/sandbox/control/protocol.js +35 -0
- package/dist/lib/sandbox/control/server.js +109 -0
- package/dist/lib/sandbox/dockerfile.js +36 -13
- package/dist/lib/sandbox/mounts.js +17 -4
- package/dist/lib/sandbox/recovery.js +128 -5
- package/dist/lib/sandbox/redaction.js +12 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/dist/lib/sandbox/workspace-identity.js +95 -0
- package/dist/lib/sandbox/workspace-view.js +103 -0
- package/dist/lib/task/activity-intent.js +257 -51
- package/dist/lib/task/commit-finalization.js +267 -0
- package/dist/lib/task/commit-intent.js +165 -0
- package/dist/lib/task/decision-intents.js +31 -17
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/implementation-inputs.js +54 -7
- package/dist/lib/task/ledger-intents.js +35 -1
- package/dist/lib/task/orchestration.js +363 -28
- package/dist/lib/task/resolve-ref.js +41 -1
- package/lib/agent-clients/adapter.ts +126 -2
- package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
- package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
- package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
- package/lib/agent-clients/adapters/claude-code.ts +21 -3
- package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
- package/lib/agent-clients/adapters/opencode.ts +18 -3
- package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/lib/agent-clients/registry.ts +9 -1
- package/lib/internal/sandbox-control.ts +24 -0
- package/lib/internal/task-activity.ts +50 -14
- package/lib/internal/task-ledger.ts +12 -3
- package/lib/internal/task-lifecycle.ts +22 -1
- package/lib/internal/task-orchestration.ts +81 -11
- package/lib/platform/github-client.ts +20 -2
- package/lib/platform/pr-checks.ts +1 -1
- package/lib/platform/pull-requests.ts +49 -1
- package/lib/platform/releases.ts +16 -4
- package/lib/process-data/index.ts +224 -0
- package/lib/process-data/normalize.ts +202 -0
- package/lib/process-data/privacy.ts +41 -0
- package/lib/process-data/reconcile.ts +110 -0
- package/lib/process-data/repair.ts +64 -0
- package/lib/process-data/sources.ts +354 -0
- package/lib/process-data/store.ts +315 -0
- package/lib/process-data/types.ts +135 -0
- package/lib/run/index.ts +5 -3
- package/lib/run/tui.ts +11 -8
- package/lib/sandbox/agent-client-reconciler.ts +44 -2
- package/lib/sandbox/capture.ts +9 -2
- package/lib/sandbox/commands/create.ts +126 -260
- package/lib/sandbox/commands/enter.ts +13 -59
- package/lib/sandbox/commands/list-running.ts +23 -6
- package/lib/sandbox/commands/ls.ts +22 -6
- package/lib/sandbox/commands/rebuild.ts +4 -3
- package/lib/sandbox/commands/refresh.ts +4 -126
- package/lib/sandbox/commands/rm.ts +24 -3
- package/lib/sandbox/commands/show.ts +29 -3
- package/lib/sandbox/commands/start.ts +6 -4
- package/lib/sandbox/config.ts +4 -0
- package/lib/sandbox/constants.ts +8 -0
- package/lib/sandbox/control/client.ts +55 -0
- package/lib/sandbox/control/protocol.ts +75 -0
- package/lib/sandbox/control/server.ts +117 -0
- package/lib/sandbox/dockerfile.ts +66 -13
- package/lib/sandbox/mounts.ts +24 -4
- package/lib/sandbox/recovery.ts +163 -6
- package/lib/sandbox/redaction.ts +14 -0
- package/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/lib/sandbox/tool-types.ts +16 -0
- package/lib/sandbox/workspace-identity.ts +123 -0
- package/lib/sandbox/workspace-view.ts +143 -0
- package/lib/task/activity-intent.ts +350 -61
- package/lib/task/commit-finalization.ts +356 -0
- package/lib/task/commit-intent.ts +230 -0
- package/lib/task/decision-intents.ts +32 -12
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/implementation-inputs.ts +62 -8
- package/lib/task/ledger-intents.ts +43 -3
- package/lib/task/orchestration.ts +501 -33
- package/lib/task/resolve-ref.ts +46 -1
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/human-decision-context.en.md +11 -2
- package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
- package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
- package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +2 -0
- package/templates/.agents/rules/task-management.zh-CN.md +2 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +21 -11
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -11
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +49 -0
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -0
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
- package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/watch-pr.en.md +1 -1
- package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.claude/rules/tool-preferences.en.md +20 -0
- package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
- package/templates/.opencode/commands/watch-pr.en.md +1 -1
- package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
- package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
JsonValue,
|
|
7
|
+
NormalizedRecord,
|
|
8
|
+
QualityFinding,
|
|
9
|
+
RepairAction,
|
|
10
|
+
RestCollectionEvidence,
|
|
11
|
+
SnapshotManifest,
|
|
12
|
+
StoredObjectEvidence
|
|
13
|
+
} from './types.ts';
|
|
14
|
+
|
|
15
|
+
type ObjectWrite = { sha256: string; path: string; created: boolean };
|
|
16
|
+
type ObjectStore = { put(bytes: Buffer): ObjectWrite; pathFor(sha256: string): string };
|
|
17
|
+
|
|
18
|
+
type PublishInput = {
|
|
19
|
+
scope: 'all' | 'local' | 'github';
|
|
20
|
+
repository: string;
|
|
21
|
+
observedFrom: string;
|
|
22
|
+
observedTo: string;
|
|
23
|
+
objects: StoredObjectEvidence[];
|
|
24
|
+
endpoints: RestCollectionEvidence[];
|
|
25
|
+
excerptsEnabled: boolean;
|
|
26
|
+
records: NormalizedRecord[];
|
|
27
|
+
quality: QualityFinding[];
|
|
28
|
+
repairs: RepairAction[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type VerificationResult = {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
snapshotId: string;
|
|
34
|
+
snapshotPath: string | null;
|
|
35
|
+
errors: string[];
|
|
36
|
+
manifest?: SnapshotManifest;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function sha256(bytes: Buffer | string): string {
|
|
40
|
+
return crypto.createHash('sha256').update(bytes).digest('hex');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function compareCodePoints(left: string, right: string): number {
|
|
44
|
+
const a = Array.from(left, (char) => char.codePointAt(0)!);
|
|
45
|
+
const b = Array.from(right, (char) => char.codePointAt(0)!);
|
|
46
|
+
for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
|
|
47
|
+
if (a[index] !== b[index]) return a[index]! - b[index]!;
|
|
48
|
+
}
|
|
49
|
+
return a.length - b.length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function canonicalString(value: unknown, seen: Set<object>): string {
|
|
53
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') {
|
|
54
|
+
return JSON.stringify(value);
|
|
55
|
+
}
|
|
56
|
+
if (typeof value === 'number') {
|
|
57
|
+
if (!Number.isFinite(value)) throw new Error('canonical JSON accepts only finite JSON values');
|
|
58
|
+
return JSON.stringify(value);
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
if (seen.has(value)) throw new Error('canonical JSON accepts only acyclic JSON values');
|
|
62
|
+
seen.add(value);
|
|
63
|
+
const output = `[${value.map((entry) => canonicalString(entry, seen)).join(',')}]`;
|
|
64
|
+
seen.delete(value);
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
if (typeof value === 'object') {
|
|
68
|
+
if (seen.has(value)) throw new Error('canonical JSON accepts only acyclic JSON values');
|
|
69
|
+
seen.add(value);
|
|
70
|
+
const record = value as Record<string, unknown>;
|
|
71
|
+
const fields = Object.keys(record)
|
|
72
|
+
.sort(compareCodePoints)
|
|
73
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalString(record[key], seen)}`);
|
|
74
|
+
seen.delete(value);
|
|
75
|
+
return `{${fields.join(',')}}`;
|
|
76
|
+
}
|
|
77
|
+
throw new Error('canonical JSON accepts only JSON values');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function canonicalJsonBytes(value: JsonValue | unknown): Buffer {
|
|
81
|
+
return Buffer.from(canonicalString(value, new Set()), 'utf8');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ensurePrivateDirectory(directory: string): void {
|
|
85
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
86
|
+
if (process.platform !== 'win32') fs.chmodSync(directory, 0o700);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function writeExclusive(filePath: string, bytes: Buffer): boolean {
|
|
90
|
+
ensurePrivateDirectory(path.dirname(filePath));
|
|
91
|
+
try {
|
|
92
|
+
const fd = fs.openSync(filePath, 'wx', 0o600);
|
|
93
|
+
try {
|
|
94
|
+
fs.writeFileSync(fd, bytes);
|
|
95
|
+
fs.fsyncSync(fd);
|
|
96
|
+
} finally {
|
|
97
|
+
fs.closeSync(fd);
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createObjectStore(root: string): ObjectStore {
|
|
107
|
+
const objectRoot = path.join(root, 'objects', 'sha256');
|
|
108
|
+
ensurePrivateDirectory(objectRoot);
|
|
109
|
+
return {
|
|
110
|
+
pathFor(digest: string) {
|
|
111
|
+
if (!/^[a-f0-9]{64}$/.test(digest)) throw new Error(`invalid SHA-256: ${digest}`);
|
|
112
|
+
return path.join(objectRoot, digest.slice(0, 2), digest);
|
|
113
|
+
},
|
|
114
|
+
put(bytes: Buffer) {
|
|
115
|
+
const digest = sha256(bytes);
|
|
116
|
+
const objectPath = path.join(objectRoot, digest.slice(0, 2), digest);
|
|
117
|
+
const created = writeExclusive(objectPath, bytes);
|
|
118
|
+
const stored = fs.readFileSync(objectPath);
|
|
119
|
+
if (sha256(stored) !== digest) throw new Error(`CAS hash mismatch for ${digest}`);
|
|
120
|
+
return { sha256: digest, path: objectPath, created };
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function dispositionCounts(objects: StoredObjectEvidence[]): SnapshotManifest['dispositionCounts'] {
|
|
126
|
+
const counts: SnapshotManifest['dispositionCounts'] = { included: 0, 'excluded-sensitive': 0, unavailable: 0 };
|
|
127
|
+
for (const object of objects) counts[object.disposition?.state ?? 'included'] += 1;
|
|
128
|
+
return counts;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function jsonLine(value: JsonValue | unknown): string {
|
|
132
|
+
return `${canonicalJsonBytes(value).toString('utf8')}\n`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function writePrivateFile(filePath: string, content: string | Buffer): void {
|
|
136
|
+
fs.writeFileSync(filePath, content, { mode: 0o600, flag: 'wx' });
|
|
137
|
+
if (process.platform !== 'win32') fs.chmodSync(filePath, 0o600);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function manifestDigest(manifest: Omit<SnapshotManifest, 'manifestSha256'>): string {
|
|
141
|
+
return sha256(canonicalJsonBytes(manifest));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function publishSnapshot(root: string, input: PublishInput): { snapshotId: string; path: string; created: boolean } {
|
|
145
|
+
ensurePrivateDirectory(root);
|
|
146
|
+
ensurePrivateDirectory(path.join(root, 'repairs'));
|
|
147
|
+
const date = new Date(input.observedTo);
|
|
148
|
+
if (!Number.isFinite(date.getTime())) throw new Error('observedTo must be an ISO timestamp');
|
|
149
|
+
|
|
150
|
+
const identity = {
|
|
151
|
+
scope: input.scope,
|
|
152
|
+
repository: input.repository,
|
|
153
|
+
observedFrom: input.observedFrom,
|
|
154
|
+
observedTo: input.observedTo,
|
|
155
|
+
objects: input.objects,
|
|
156
|
+
endpoints: input.endpoints,
|
|
157
|
+
excerptsEnabled: input.excerptsEnabled,
|
|
158
|
+
dispositionCounts: dispositionCounts(input.objects),
|
|
159
|
+
recordCount: input.records.length,
|
|
160
|
+
findingCount: input.quality.length,
|
|
161
|
+
repairCount: input.repairs.length
|
|
162
|
+
};
|
|
163
|
+
const identityDigest = sha256(canonicalJsonBytes(identity));
|
|
164
|
+
const stamp = input.observedTo.replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
165
|
+
const snapshotId = `${stamp}-${identityDigest.slice(0, 12)}`;
|
|
166
|
+
const partialManifest: Omit<SnapshotManifest, 'manifestSha256'> = {
|
|
167
|
+
schema: 'raw-manifest/v1',
|
|
168
|
+
snapshotId,
|
|
169
|
+
...identity,
|
|
170
|
+
privacyPolicyVersion: 'process-data-privacy/v1'
|
|
171
|
+
};
|
|
172
|
+
const manifest: SnapshotManifest = { ...partialManifest, manifestSha256: manifestDigest(partialManifest) };
|
|
173
|
+
|
|
174
|
+
const stagingRoot = path.join(root, '.staging');
|
|
175
|
+
ensurePrivateDirectory(stagingRoot);
|
|
176
|
+
const staging = fs.mkdtempSync(path.join(stagingRoot, `${snapshotId}-`));
|
|
177
|
+
if (process.platform !== 'win32') fs.chmodSync(staging, 0o700);
|
|
178
|
+
try {
|
|
179
|
+
writePrivateFile(path.join(staging, 'manifest.json'), jsonLine(manifest));
|
|
180
|
+
writePrivateFile(
|
|
181
|
+
path.join(staging, 'normalized.jsonl'),
|
|
182
|
+
input.records.map((record) => jsonLine({ schema: 'normalized/v1', ...record })).join('')
|
|
183
|
+
);
|
|
184
|
+
writePrivateFile(path.join(staging, 'quality.json'), jsonLine({ schema: 'quality/v1', findings: input.quality }));
|
|
185
|
+
writePrivateFile(
|
|
186
|
+
path.join(staging, 'repair-manifest.jsonl'),
|
|
187
|
+
input.repairs.map((repair) => jsonLine({ schema: 'repair/v1', ...repair })).join('')
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const destination = path.join(
|
|
191
|
+
root,
|
|
192
|
+
'snapshots',
|
|
193
|
+
String(date.getUTCFullYear()),
|
|
194
|
+
String(date.getUTCMonth() + 1).padStart(2, '0'),
|
|
195
|
+
String(date.getUTCDate()).padStart(2, '0'),
|
|
196
|
+
snapshotId
|
|
197
|
+
);
|
|
198
|
+
ensurePrivateDirectory(path.dirname(destination));
|
|
199
|
+
if (fs.existsSync(destination)) {
|
|
200
|
+
const existing = fs.readFileSync(path.join(destination, 'manifest.json'), 'utf8');
|
|
201
|
+
if (existing !== jsonLine(manifest)) throw new Error(`snapshot collision: ${snapshotId}`);
|
|
202
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
203
|
+
return { snapshotId, path: destination, created: false };
|
|
204
|
+
}
|
|
205
|
+
fs.renameSync(staging, destination);
|
|
206
|
+
return { snapshotId, path: destination, created: true };
|
|
207
|
+
} catch (error) {
|
|
208
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function findSnapshot(root: string, snapshotId: string): string | null {
|
|
214
|
+
const snapshotRoot = path.join(root, 'snapshots');
|
|
215
|
+
if (!fs.existsSync(snapshotRoot)) return null;
|
|
216
|
+
const stack = [snapshotRoot];
|
|
217
|
+
while (stack.length > 0) {
|
|
218
|
+
const current = stack.pop()!;
|
|
219
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
220
|
+
if (!entry.isDirectory()) continue;
|
|
221
|
+
const candidate = path.join(current, entry.name);
|
|
222
|
+
if (entry.name === snapshotId && fs.existsSync(path.join(candidate, 'manifest.json'))) return candidate;
|
|
223
|
+
stack.push(candidate);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function verifySnapshot(root: string, snapshotId: string): VerificationResult {
|
|
230
|
+
const snapshotPath = findSnapshot(root, snapshotId);
|
|
231
|
+
if (!snapshotPath) return { ok: false, snapshotId, snapshotPath: null, errors: ['snapshot not found'] };
|
|
232
|
+
const errors: string[] = [];
|
|
233
|
+
const checkMode = (target: string, expected: number, label: string) => {
|
|
234
|
+
if (process.platform === 'win32' || !fs.existsSync(target)) return;
|
|
235
|
+
const actual = fs.statSync(target).mode & 0o777;
|
|
236
|
+
if (actual !== expected) errors.push(`${label} permission mismatch: expected ${expected.toString(8)}, got ${actual.toString(8)}`);
|
|
237
|
+
};
|
|
238
|
+
checkMode(snapshotPath, 0o700, 'snapshot directory');
|
|
239
|
+
for (const file of ['manifest.json', 'normalized.jsonl', 'quality.json', 'repair-manifest.jsonl']) {
|
|
240
|
+
checkMode(path.join(snapshotPath, file), 0o600, file);
|
|
241
|
+
}
|
|
242
|
+
let manifest: SnapshotManifest;
|
|
243
|
+
try {
|
|
244
|
+
manifest = JSON.parse(fs.readFileSync(path.join(snapshotPath, 'manifest.json'), 'utf8')) as SnapshotManifest;
|
|
245
|
+
} catch {
|
|
246
|
+
return { ok: false, snapshotId, snapshotPath, errors: ['manifest is invalid JSON'] };
|
|
247
|
+
}
|
|
248
|
+
if (manifest.schema !== 'raw-manifest/v1') errors.push(`unsupported manifest schema: ${manifest.schema}`);
|
|
249
|
+
if (manifest.snapshotId !== snapshotId) errors.push('snapshot identity mismatch');
|
|
250
|
+
if (typeof manifest.excerptsEnabled !== 'boolean') errors.push('manifest excerpts opt-in is invalid');
|
|
251
|
+
if (!manifest.dispositionCounts || canonicalJsonBytes(manifest.dispositionCounts).toString('utf8') !== canonicalJsonBytes(dispositionCounts(manifest.objects)).toString('utf8')) {
|
|
252
|
+
errors.push('manifest disposition counts mismatch');
|
|
253
|
+
}
|
|
254
|
+
const { manifestSha256, ...partial } = manifest;
|
|
255
|
+
if (manifestDigest(partial) !== manifestSha256) errors.push('manifest hash mismatch');
|
|
256
|
+
const store = createObjectStore(root);
|
|
257
|
+
try {
|
|
258
|
+
const lines = fs.readFileSync(path.join(snapshotPath, 'normalized.jsonl'), 'utf8').split('\n').filter(Boolean);
|
|
259
|
+
const ids = new Set<string>();
|
|
260
|
+
for (const line of lines) {
|
|
261
|
+
const record = JSON.parse(line) as { schema?: string; recordId?: string };
|
|
262
|
+
if (record.schema !== 'normalized/v1' || typeof record.recordId !== 'string') {
|
|
263
|
+
errors.push('normalized record schema is invalid');
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (ids.has(record.recordId)) errors.push(`duplicate normalized record: ${record.recordId}`);
|
|
267
|
+
ids.add(record.recordId);
|
|
268
|
+
}
|
|
269
|
+
if (lines.length !== manifest.recordCount) errors.push('normalized record count mismatch');
|
|
270
|
+
const quality = JSON.parse(fs.readFileSync(path.join(snapshotPath, 'quality.json'), 'utf8')) as { schema?: string; findings?: unknown[] };
|
|
271
|
+
if (quality.schema !== 'quality/v1' || !Array.isArray(quality.findings)) errors.push('quality schema is invalid');
|
|
272
|
+
else if (quality.findings.length !== manifest.findingCount) errors.push('quality finding count mismatch');
|
|
273
|
+
const repairLines = fs.readFileSync(path.join(snapshotPath, 'repair-manifest.jsonl'), 'utf8').split('\n').filter(Boolean);
|
|
274
|
+
for (const line of repairLines) {
|
|
275
|
+
const repair = JSON.parse(line) as { schema?: string; repairId?: string };
|
|
276
|
+
if (repair.schema !== 'repair/v1' || typeof repair.repairId !== 'string') errors.push('repair manifest schema is invalid');
|
|
277
|
+
}
|
|
278
|
+
if (repairLines.length !== manifest.repairCount) errors.push('repair manifest count mismatch');
|
|
279
|
+
} catch {
|
|
280
|
+
errors.push('snapshot derived files are missing or invalid');
|
|
281
|
+
}
|
|
282
|
+
for (const object of manifest.objects) {
|
|
283
|
+
if (object.disposition?.state && object.disposition.state !== 'included') continue;
|
|
284
|
+
try {
|
|
285
|
+
const objectPath = store.pathFor(object.sha256);
|
|
286
|
+
checkMode(path.dirname(objectPath), 0o700, `object directory ${object.sha256.slice(0, 2)}`);
|
|
287
|
+
checkMode(objectPath, 0o600, `object ${object.sourceIdentity}`);
|
|
288
|
+
const bytes = fs.readFileSync(objectPath);
|
|
289
|
+
if (sha256(bytes) !== object.sha256) errors.push(`object hash mismatch: ${object.sourceIdentity}`);
|
|
290
|
+
if (bytes.length !== object.bytes) errors.push(`object byte count mismatch: ${object.sourceIdentity}`);
|
|
291
|
+
} catch {
|
|
292
|
+
errors.push(`object missing: ${object.sourceIdentity}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const page of manifest.endpoints.flatMap((endpoint) => endpoint.pages)) {
|
|
296
|
+
try {
|
|
297
|
+
if (sha256(fs.readFileSync(store.pathFor(page.canonicalSha256))) !== page.canonicalSha256) {
|
|
298
|
+
errors.push(`page hash mismatch: ${page.index}`);
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
errors.push(`page object missing: ${page.canonicalSha256}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return { ok: errors.length === 0, snapshotId, snapshotPath, errors, manifest };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export {
|
|
308
|
+
canonicalJsonBytes,
|
|
309
|
+
createObjectStore,
|
|
310
|
+
findSnapshot,
|
|
311
|
+
publishSnapshot,
|
|
312
|
+
sha256,
|
|
313
|
+
verifySnapshot
|
|
314
|
+
};
|
|
315
|
+
export type { ObjectStore, ObjectWrite, PublishInput, VerificationResult };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
type JsonPrimitive = null | boolean | number | string;
|
|
2
|
+
type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
|
3
|
+
|
|
4
|
+
type Availability<T> =
|
|
5
|
+
| { state: 'known'; value: T }
|
|
6
|
+
| { state: 'unknown' | 'unavailable'; reason: string };
|
|
7
|
+
|
|
8
|
+
type SourceKind = 'local-file' | 'github-rest' | 'operational-report' | 'structured-telemetry';
|
|
9
|
+
|
|
10
|
+
type PrivacyDisposition =
|
|
11
|
+
| { state: 'included' }
|
|
12
|
+
| { state: 'excluded-sensitive'; ruleId: string }
|
|
13
|
+
| { state: 'unavailable'; reason: string };
|
|
14
|
+
|
|
15
|
+
type CapturedObject = {
|
|
16
|
+
sourceKind: SourceKind;
|
|
17
|
+
sourceIdentity: string;
|
|
18
|
+
sha256: string;
|
|
19
|
+
bytes: number;
|
|
20
|
+
content?: string;
|
|
21
|
+
disposition?: PrivacyDisposition;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type StoredObjectEvidence = Omit<CapturedObject, 'content'>;
|
|
25
|
+
|
|
26
|
+
type RestPageEvidence = {
|
|
27
|
+
index: number;
|
|
28
|
+
itemCount: number;
|
|
29
|
+
canonicalSha256: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type RestCollectionEvidence = {
|
|
33
|
+
endpoint: string;
|
|
34
|
+
requestCount: number;
|
|
35
|
+
dataPageCount: number;
|
|
36
|
+
itemCount: number;
|
|
37
|
+
termination: 'short-page';
|
|
38
|
+
pages: RestPageEvidence[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type NormalizedKind =
|
|
42
|
+
| 'task'
|
|
43
|
+
| 'artifact'
|
|
44
|
+
| 'platform-resource'
|
|
45
|
+
| 'lifecycle-event'
|
|
46
|
+
| 'review-finding'
|
|
47
|
+
| 'human-ruling'
|
|
48
|
+
| 'telemetry'
|
|
49
|
+
| 'operational-report'
|
|
50
|
+
| 'missing'
|
|
51
|
+
| 'conflict';
|
|
52
|
+
|
|
53
|
+
type NormalizedRecord = {
|
|
54
|
+
recordId: string;
|
|
55
|
+
kind: NormalizedKind;
|
|
56
|
+
sourceIdentity: string;
|
|
57
|
+
sourceSha256: string;
|
|
58
|
+
binding?: string;
|
|
59
|
+
observedAt?: Availability<string>;
|
|
60
|
+
data?: JsonValue;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type QualityCategory =
|
|
64
|
+
| 'missing-local'
|
|
65
|
+
| 'missing-remote'
|
|
66
|
+
| 'duplicate-identity'
|
|
67
|
+
| 'binding-conflict'
|
|
68
|
+
| 'content-mismatch'
|
|
69
|
+
| 'schema-difference'
|
|
70
|
+
| 'mutable-remote'
|
|
71
|
+
| 'unrecoverable'
|
|
72
|
+
| 'privacy-excluded';
|
|
73
|
+
|
|
74
|
+
type QualityFinding = {
|
|
75
|
+
findingId: string;
|
|
76
|
+
category: QualityCategory;
|
|
77
|
+
severity: 'info' | 'warning' | 'error';
|
|
78
|
+
identities: string[];
|
|
79
|
+
repairable: boolean;
|
|
80
|
+
reason: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type RepairAction = {
|
|
84
|
+
repairId: string;
|
|
85
|
+
operation: 'add' | 'supersede' | 'link';
|
|
86
|
+
sourceRecordId: string;
|
|
87
|
+
targetRecordId: string;
|
|
88
|
+
preconditionSha256: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type SnapshotManifest = {
|
|
92
|
+
schema: 'raw-manifest/v1';
|
|
93
|
+
snapshotId: string;
|
|
94
|
+
scope: 'all' | 'local' | 'github';
|
|
95
|
+
repository: string;
|
|
96
|
+
observedFrom: string;
|
|
97
|
+
observedTo: string;
|
|
98
|
+
objects: StoredObjectEvidence[];
|
|
99
|
+
endpoints: RestCollectionEvidence[];
|
|
100
|
+
recordCount: number;
|
|
101
|
+
findingCount: number;
|
|
102
|
+
repairCount: number;
|
|
103
|
+
privacyPolicyVersion: 'process-data-privacy/v1';
|
|
104
|
+
excerptsEnabled: boolean;
|
|
105
|
+
dispositionCounts: Record<'included' | 'excluded-sensitive' | 'unavailable', number>;
|
|
106
|
+
manifestSha256: string;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
type ProcessDataError = {
|
|
110
|
+
code: string;
|
|
111
|
+
message: string;
|
|
112
|
+
retryable?: boolean;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
type ProcessResult<T> = { ok: true; value: T } | { ok: false; error: ProcessDataError };
|
|
116
|
+
|
|
117
|
+
export type {
|
|
118
|
+
Availability,
|
|
119
|
+
CapturedObject,
|
|
120
|
+
JsonPrimitive,
|
|
121
|
+
JsonValue,
|
|
122
|
+
NormalizedKind,
|
|
123
|
+
NormalizedRecord,
|
|
124
|
+
PrivacyDisposition,
|
|
125
|
+
ProcessDataError,
|
|
126
|
+
ProcessResult,
|
|
127
|
+
QualityCategory,
|
|
128
|
+
QualityFinding,
|
|
129
|
+
RepairAction,
|
|
130
|
+
RestCollectionEvidence,
|
|
131
|
+
RestPageEvidence,
|
|
132
|
+
SnapshotManifest,
|
|
133
|
+
SourceKind,
|
|
134
|
+
StoredObjectEvidence
|
|
135
|
+
};
|
package/lib/run/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loadServerConfig } from '../server/config.ts';
|
|
4
|
-
import {
|
|
4
|
+
import { resolveSandboxTarget, type SandboxWorkspaceIdentity } from '../sandbox/workspace-identity.ts';
|
|
5
5
|
import { createRunId, runInSandbox, type SandboxRunMetadata } from '../sandbox/capture.ts';
|
|
6
6
|
import { loadShortIdByTaskId, normalizeShortIdInput } from '../task/short-id.ts';
|
|
7
7
|
import { buildTuiCommand, renderPrompt, selectTui } from './tui.ts';
|
|
@@ -19,6 +19,7 @@ export type ParsedRunArgs = {
|
|
|
19
19
|
export type SandboxRunRequest = {
|
|
20
20
|
taskRef: string;
|
|
21
21
|
branch: string;
|
|
22
|
+
workspace?: SandboxWorkspaceIdentity;
|
|
22
23
|
command: string[];
|
|
23
24
|
runId?: string;
|
|
24
25
|
recreate?: boolean;
|
|
@@ -136,7 +137,6 @@ function resolveActiveTaskIdentity(taskRef: string, repoRoot: string): ActiveTas
|
|
|
136
137
|
|
|
137
138
|
if (TASK_ID_RE.test(taskRef)) {
|
|
138
139
|
taskId = taskRef;
|
|
139
|
-
resolvedTaskRef = loadShortIdByTaskId(repoRoot).get(taskId) ?? taskRef;
|
|
140
140
|
} else {
|
|
141
141
|
const normalized = normalizeShortIdInput(taskRef, { shortIdLength: readShortIdLength(repoRoot) });
|
|
142
142
|
if (normalized.kind !== 'shortId') return null;
|
|
@@ -211,7 +211,8 @@ export async function runSkill(args: string[], options: RunSkillOptions = {}): P
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
const repoRoot = options.repoRoot ?? config?.repoRoot ?? process.cwd();
|
|
214
|
-
const
|
|
214
|
+
const target = resolveSandboxTarget(parsed.taskRef, repoRoot);
|
|
215
|
+
const branch = target.branch;
|
|
215
216
|
const identity = resolveActiveTaskIdentity(parsed.taskRef, repoRoot);
|
|
216
217
|
const runId = identity ? createRunId() : undefined;
|
|
217
218
|
const writeStdout = options.writeStdout ?? ((chunk: string) => process.stdout.write(chunk));
|
|
@@ -222,6 +223,7 @@ export async function runSkill(args: string[], options: RunSkillOptions = {}): P
|
|
|
222
223
|
const result = await runSandbox({
|
|
223
224
|
taskRef: parsed.taskRef,
|
|
224
225
|
branch,
|
|
226
|
+
workspace: target.workspace,
|
|
225
227
|
command,
|
|
226
228
|
runId,
|
|
227
229
|
recreate: parsed.recreate
|
package/lib/run/tui.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { renderAgentClientInvocation } from '../agent-clients/invocation.ts';
|
|
2
2
|
import { getAgentClientAdapter } from '../agent-clients/registry.ts';
|
|
3
|
+
import type { AgentClientId } from '../agent-clients/types.ts';
|
|
3
4
|
|
|
4
5
|
export type TuiName = 'claude' | 'codex' | 'antigravity' | 'opencode';
|
|
5
6
|
|
|
6
7
|
const TUI_NAMES = new Set(['claude', 'codex', 'antigravity', 'opencode']);
|
|
8
|
+
const TUI_AGENT_CLIENT_IDS: Readonly<Record<TuiName, AgentClientId>> = Object.freeze({
|
|
9
|
+
claude: 'claude-code',
|
|
10
|
+
codex: 'codex',
|
|
11
|
+
antigravity: 'antigravity-cli',
|
|
12
|
+
opencode: 'opencode'
|
|
13
|
+
});
|
|
7
14
|
|
|
8
15
|
export type CommandConfig = {
|
|
9
16
|
defaultTui?: unknown;
|
|
@@ -29,14 +36,10 @@ export function selectTui(
|
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export function renderPrompt(params: { tui: TuiName; skill: string; args: string[] }): string {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
args: params.args
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
return `/${suffix}`;
|
|
39
|
+
return renderAgentClientInvocation(
|
|
40
|
+
getAgentClientAdapter(TUI_AGENT_CLIENT_IDS[params.tui]).invocation,
|
|
41
|
+
{ skillName: params.skill, args: params.args }
|
|
42
|
+
);
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
export function buildTuiCommand(tui: TuiName, prompt: string): [string, string[]] {
|
|
@@ -51,6 +51,12 @@ type SandboxRuntimeCapabilityProjection = Readonly<{
|
|
|
51
51
|
}>;
|
|
52
52
|
repair?: AgentClientSandboxRecoveryCheck['repair'];
|
|
53
53
|
}>[];
|
|
54
|
+
image: SandboxImageContribution;
|
|
55
|
+
}>;
|
|
56
|
+
|
|
57
|
+
type SandboxImageContribution = Readonly<{
|
|
58
|
+
dockerfileFragments: readonly string[];
|
|
59
|
+
dotfilesExclusions: readonly string[];
|
|
54
60
|
}>;
|
|
55
61
|
|
|
56
62
|
type PlannedAgentClientRecoveryCheck = Readonly<{
|
|
@@ -64,6 +70,7 @@ type SandboxCapabilityPlan = Readonly<{
|
|
|
64
70
|
hooksByPhase: Readonly<Record<SandboxHookPhase, readonly AgentClientSandboxHook[]>>;
|
|
65
71
|
recoveryChecks: readonly PlannedAgentClientRecoveryCheck[];
|
|
66
72
|
aliases: readonly SandboxAlias[];
|
|
73
|
+
image: SandboxImageContribution;
|
|
67
74
|
imageSignature: string;
|
|
68
75
|
runtimeSignature: string;
|
|
69
76
|
runtimeProjection: SandboxRuntimeCapabilityProjection;
|
|
@@ -147,6 +154,36 @@ function assertUniqueTools(tools: readonly SandboxTool[]): void {
|
|
|
147
154
|
}
|
|
148
155
|
}
|
|
149
156
|
|
|
157
|
+
function unique(values: readonly string[]): readonly string[] {
|
|
158
|
+
return Object.freeze([...new Set(values)]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sandboxImageContribution(
|
|
162
|
+
adapters: readonly AgentClientAdapter[],
|
|
163
|
+
tools: readonly SandboxTool[]
|
|
164
|
+
): SandboxImageContribution {
|
|
165
|
+
const homePrefix = '/home/devuser/';
|
|
166
|
+
const derivedExclusions = tools.map((tool) => {
|
|
167
|
+
if (!tool.containerMount.startsWith(homePrefix)) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Agent Client '${tool.id}' container mount must be below ${homePrefix.slice(0, -1)}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return tool.containerMount.slice(homePrefix.length);
|
|
173
|
+
});
|
|
174
|
+
return Object.freeze({
|
|
175
|
+
dockerfileFragments: unique(adapters.flatMap((adapter) =>
|
|
176
|
+
adapter.sandbox.image?.dockerfileFragment
|
|
177
|
+
? [adapter.sandbox.image.dockerfileFragment]
|
|
178
|
+
: []
|
|
179
|
+
)),
|
|
180
|
+
dotfilesExclusions: unique([
|
|
181
|
+
...derivedExclusions,
|
|
182
|
+
...adapters.flatMap((adapter) => adapter.sandbox.image?.dotfilesExclusions ?? [])
|
|
183
|
+
])
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
150
187
|
function hooksByPhase(
|
|
151
188
|
adapters: readonly AgentClientAdapter[]
|
|
152
189
|
): SandboxCapabilityPlan['hooksByPhase'] {
|
|
@@ -207,6 +244,7 @@ function createSandboxCapabilityPlan(
|
|
|
207
244
|
|
|
208
245
|
const tools = Object.freeze([...nonClientTools, ...selectedTools, ...selectedCustomTools]);
|
|
209
246
|
assertUniqueTools(tools);
|
|
247
|
+
const image = sandboxImageContribution(selectedAgentClients, selectedTools);
|
|
210
248
|
const phaseHooks = hooksByPhase(selectedAgentClients);
|
|
211
249
|
const hookProjection = selectedAgentClients.flatMap((adapter) =>
|
|
212
250
|
adapter.sandbox.hooks.map((hook) => ({
|
|
@@ -239,7 +277,8 @@ function createSandboxCapabilityPlan(
|
|
|
239
277
|
tools: Object.freeze(tools.map(runtimeToolProjection)),
|
|
240
278
|
selectedAgentClients: Object.freeze(selectedAgentClients.map((adapter) => adapter.id)),
|
|
241
279
|
hooks: Object.freeze(hookProjection),
|
|
242
|
-
recoveryChecks: recoveryCheckProjection
|
|
280
|
+
recoveryChecks: recoveryCheckProjection,
|
|
281
|
+
image
|
|
243
282
|
});
|
|
244
283
|
const cleanupInventory = Object.freeze([
|
|
245
284
|
agentInfraTool(config.home),
|
|
@@ -256,6 +295,7 @@ function createSandboxCapabilityPlan(
|
|
|
256
295
|
aliases: Object.freeze(
|
|
257
296
|
listAgentClientAdapters().flatMap((adapter) => adapter.sandbox.aliases)
|
|
258
297
|
),
|
|
298
|
+
image,
|
|
259
299
|
imageSignature: hash(tools.map(({ id, install }) => ({ id, install }))),
|
|
260
300
|
runtimeSignature: hash(runtimeProjection),
|
|
261
301
|
runtimeProjection,
|
|
@@ -367,12 +407,14 @@ export {
|
|
|
367
407
|
DEFAULT_SANDBOX_HOOK_TIMEOUT_MS,
|
|
368
408
|
createSandboxCapabilityPlan,
|
|
369
409
|
runBoundedSandboxHookCommand,
|
|
370
|
-
runSandboxHooks
|
|
410
|
+
runSandboxHooks,
|
|
411
|
+
sandboxImageContribution
|
|
371
412
|
};
|
|
372
413
|
export type {
|
|
373
414
|
HookExecutionResult,
|
|
374
415
|
SandboxCapabilityConfig,
|
|
375
416
|
SandboxCapabilityPlan,
|
|
417
|
+
SandboxImageContribution,
|
|
376
418
|
PlannedAgentClientRecoveryCheck,
|
|
377
419
|
SandboxRuntimeCapabilityProjection
|
|
378
420
|
};
|
package/lib/sandbox/capture.ts
CHANGED
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
startSandboxContainer,
|
|
12
12
|
type SandboxRow
|
|
13
13
|
} from './commands/list-running.ts';
|
|
14
|
+
import { resolveSandboxTarget, type SandboxWorkspaceIdentity } from './workspace-identity.ts';
|
|
14
15
|
|
|
15
16
|
export type SandboxCaptureRequest = {
|
|
16
17
|
taskRef: string;
|
|
17
18
|
branch: string;
|
|
19
|
+
workspace?: SandboxWorkspaceIdentity;
|
|
18
20
|
command: string[];
|
|
19
21
|
timeoutMs?: number;
|
|
20
22
|
recreate?: boolean;
|
|
@@ -44,6 +46,7 @@ export type SandboxCaptureOptions = {
|
|
|
44
46
|
startContainer?: (name: string) => void;
|
|
45
47
|
ensureReady?: (params: {
|
|
46
48
|
branch: string;
|
|
49
|
+
workspace?: SandboxWorkspaceIdentity;
|
|
47
50
|
row: SandboxRow;
|
|
48
51
|
allowRecreate: boolean;
|
|
49
52
|
}) => Promise<SandboxReadyResult>;
|
|
@@ -183,6 +186,8 @@ export async function runInSandbox(
|
|
|
183
186
|
): Promise<SandboxCaptureResult> {
|
|
184
187
|
const config = options.engine ? null : loadConfig();
|
|
185
188
|
const engine = options.engine ?? detectEngine(config!);
|
|
189
|
+
const workspace = request.workspace
|
|
190
|
+
?? (config ? resolveSandboxTarget(request.taskRef, config.repoRoot).workspace : undefined);
|
|
186
191
|
const rows =
|
|
187
192
|
options.rows ??
|
|
188
193
|
(() => {
|
|
@@ -200,6 +205,7 @@ export async function runInSandbox(
|
|
|
200
205
|
if (options.ensureReady) {
|
|
201
206
|
container = (await options.ensureReady({
|
|
202
207
|
branch: request.branch,
|
|
208
|
+
workspace,
|
|
203
209
|
row: found,
|
|
204
210
|
allowRecreate: request.recreate ?? false
|
|
205
211
|
})).container;
|
|
@@ -208,11 +214,12 @@ export async function runInSandbox(
|
|
|
208
214
|
config,
|
|
209
215
|
engine,
|
|
210
216
|
branch: request.branch,
|
|
217
|
+
workspace,
|
|
211
218
|
row: found,
|
|
212
219
|
allowRecreate: request.recreate,
|
|
213
|
-
recreate: async (
|
|
220
|
+
recreate: async () => {
|
|
214
221
|
const { create } = await import('./commands/create.ts');
|
|
215
|
-
await create([
|
|
222
|
+
await create([request.taskRef, '--no-refresh']);
|
|
216
223
|
}
|
|
217
224
|
})).container;
|
|
218
225
|
} else if (!found.running) {
|