cadet-agent 0.20.2 → 0.24.0
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 +46 -6
- package/package.json +35 -35
- package/src/cli.mjs +373 -33
- package/src/harness/archive.mjs +242 -0
- package/src/harness/budget.mjs +298 -0
- package/src/harness/context.mjs +229 -0
- package/src/harness/hook.mjs +147 -0
- package/src/harness/index.mjs +60 -0
- package/src/harness/ledger.mjs +309 -0
- package/src/harness/policy.mjs +359 -0
- package/src/harness/redaction.mjs +133 -0
- package/src/harness/routing.mjs +153 -0
- package/src/harness/state.mjs +646 -0
- package/src/harness/util.mjs +149 -0
- package/src/harness/verification.mjs +490 -0
- package/src/install.mjs +92 -154
package/src/install.mjs
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join,
|
|
3
|
-
import { inflateRawSync } from 'node:zlib';
|
|
4
|
-
import { Readable } from 'node:stream';
|
|
1
|
+
import { readFileSync, unlinkSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
5
3
|
import { runUpgrades } from './upgrades.mjs';
|
|
4
|
+
import {
|
|
5
|
+
extractArchive, readArchiveEntry, findEocd,
|
|
6
|
+
DEFAULT_ARCHIVE_LIMITS, ArchiveError,
|
|
7
|
+
} from './harness/archive.mjs';
|
|
6
8
|
|
|
7
9
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
8
10
|
|
|
9
11
|
const DEFAULT_API = 'https://api.github.com/repos/naishtech/cadet-agent/releases/latest';
|
|
10
12
|
const USER_AGENT = 'cadet-agent-cli';
|
|
11
13
|
|
|
14
|
+
/** Network timeouts and bounded download limits. */
|
|
15
|
+
const FETCH_TIMEOUT_MS = 60_000;
|
|
16
|
+
const READ_TIMEOUT_MS = 120_000;
|
|
17
|
+
const MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024;
|
|
18
|
+
|
|
12
19
|
function resolveApiUrl(override) {
|
|
13
20
|
return override || process.env.CADET_AGENT_RELEASE_URL || DEFAULT_API;
|
|
14
21
|
}
|
|
@@ -29,106 +36,35 @@ function buildHeaders(extra = {}) {
|
|
|
29
36
|
return headers;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
function findEocd(buf) {
|
|
42
|
-
// Search backwards from end for EOCD signature (comment is max 65535 bytes)
|
|
43
|
-
const maxStart = Math.max(0, buf.length - 65535 - 22);
|
|
44
|
-
for (let i = buf.length - 22; i >= maxStart; i--) {
|
|
45
|
-
if (read32(buf, i) === SIG_EOCD) return i;
|
|
46
|
-
}
|
|
47
|
-
throw new Error('Not a valid ZIP file: EOCD signature not found');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function* centralDirectoryEntries(buf, cdOffset, cdSize) {
|
|
51
|
-
let off = cdOffset;
|
|
52
|
-
const end = cdOffset + cdSize;
|
|
53
|
-
while (off < end) {
|
|
54
|
-
if (read32(buf, off) !== SIG_CD) break;
|
|
55
|
-
const method = read16(buf, off + 10);
|
|
56
|
-
const compressedSize = read32(buf, off + 20);
|
|
57
|
-
const uncompressedSize = read32(buf, off + 24);
|
|
58
|
-
const filenameLen = read16(buf, off + 28);
|
|
59
|
-
const extraLen = read16(buf, off + 30);
|
|
60
|
-
const commentLen = read16(buf, off + 32);
|
|
61
|
-
const localHeaderOff = read32(buf, off + 42);
|
|
62
|
-
const filename = buf.toString('utf-8', off + 46, off + 46 + filenameLen).replace(/\\/g, '/');
|
|
63
|
-
|
|
64
|
-
// Skip directory entries (trailing / in filename, or uncompressedSize == 0 with no method)
|
|
65
|
-
if (!filename.endsWith('/')) {
|
|
66
|
-
yield { filename, method, compressedSize, uncompressedSize, localHeaderOff };
|
|
39
|
+
/** fetch with an AbortController timeout; never leaves a request hanging. */
|
|
40
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
43
|
+
try {
|
|
44
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.name === 'AbortError') {
|
|
47
|
+
throw new Error(`Request timed out after ${timeoutMs}ms: ${url}`);
|
|
67
48
|
}
|
|
68
|
-
|
|
69
|
-
|
|
49
|
+
throw err;
|
|
50
|
+
} finally {
|
|
51
|
+
clearTimeout(timer);
|
|
70
52
|
}
|
|
71
53
|
}
|
|
72
54
|
|
|
73
|
-
|
|
74
|
-
|
|
55
|
+
// ── ZIP extraction (hardened via src/harness/archive.mjs) ────────────────────
|
|
56
|
+
//
|
|
57
|
+
// Containment and resource limits live in the archive module; these wrappers
|
|
58
|
+
// preserve the historical return shapes while refusing anything unsafe.
|
|
75
59
|
|
|
76
|
-
|
|
77
|
-
const lfhFilenameLen = read16(buf, localHeaderOff + 26);
|
|
78
|
-
const lfhExtraLen = read16(buf, localHeaderOff + 28);
|
|
60
|
+
const ARCHIVE_LIMITS = DEFAULT_ARCHIVE_LIMITS;
|
|
79
61
|
|
|
80
|
-
|
|
81
|
-
const compressed = buf.subarray(dataStart, dataStart + compressedSize);
|
|
82
|
-
|
|
83
|
-
let data;
|
|
84
|
-
if (method === 0) {
|
|
85
|
-
// Stored — no compression
|
|
86
|
-
data = compressed;
|
|
87
|
-
} else if (method === 8) {
|
|
88
|
-
// Deflate
|
|
89
|
-
data = inflateRawSync(compressed);
|
|
90
|
-
} else {
|
|
91
|
-
throw new Error(
|
|
92
|
-
`Unsupported compression method ${method} for ${filename}.\n` +
|
|
93
|
-
`This zip uses a compression format this CLI doesn't support.\n` +
|
|
94
|
-
`Try downloading cadet-agent.zip manually from:\n` +
|
|
95
|
-
` https://github.com/naishtech/cadet-agent/releases/latest`
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const outPath = join(targetDir, filename);
|
|
100
|
-
mkdirSync(dirname(outPath), { recursive: true });
|
|
101
|
-
|
|
102
|
-
return new Promise((resolve, reject) => {
|
|
103
|
-
const rs = Readable.from(data);
|
|
104
|
-
const ws = createWriteStream(outPath);
|
|
105
|
-
rs.pipe(ws);
|
|
106
|
-
ws.on('finish', () => resolve(outPath));
|
|
107
|
-
ws.on('error', (err) => reject(new Error(`Write failed for ${filename}: ${err.message}`)));
|
|
108
|
-
rs.on('error', (err) => reject(new Error(`Read failed for ${filename}: ${err.message}`)));
|
|
109
|
-
});
|
|
110
|
-
}
|
|
62
|
+
export { ArchiveError, findEocd };
|
|
111
63
|
|
|
64
|
+
/** Extract every file entry into targetDir, returning the written paths. */
|
|
112
65
|
export async function extractZip(buf, targetDir) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const cdOff = read32(buf, eocdOff + 16);
|
|
116
|
-
|
|
117
|
-
// ZIP64 detection — 0xFFFFFFFF in 32-bit fields means real values are in ZIP64 extra records
|
|
118
|
-
if (cdOff === 0xFFFFFFFF || cdSize === 0xFFFFFFFF) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
'This zip uses ZIP64 format, which is not supported.\n' +
|
|
121
|
-
'Try downloading cadet-agent.zip manually from:\n' +
|
|
122
|
-
' https://github.com/naishtech/cadet-agent/releases/latest'
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const paths = [];
|
|
127
|
-
for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
|
|
128
|
-
const outPath = await extractFile(buf, entry, targetDir);
|
|
129
|
-
paths.push(outPath);
|
|
130
|
-
}
|
|
131
|
-
return paths;
|
|
66
|
+
const { extracted } = extractArchive(buf, targetDir, { limits: ARCHIVE_LIMITS });
|
|
67
|
+
return extracted;
|
|
132
68
|
}
|
|
133
69
|
|
|
134
70
|
// ── GitHub release download ─────────────────────────────────────────────────
|
|
@@ -137,9 +73,9 @@ async function fetchLatestRelease(apiUrl) {
|
|
|
137
73
|
const url = resolveApiUrl(apiUrl);
|
|
138
74
|
console.log('🔍 Fetching latest Cadet-Agent release...');
|
|
139
75
|
|
|
140
|
-
const res = await
|
|
76
|
+
const res = await fetchWithTimeout(url, {
|
|
141
77
|
headers: buildHeaders({ 'Accept': 'application/vnd.github+json' }),
|
|
142
|
-
});
|
|
78
|
+
}, FETCH_TIMEOUT_MS);
|
|
143
79
|
|
|
144
80
|
if (!res.ok) {
|
|
145
81
|
if (res.status === 403 || res.status === 429) {
|
|
@@ -168,33 +104,55 @@ function findZipAsset(release) {
|
|
|
168
104
|
async function downloadZip(url) {
|
|
169
105
|
console.log('⬇️ Downloading cadet-agent.zip...');
|
|
170
106
|
|
|
171
|
-
const res = await
|
|
107
|
+
const res = await fetchWithTimeout(url, {
|
|
172
108
|
headers: buildHeaders({ 'Accept': 'application/octet-stream' }),
|
|
173
|
-
});
|
|
109
|
+
}, FETCH_TIMEOUT_MS);
|
|
174
110
|
|
|
175
111
|
if (!res.ok) {
|
|
176
112
|
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
177
113
|
}
|
|
178
114
|
|
|
179
115
|
const contentLength = res.headers.get('content-length');
|
|
180
|
-
const
|
|
116
|
+
const declared = contentLength ? parseInt(contentLength, 10) : 0;
|
|
117
|
+
if (declared > MAX_DOWNLOAD_BYTES) {
|
|
118
|
+
throw new ArchiveError(
|
|
119
|
+
`Refusing download: declared size ${declared} bytes exceeds the ${MAX_DOWNLOAD_BYTES}-byte limit`,
|
|
120
|
+
'download-too-large'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
181
123
|
|
|
182
|
-
// Stream to buffer with progress
|
|
124
|
+
// Stream to buffer with progress, enforcing a hard size cap and read timeout.
|
|
183
125
|
const chunks = [];
|
|
184
126
|
let downloaded = 0;
|
|
185
127
|
const reader = res.body.getReader();
|
|
128
|
+
const readTimer = setTimeout(() => { try { reader.cancel('read timeout'); } catch { /* ignore */ } }, READ_TIMEOUT_MS);
|
|
186
129
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
130
|
+
try {
|
|
131
|
+
while (true) {
|
|
132
|
+
const { done, value } = await reader.read();
|
|
133
|
+
if (done) break;
|
|
134
|
+
downloaded += value.length;
|
|
135
|
+
if (downloaded > MAX_DOWNLOAD_BYTES) {
|
|
136
|
+
try { await reader.cancel('size limit'); } catch { /* ignore */ }
|
|
137
|
+
throw new ArchiveError(
|
|
138
|
+
`Refusing download: body exceeded the ${MAX_DOWNLOAD_BYTES}-byte limit`,
|
|
139
|
+
'download-too-large'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
chunks.push(value);
|
|
143
|
+
if (declared > 0) {
|
|
144
|
+
const pct = Math.round((downloaded / declared) * 100);
|
|
145
|
+
process.stdout.write(`\r ${pct}% (${(downloaded / 1024).toFixed(0)} KB / ${(declared / 1024).toFixed(0)} KB)`);
|
|
146
|
+
}
|
|
195
147
|
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err instanceof ArchiveError) throw err;
|
|
150
|
+
if (err.name === 'AbortError') throw new Error(`Download timed out after ${READ_TIMEOUT_MS}ms`);
|
|
151
|
+
throw err;
|
|
152
|
+
} finally {
|
|
153
|
+
clearTimeout(readTimer);
|
|
196
154
|
}
|
|
197
|
-
if (
|
|
155
|
+
if (declared > 0) process.stdout.write('\n');
|
|
198
156
|
|
|
199
157
|
return Buffer.concat(chunks);
|
|
200
158
|
}
|
|
@@ -242,11 +200,11 @@ export async function install(targetDir, opts = {}) {
|
|
|
242
200
|
console.log(' Reviewer: /cadet-agent-reviewer');
|
|
243
201
|
console.log(' Git guard: manual — see .continue\\rules\\cadet-agent.md for instructions');
|
|
244
202
|
console.log(' Claude Code:');
|
|
245
|
-
console.log(' Already active — .claude\\skills\\cadet-agent.md loads as a project skill');
|
|
203
|
+
console.log(' Already active — .claude\\skills\\cadet-agent\\SKILL.md loads as a project skill');
|
|
246
204
|
console.log(' Slash commands: /cadet-requirements, /cadet-architecture, /cadet-spike,');
|
|
247
205
|
console.log(' /cadet-breakdown, /cadet-tdd, /cadet-debug, /cadet-review, /cadet-resume');
|
|
248
206
|
console.log(' Reviewer: /cadet-agent-reviewer');
|
|
249
|
-
console.log(' Git guard: manual — see .claude\\skills\\cadet-agent.md for instructions');
|
|
207
|
+
console.log(' Git guard: manual — see .claude\\skills\\cadet-agent\\SKILL.md for instructions');
|
|
250
208
|
console.log('');
|
|
251
209
|
}
|
|
252
210
|
|
|
@@ -324,32 +282,28 @@ function deleteObsoleteManagedFiles(targetDir, managedPaths, zipFilenames) {
|
|
|
324
282
|
return deleted;
|
|
325
283
|
}
|
|
326
284
|
|
|
327
|
-
export async function extractZipWithManifest(buf, targetDir, { preserved, managed }) {
|
|
328
|
-
const eocdOff = findEocd(buf);
|
|
329
|
-
const cdSize = read32(buf, eocdOff + 12);
|
|
330
|
-
const cdOff = read32(buf, eocdOff + 16);
|
|
331
|
-
|
|
332
|
-
if (cdOff === 0xFFFFFFFF || cdSize === 0xFFFFFFFF) {
|
|
333
|
-
throw new Error(
|
|
334
|
-
'This zip uses ZIP64 format, which is not supported.\n' +
|
|
335
|
-
'Try downloading cadet-agent.zip manually from:\n' +
|
|
336
|
-
' https://github.com/naishtech/cadet-agent/releases/latest'
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
|
|
285
|
+
export async function extractZipWithManifest(buf, targetDir, { preserved, managed, limits = ARCHIVE_LIMITS }) {
|
|
340
286
|
const updated = [];
|
|
341
287
|
const preserved_list = [];
|
|
342
288
|
const added = [];
|
|
343
289
|
const zipFilenames = [];
|
|
344
290
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
291
|
+
const { extracted } = extractArchive(buf, targetDir, {
|
|
292
|
+
limits,
|
|
293
|
+
filter: (entry) => {
|
|
294
|
+
zipFilenames.push(entry.filename);
|
|
295
|
+
if (matchesPreservedPath(entry.filename, preserved)) {
|
|
296
|
+
preserved_list.push(entry.filename);
|
|
297
|
+
return { skip: true };
|
|
298
|
+
}
|
|
299
|
+
return true;
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// `extractArchive` skips directory entries; classify the extracted files.
|
|
304
|
+
for (const outPath of extracted) {
|
|
305
|
+
const rel = outPath.replace(/\\/g, '/');
|
|
306
|
+
if (matchesManagedPath(rel, managed)) {
|
|
353
307
|
updated.push(outPath);
|
|
354
308
|
} else {
|
|
355
309
|
added.push(outPath);
|
|
@@ -366,26 +320,10 @@ export async function extractZipWithManifest(buf, targetDir, { preserved, manage
|
|
|
366
320
|
|
|
367
321
|
export function findManagedPathsInZip(buf) {
|
|
368
322
|
try {
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
|
|
374
|
-
const fn = entry.filename.replace(/^\.\//, '').replace(/\\/g, '/');
|
|
375
|
-
if (fn === '.cadet/agent/core/FrameworkManifest.json') {
|
|
376
|
-
// Extract just this one entry to read managedPaths
|
|
377
|
-
const lfhFilenameLen = read16(buf, entry.localHeaderOff + 26);
|
|
378
|
-
const lfhExtraLen = read16(buf, entry.localHeaderOff + 28);
|
|
379
|
-
const dataStart = entry.localHeaderOff + 30 + lfhFilenameLen + lfhExtraLen;
|
|
380
|
-
const compressed = buf.subarray(dataStart, dataStart + entry.compressedSize);
|
|
381
|
-
let data;
|
|
382
|
-
if (entry.method === 0) data = compressed;
|
|
383
|
-
else if (entry.method === 8) data = inflateRawSync(compressed);
|
|
384
|
-
else break;
|
|
385
|
-
const manifest = JSON.parse(data.toString('utf-8'));
|
|
386
|
-
return manifest.managedPaths || [];
|
|
387
|
-
}
|
|
388
|
-
}
|
|
323
|
+
const data = readArchiveEntry(buf, '.cadet/agent/core/FrameworkManifest.json', ARCHIVE_LIMITS);
|
|
324
|
+
if (!data) return [];
|
|
325
|
+
const manifest = JSON.parse(data.toString('utf-8'));
|
|
326
|
+
return manifest.managedPaths || [];
|
|
389
327
|
} catch {
|
|
390
328
|
// Not a valid zip or manifest not found
|
|
391
329
|
}
|