great-cto 3.70.0 → 3.71.1
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 +9 -1
- package/board/.claude-plugin/plugin.json +93 -1
- package/board/packages/board/lib/routes.mjs +14 -1
- package/board/packages/board/lib/work.mjs +34 -3
- package/board/packages/board/public/assets/admin-shell.css +63 -0
- package/board/packages/board/public/assets/admin-shell.js +39 -5
- package/board/packages/board/public/assets/work-view.js +320 -37
- package/board/packages/board/public/index.html +43 -1
- package/board/scripts/codex-pipeline.mjs +7 -1
- package/board/scripts/lib/claude-exec.mjs +49 -6
- package/board/scripts/lib/codex-exec.mjs +42 -18
- package/board/scripts/lib/codex-host-state.mjs +18 -2
- package/board/scripts/lib/codex-pipeline.mjs +44 -7
- package/board/scripts/lib/host-graph-bindings.mjs +135 -0
- package/board/scripts/lib/host-graph-events.mjs +157 -0
- package/board/scripts/lib/host-graph-journal.mjs +210 -0
- package/board/scripts/lib/host-graph-observer.mjs +61 -0
- package/board/scripts/lib/host-graph-runtime.mjs +38 -0
- package/board/scripts/work-task.mjs +17 -4
- package/dist/adapt.js +10 -21
- package/dist/codex.js +4 -15
- package/dist/companion.js +6 -122
- package/dist/installer.js +4 -72
- package/dist/main.js +51 -118
- package/dist/release-activation.mjs +561 -0
- package/dist/release-archive.mjs +165 -0
- package/dist/release-trust.mjs +514 -0
- package/dist/release-update.mjs +303 -0
- package/dist/self-upgrade.js +10 -39
- package/dist/update-check.js +1 -0
- package/dist/upgrade.js +5 -120
- package/package.json +1 -1
- package/board/packages/board/fixtures/beads-read-owner.mjs +0 -3
- package/board/packages/board/fixtures/fake-bd-controlled.mjs +0 -55
- package/board/packages/board/fixtures/fake-bd-fail.sh +0 -9
- package/board/packages/board/fixtures/fake-bd-slow.sh +0 -12
- package/board/packages/board/fixtures/held-receipt-git.mjs +0 -21
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
/** Verified installation transactions. Publisher admission is not a release approval. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { getAdmissionInfo, getAdmittedArtifact, inspectNpmPayload, inspectPluginPayload } from './release-trust.mjs';
|
|
8
|
+
const MAX_FILES = 10000, MAX_BYTES = 512 * 1024 * 1024;
|
|
9
|
+
const STABLE = /^(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})$/;
|
|
10
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
11
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
12
|
+
const owned = stat => typeof process.getuid !== 'function' || stat.uid === (typeof stat.uid === 'bigint' ? BigInt(process.getuid()) : process.getuid());
|
|
13
|
+
const within = (root, file) => file !== root && file.startsWith(root + path.sep);
|
|
14
|
+
const fail = reason => { throw Object.assign(new Error(reason), { activationReason: reason }); };
|
|
15
|
+
const compare = (a, b) => { const x = a.split('.').map(Number), y = b.split('.').map(Number); for (let i = 0; i < 3; i++)
|
|
16
|
+
if (x[i] !== y[i])
|
|
17
|
+
return Math.sign(x[i] - y[i]); return 0; };
|
|
18
|
+
const access = { getAdmissionInfo, getAdmittedArtifact, inspectNpmPayload, inspectPluginPayload };
|
|
19
|
+
function canonicalDirectory(dir, { create = false, privateMode = false } = {}) {
|
|
20
|
+
if (typeof dir !== 'string' || !path.isAbsolute(dir))
|
|
21
|
+
fail('absolute-directory-required');
|
|
22
|
+
const resolved = path.resolve(dir);
|
|
23
|
+
for (let current = resolved; current !== path.dirname(current); current = path.dirname(current)) {
|
|
24
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink())
|
|
25
|
+
fail('directory-symlink-refused');
|
|
26
|
+
}
|
|
27
|
+
if (create)
|
|
28
|
+
fs.mkdirSync(resolved, { recursive: true, mode: 0o700 });
|
|
29
|
+
const stat = fs.lstatSync(resolved);
|
|
30
|
+
if (!stat.isDirectory() || !owned(stat) || (privateMode ? (stat.mode & 0o777) !== 0o700 : !!(stat.mode & 0o022)))
|
|
31
|
+
fail('unsafe-directory');
|
|
32
|
+
if (fs.realpathSync(resolved) !== resolved)
|
|
33
|
+
fail('noncanonical-directory');
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
function readRegular(file, { max = MAX_BYTES, privateMode = false, allowRootOwner = false } = {}) {
|
|
37
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
38
|
+
try {
|
|
39
|
+
const before = fs.fstatSync(fd, { bigint: true });
|
|
40
|
+
if (!before.isFile() || before.nlink !== 1n || before.size > BigInt(max) || !(owned(before) || (allowRootOwner && before.uid === 0n)) || (before.mode & 18n) || (privateMode && (before.mode & 511n) !== 384n))
|
|
41
|
+
fail('unsafe-file');
|
|
42
|
+
const bytes = fs.readFileSync(fd), after = fs.fstatSync(fd, { bigint: true });
|
|
43
|
+
if (before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs)
|
|
44
|
+
fail('file-changed-during-read');
|
|
45
|
+
return bytes;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
fs.closeSync(fd);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function syncDirectory(dir) { const fd = fs.openSync(dir, fs.constants.O_RDONLY); try {
|
|
52
|
+
fs.fsyncSync(fd);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
fs.closeSync(fd);
|
|
56
|
+
} }
|
|
57
|
+
function syncAncestors(dir, root) {
|
|
58
|
+
for (let current = dir;; current = path.dirname(current)) {
|
|
59
|
+
syncDirectory(current);
|
|
60
|
+
if (current === root)
|
|
61
|
+
return;
|
|
62
|
+
if (!within(root, current))
|
|
63
|
+
fail('transaction-path-outside-state-root');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function writeRecord(file, record) {
|
|
67
|
+
const temp = file + '.' + randomUUID() + '.tmp';
|
|
68
|
+
const fd = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
|
|
69
|
+
try {
|
|
70
|
+
fs.writeFileSync(fd, JSON.stringify(record) + '\n');
|
|
71
|
+
fs.fsyncSync(fd);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
fs.closeSync(fd);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
fs.renameSync(temp, file);
|
|
78
|
+
syncDirectory(path.dirname(file));
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
fs.rmSync(temp, { force: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function readRecord(file) { return JSON.parse(readRegular(file, { max: 1024 * 1024, privateMode: true }).toString('utf8')); }
|
|
85
|
+
function packageSnapshot(root) {
|
|
86
|
+
canonicalDirectory(root);
|
|
87
|
+
const files = [], directories = [], names = new Set();
|
|
88
|
+
let total = 0, entries = 0;
|
|
89
|
+
function walk(dir) {
|
|
90
|
+
const dirStat = fs.lstatSync(dir);
|
|
91
|
+
if (!owned(dirStat) || dirStat.mode & 0o6022)
|
|
92
|
+
fail('unsafe-package-directory');
|
|
93
|
+
directories.push({ path: path.relative(root, dir), mode: dirStat.mode & 0o777 });
|
|
94
|
+
for (const entry of fs.readdirSync(dir).sort()) {
|
|
95
|
+
if (++entries > MAX_FILES * 2 || path.relative(root, dir).split(path.sep).length > 64)
|
|
96
|
+
fail('package-backup-limit');
|
|
97
|
+
if (entry.includes('\\') || /[\0-\x1f]/.test(entry))
|
|
98
|
+
fail('invalid-package-path');
|
|
99
|
+
const file = path.join(dir, entry), stat = fs.lstatSync(file);
|
|
100
|
+
if (!owned(stat) || stat.isSymbolicLink() || stat.mode & 0o6022)
|
|
101
|
+
fail('unsafe-package-entry');
|
|
102
|
+
if (stat.isDirectory()) {
|
|
103
|
+
walk(file);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!stat.isFile() || stat.nlink !== 1)
|
|
107
|
+
fail('unsafe-package-entry');
|
|
108
|
+
const rel = path.relative(root, file), name = rel.normalize('NFC').toLowerCase();
|
|
109
|
+
if (names.has(name))
|
|
110
|
+
fail('colliding-package-path');
|
|
111
|
+
names.add(name);
|
|
112
|
+
const bytes = readRegular(file);
|
|
113
|
+
total += bytes.length;
|
|
114
|
+
if (files.length >= MAX_FILES || total > MAX_BYTES)
|
|
115
|
+
fail('package-backup-limit');
|
|
116
|
+
files.push({ path: rel, size: bytes.length, sha256: hash(bytes), mode: stat.mode & 0o777, bytes });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
walk(root);
|
|
120
|
+
files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
121
|
+
const inventory = files.map(({ bytes, ...file }) => file);
|
|
122
|
+
directories.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
123
|
+
return { files, directories, digest: hash(JSON.stringify({ files: inventory, directories })), filesDigest: hash(JSON.stringify(inventory)) };
|
|
124
|
+
}
|
|
125
|
+
function binSelection(bin, packageRoot) {
|
|
126
|
+
const stat = fs.lstatSync(bin);
|
|
127
|
+
if (!stat.isSymbolicLink() || !owned(stat))
|
|
128
|
+
fail('unsupported-bin-selection');
|
|
129
|
+
const link = fs.readlinkSync(bin);
|
|
130
|
+
if (path.resolve(path.dirname(bin), link) !== path.join(packageRoot, 'index.mjs'))
|
|
131
|
+
fail('foreign-bin-selection');
|
|
132
|
+
return { kind: 'symlink', link };
|
|
133
|
+
}
|
|
134
|
+
/** Resolve only the normal POSIX npm layout, never npm's configured fallback prefix. */
|
|
135
|
+
export function resolveNpmInstallation(binaryPath) {
|
|
136
|
+
if (typeof binaryPath !== 'string' || !path.isAbsolute(binaryPath))
|
|
137
|
+
fail('unsupported-installation');
|
|
138
|
+
if (/(?:^|\/)(?:\.pnpm|pnpm|\.volta|volta|_npx)(?:\/|$)/.test(binaryPath))
|
|
139
|
+
fail('unsupported-package-manager');
|
|
140
|
+
const realBinary = fs.realpathSync(binaryPath), suffix = '/lib/node_modules/great-cto/index.mjs';
|
|
141
|
+
if (!realBinary.endsWith(suffix))
|
|
142
|
+
fail('unsupported-installation');
|
|
143
|
+
const prefix = realBinary.slice(0, -suffix.length);
|
|
144
|
+
if (!prefix || prefix === '/')
|
|
145
|
+
fail('unsupported-installation');
|
|
146
|
+
canonicalDirectory(prefix);
|
|
147
|
+
const packageRoot = path.join(prefix, 'lib/node_modules/great-cto'), binPath = path.join(prefix, 'bin/great-cto');
|
|
148
|
+
canonicalDirectory(packageRoot);
|
|
149
|
+
canonicalDirectory(path.dirname(binPath));
|
|
150
|
+
const selection = binSelection(binPath, packageRoot);
|
|
151
|
+
if (fs.realpathSync(binPath) !== realBinary)
|
|
152
|
+
fail('foreign-bin-selection');
|
|
153
|
+
const manifest = JSON.parse(readRegular(path.join(packageRoot, 'package.json'), { max: 1024 * 1024 }).toString('utf8'));
|
|
154
|
+
if (manifest.name !== 'great-cto' || !STABLE.test(manifest.version))
|
|
155
|
+
fail('invalid-installed-package');
|
|
156
|
+
// npm removes the previous manifest's selections before installing a replacement.
|
|
157
|
+
if (!manifest.bin || Object.keys(manifest.bin).length !== 1 || manifest.bin['great-cto'] !== 'index.mjs' || manifest.man)
|
|
158
|
+
fail('unsupported-installed-selections');
|
|
159
|
+
return { prefix, packageRoot, binPath, binaryPath: realBinary, version: manifest.version, selection };
|
|
160
|
+
}
|
|
161
|
+
function fixtureMode(testing, prefix, stateRoot, artifactPath = null) {
|
|
162
|
+
if (!testing)
|
|
163
|
+
return false;
|
|
164
|
+
if (!testing.access || typeof testing.run !== 'function')
|
|
165
|
+
fail('synthetic-runner-required');
|
|
166
|
+
const root = canonicalDirectory(testing.fixtureRoot, { privateMode: true });
|
|
167
|
+
const temp = fs.realpathSync(os.tmpdir());
|
|
168
|
+
if (!within(temp, root) || !within(root, prefix) || !within(root, path.resolve(stateRoot)) || (artifactPath && !within(root, artifactPath)))
|
|
169
|
+
fail('synthetic-fixture-outside-temp');
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
function outcome(state, context = {}, reason = null) {
|
|
173
|
+
const synthetic = !!context.synthetic;
|
|
174
|
+
return { state: synthetic && ['SMOKE_PASSED', 'ALREADY_ACTIVE', 'ROLLED_BACK'].includes(state) ? 'SIMULATED_' + state : state,
|
|
175
|
+
authority: synthetic || context.recovery || !['SMOKE_PASSED', 'ALREADY_ACTIVE'].includes(state) ? 'NONE' : 'INSTALLATION_ACTIVATION',
|
|
176
|
+
evidence: synthetic ? 'SYNTHETIC_ONLY' : 'LOCAL_TRANSACTION', releaseEligibility: 'NOT_CHECKED',
|
|
177
|
+
...(context.version ? { version: context.version } : {}), ...(context.installationId ? { installationId: context.installationId } : {}),
|
|
178
|
+
...(context.transactionId ? { transactionId: context.transactionId } : {}), ...(reason ? { reason } : {}) };
|
|
179
|
+
}
|
|
180
|
+
function safeEnvironment(tx, npmPath) {
|
|
181
|
+
const home = path.join(tx, 'home'), temp = path.join(tx, 'tmp');
|
|
182
|
+
fs.mkdirSync(home, { mode: 0o700 });
|
|
183
|
+
fs.mkdirSync(temp, { mode: 0o700 });
|
|
184
|
+
const config = path.join(tx, 'empty.npmrc');
|
|
185
|
+
fs.writeFileSync(config, '', { flag: 'wx', mode: 0o600 });
|
|
186
|
+
return { PATH: [path.dirname(process.execPath), path.dirname(npmPath), '/usr/bin', '/bin', '/usr/sbin', '/sbin'].join(path.delimiter),
|
|
187
|
+
HOME: home, USERPROFILE: home, TMPDIR: temp, NPM_CONFIG_USERCONFIG: config, NPM_CONFIG_GLOBALCONFIG: config,
|
|
188
|
+
NPM_CONFIG_CACHE: path.join(tx, 'npm-cache'), NPM_CONFIG_IGNORE_SCRIPTS: 'true', NPM_CONFIG_OFFLINE: 'true',
|
|
189
|
+
NPM_CONFIG_AUDIT: 'false', NPM_CONFIG_FUND: 'false', NPM_CONFIG_PACKAGE_LOCK: 'false',
|
|
190
|
+
DO_NOT_TRACK: '1', GREAT_CTO_TELEMETRY: 'off', GREAT_CTO_NO_BOARD: '1', GREAT_CTO_NO_UPDATE_CHECK: '1', CI: '1' };
|
|
191
|
+
}
|
|
192
|
+
function targetState(installation) {
|
|
193
|
+
const snapshot = packageSnapshot(installation.packageRoot);
|
|
194
|
+
return { packageDigest: snapshot.digest, filesDigest: snapshot.filesDigest, selection: binSelection(installation.binPath, installation.packageRoot) };
|
|
195
|
+
}
|
|
196
|
+
function stateMatches(actual, expected) { return actual.packageDigest === expected.packageDigest && JSON.stringify(actual.selection) === JSON.stringify(expected.selection); }
|
|
197
|
+
function expectedCandidate(files) {
|
|
198
|
+
const inventory = files.map(file => ({ path: file.path, size: file.bytes.length, sha256: hash(file.bytes), mode: file.mode })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
199
|
+
return hash(JSON.stringify(inventory));
|
|
200
|
+
}
|
|
201
|
+
function privateInstallationState(stateRoot, packageRoot, { recovering = false } = {}) {
|
|
202
|
+
const root = canonicalDirectory(stateRoot, { create: true, privateMode: true });
|
|
203
|
+
const installationId = hash(packageRoot), dir = path.join(root, installationId);
|
|
204
|
+
canonicalDirectory(dir, { create: true, privateMode: true });
|
|
205
|
+
const lock = path.join(dir, 'lock');
|
|
206
|
+
if (recovering && fs.existsSync(lock)) {
|
|
207
|
+
const guard = path.join(dir, 'recovery-lock');
|
|
208
|
+
try {
|
|
209
|
+
fs.mkdirSync(guard, { mode: 0o700 });
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
if (e.code === 'EEXIST')
|
|
213
|
+
fail('recovery-busy-lock');
|
|
214
|
+
throw e;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
canonicalDirectory(lock, { privateMode: true });
|
|
218
|
+
const ownerFile = path.join(lock, 'owner.json'), owner = readRecord(ownerFile);
|
|
219
|
+
if (owner.schema !== 1 || !Number.isInteger(owner.pid) || owner.pid <= 0 || !UUID.test(owner.token))
|
|
220
|
+
fail('invalid-lock-owner');
|
|
221
|
+
try {
|
|
222
|
+
process.kill(owner.pid, 0);
|
|
223
|
+
fail('installation-busy-lock');
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
if (e.code !== 'ESRCH')
|
|
227
|
+
throw e;
|
|
228
|
+
}
|
|
229
|
+
// Normal updaters cannot remove this lock; recovery contenders are serialized.
|
|
230
|
+
if (JSON.stringify(readRecord(ownerFile)) !== JSON.stringify(owner))
|
|
231
|
+
fail('lock-owner-changed');
|
|
232
|
+
fs.renameSync(lock, path.join(dir, 'orphaned-lock-' + owner.token));
|
|
233
|
+
syncDirectory(dir);
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
fs.rmdirSync(guard);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
fs.mkdirSync(lock, { mode: 0o700 });
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
if (e.code === 'EEXIST')
|
|
244
|
+
fail('installation-busy-lock');
|
|
245
|
+
throw e;
|
|
246
|
+
}
|
|
247
|
+
const lockToken = randomUUID();
|
|
248
|
+
try {
|
|
249
|
+
writeRecord(path.join(lock, 'owner.json'), { schema: 1, pid: process.pid, token: lockToken });
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
try {
|
|
253
|
+
fs.rmdirSync(lock);
|
|
254
|
+
}
|
|
255
|
+
catch { /* leave failed ownership evidence */ }
|
|
256
|
+
throw e;
|
|
257
|
+
}
|
|
258
|
+
syncDirectory(dir);
|
|
259
|
+
return { installationId, dir, lock, lockToken, journalPath: path.join(dir, 'transaction.json'), watermarkPath: path.join(dir, 'watermark.json') };
|
|
260
|
+
}
|
|
261
|
+
function releaseLock(state) {
|
|
262
|
+
if (!state?.lock)
|
|
263
|
+
return;
|
|
264
|
+
try {
|
|
265
|
+
const ownerFile = path.join(state.lock, 'owner.json'), owner = readRecord(ownerFile);
|
|
266
|
+
if (owner.pid !== process.pid || owner.token !== state.lockToken)
|
|
267
|
+
return;
|
|
268
|
+
fs.unlinkSync(ownerFile);
|
|
269
|
+
fs.rmdirSync(state.lock);
|
|
270
|
+
syncDirectory(state.dir);
|
|
271
|
+
}
|
|
272
|
+
catch { /* Never remove a lock whose ownership cannot be established. */ }
|
|
273
|
+
}
|
|
274
|
+
function backupSnapshot(snapshot, backupRoot) {
|
|
275
|
+
fs.mkdirSync(backupRoot, { recursive: true, mode: 0o700 });
|
|
276
|
+
for (const directory of snapshot.directories)
|
|
277
|
+
fs.mkdirSync(path.join(backupRoot, directory.path), { recursive: true, mode: 0o700 });
|
|
278
|
+
for (const file of snapshot.files) {
|
|
279
|
+
const out = path.join(backupRoot, file.path);
|
|
280
|
+
fs.mkdirSync(path.dirname(out), { recursive: true, mode: 0o700 });
|
|
281
|
+
const fd = fs.openSync(out, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, file.mode);
|
|
282
|
+
try {
|
|
283
|
+
fs.writeFileSync(fd, file.bytes);
|
|
284
|
+
fs.fchmodSync(fd, file.mode);
|
|
285
|
+
fs.fsyncSync(fd);
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
fs.closeSync(fd);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
for (const directory of [...snapshot.directories].reverse()) {
|
|
292
|
+
const dir = path.join(backupRoot, directory.path);
|
|
293
|
+
fs.chmodSync(dir, directory.mode);
|
|
294
|
+
syncDirectory(dir);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function validJournal(journal, installation, state) {
|
|
298
|
+
if (journal?.schema !== 1 || journal.installationId !== state.installationId || journal.packageRoot !== installation.packageRoot || journal.binPath !== installation.binPath || !UUID.test(journal.transactionId) || !/^[a-f0-9]{64}$/.test(journal.previous?.packageDigest || '') || !STABLE.test(journal.previous?.version || '') || journal.previous?.provenance !== 'LEGACY_LOCAL_RECOVERY' || !STABLE.test(journal.candidate?.version || '') || !/^[a-f0-9]{64}$/.test(journal.candidate?.digest || '') || !/^[a-f0-9]{40}$/.test(journal.candidate?.commit || '') || typeof journal.synthetic !== 'boolean')
|
|
299
|
+
fail('invalid-recovery-journal');
|
|
300
|
+
if (journal.previous.selection?.kind !== 'symlink' || path.resolve(path.dirname(installation.binPath), journal.previous.selection.link) !== path.join(installation.packageRoot, 'index.mjs'))
|
|
301
|
+
fail('invalid-recovery-selection');
|
|
302
|
+
return journal;
|
|
303
|
+
}
|
|
304
|
+
function rollback(installation, state, journal, context) {
|
|
305
|
+
try {
|
|
306
|
+
validJournal(journal, installation, state);
|
|
307
|
+
const backup = path.join(state.dir, 'transactions', journal.transactionId, 'previous/package');
|
|
308
|
+
const snapshot = packageSnapshot(backup);
|
|
309
|
+
if (snapshot.digest !== journal.previous.packageDigest)
|
|
310
|
+
fail('recovery-backup-digest-mismatch');
|
|
311
|
+
const current = targetState(installation);
|
|
312
|
+
if (stateMatches(current, journal.previous)) {
|
|
313
|
+
journal.state = 'ROLLED_BACK';
|
|
314
|
+
writeRecord(state.journalPath, journal);
|
|
315
|
+
return outcome('ROLLED_BACK', context);
|
|
316
|
+
}
|
|
317
|
+
if (!journal.written || !stateMatches(current, journal.written))
|
|
318
|
+
fail('recovery-target-drift-or-unobserved-write');
|
|
319
|
+
const restored = path.join(state.dir, 'transactions', journal.transactionId, 'restored-' + randomUUID());
|
|
320
|
+
backupSnapshot(snapshot, restored);
|
|
321
|
+
// Check immediately before replacement, without restoring a shared npm prefix.
|
|
322
|
+
if (!stateMatches(targetState(installation), journal.written))
|
|
323
|
+
fail('recovery-target-drift');
|
|
324
|
+
const displaced = path.join(state.dir, 'transactions', journal.transactionId, 'failed-package-' + randomUUID());
|
|
325
|
+
fs.renameSync(installation.packageRoot, displaced);
|
|
326
|
+
if (packageSnapshot(displaced).digest !== journal.written.packageDigest) {
|
|
327
|
+
if (!fs.existsSync(installation.packageRoot))
|
|
328
|
+
fs.renameSync(displaced, installation.packageRoot);
|
|
329
|
+
fail('recovery-target-changed-at-replacement');
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
fs.renameSync(restored, installation.packageRoot);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
fs.renameSync(displaced, installation.packageRoot);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
const selected = binSelection(installation.binPath, installation.packageRoot);
|
|
339
|
+
if (JSON.stringify(selected) !== JSON.stringify(journal.written.selection))
|
|
340
|
+
fail('recovery-bin-drift');
|
|
341
|
+
if (JSON.stringify(selected) !== JSON.stringify(journal.previous.selection)) {
|
|
342
|
+
const displacedBin = path.join(state.dir, 'transactions', journal.transactionId, 'failed-bin-' + randomUUID());
|
|
343
|
+
fs.renameSync(installation.binPath, displacedBin);
|
|
344
|
+
if (fs.readlinkSync(displacedBin) !== journal.written.selection.link) {
|
|
345
|
+
if (!fs.existsSync(installation.binPath))
|
|
346
|
+
fs.renameSync(displacedBin, installation.binPath);
|
|
347
|
+
fail('recovery-bin-changed-at-replacement');
|
|
348
|
+
}
|
|
349
|
+
// symlink creation is exclusive: a concurrent new selection is never replaced.
|
|
350
|
+
fs.symlinkSync(journal.previous.selection.link, installation.binPath);
|
|
351
|
+
}
|
|
352
|
+
syncDirectory(path.dirname(installation.packageRoot));
|
|
353
|
+
syncDirectory(path.dirname(installation.binPath));
|
|
354
|
+
if (!stateMatches(targetState(installation), journal.previous))
|
|
355
|
+
fail('restored-state-mismatch');
|
|
356
|
+
journal.state = 'ROLLED_BACK';
|
|
357
|
+
journal.recovery = 'RESTORED_PREEXISTING_OPERATOR_STATE';
|
|
358
|
+
writeRecord(state.journalPath, journal);
|
|
359
|
+
return outcome('ROLLED_BACK', context);
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
journal.state = 'RECOVERY_REQUIRED';
|
|
363
|
+
journal.failure = error.activationReason || 'recovery-check-failed';
|
|
364
|
+
try {
|
|
365
|
+
writeRecord(state.journalPath, journal);
|
|
366
|
+
}
|
|
367
|
+
catch { /* preserve original journal if its writer fails */ }
|
|
368
|
+
return outcome('RECOVERY_REQUIRED', context, journal.failure);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/** No candidate process executes until the ticket and exact bytes are admitted. */
|
|
372
|
+
export function activateNpmRelease(admission, options) {
|
|
373
|
+
let state = null, journal = null, installation = null, context = {};
|
|
374
|
+
try {
|
|
375
|
+
const api = options?.testing?.access || access;
|
|
376
|
+
const info = api.getAdmissionInfo(admission);
|
|
377
|
+
if (!info?.isProduction && !options?.testing)
|
|
378
|
+
fail('production-artifact-admission-required');
|
|
379
|
+
if (!info || !STABLE.test(info.version))
|
|
380
|
+
fail('invalid-admission-version');
|
|
381
|
+
const artifact = api.getAdmittedArtifact(admission, 'npm'), payload = api.inspectNpmPayload(admission);
|
|
382
|
+
installation = resolveNpmInstallation(options.binaryPath);
|
|
383
|
+
const synthetic = fixtureMode(options.testing, installation.prefix, options.stateRoot, artifact.path);
|
|
384
|
+
if (synthetic && info.isProduction)
|
|
385
|
+
fail('synthetic-adapter-cannot-activate-production-ticket');
|
|
386
|
+
context = { synthetic, version: info.version };
|
|
387
|
+
const statePath = path.resolve(options.stateRoot), archivePath = path.resolve(artifact.path);
|
|
388
|
+
if (statePath === installation.prefix || within(installation.prefix, statePath) || archivePath === installation.prefix || within(installation.prefix, archivePath))
|
|
389
|
+
fail('staging-inside-installation');
|
|
390
|
+
if (payload.version !== info.version || !Array.isArray(payload.files))
|
|
391
|
+
fail('payload-version-mismatch');
|
|
392
|
+
if (hash(readRegular(artifact.path)) !== artifact.sha256)
|
|
393
|
+
fail('artifact-digest-mismatch');
|
|
394
|
+
if (compare(info.version, installation.version) < 0)
|
|
395
|
+
fail('candidate-downgrade');
|
|
396
|
+
const previous = packageSnapshot(installation.packageRoot), selection = binSelection(installation.binPath, installation.packageRoot);
|
|
397
|
+
state = privateInstallationState(options.stateRoot, installation.packageRoot);
|
|
398
|
+
context.installationId = state.installationId;
|
|
399
|
+
if (fs.statSync(state.dir).dev !== fs.statSync(installation.packageRoot).dev)
|
|
400
|
+
fail('cross-device-recovery-unsupported');
|
|
401
|
+
if (fs.existsSync(state.journalPath)) {
|
|
402
|
+
const old = readRecord(state.journalPath);
|
|
403
|
+
if (!!old.synthetic !== synthetic)
|
|
404
|
+
fail('installation-authority-mismatch');
|
|
405
|
+
if (!['SMOKE_PASSED', 'ROLLED_BACK', 'REFUSED'].includes(old.state))
|
|
406
|
+
return outcome('RECOVERY_REQUIRED', context, 'unfinished-activation');
|
|
407
|
+
}
|
|
408
|
+
if (fs.existsSync(state.watermarkPath)) {
|
|
409
|
+
const watermark = readRecord(state.watermarkPath);
|
|
410
|
+
if (watermark.schema !== 1 || !STABLE.test(watermark.version) || !/^[a-f0-9]{64}$/.test(watermark.digest || ''))
|
|
411
|
+
fail('invalid-admission-watermark');
|
|
412
|
+
if (!!watermark.synthetic !== synthetic)
|
|
413
|
+
fail('watermark-authority-mismatch');
|
|
414
|
+
if (compare(info.version, watermark.version) < 0)
|
|
415
|
+
fail('candidate-below-watermark');
|
|
416
|
+
if (info.version === watermark.version) {
|
|
417
|
+
if (artifact.sha256 !== watermark.digest)
|
|
418
|
+
fail('same-version-content-conflict');
|
|
419
|
+
if (installation.version !== info.version || previous.filesDigest !== expectedCandidate(payload.files))
|
|
420
|
+
fail('selected-package-drift');
|
|
421
|
+
return outcome('ALREADY_ACTIVE', context);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else if (info.version === installation.version)
|
|
425
|
+
fail('same-version-without-retained-admission');
|
|
426
|
+
const txid = randomUUID(), tx = path.join(state.dir, 'transactions', txid);
|
|
427
|
+
fs.mkdirSync(tx, { recursive: true, mode: 0o700 });
|
|
428
|
+
context.transactionId = txid;
|
|
429
|
+
backupSnapshot(previous, path.join(tx, 'previous/package'));
|
|
430
|
+
syncAncestors(path.join(tx, 'previous/package'), path.resolve(options.stateRoot));
|
|
431
|
+
const activationArchive = path.join(tx, 'candidate.tgz'), admittedBytes = readRegular(artifact.path, { max: 128 * 1024 * 1024 });
|
|
432
|
+
if (hash(admittedBytes) !== artifact.sha256)
|
|
433
|
+
fail('artifact-changed-before-staging');
|
|
434
|
+
const archiveFd = fs.openSync(activationArchive, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
|
|
435
|
+
try {
|
|
436
|
+
fs.writeFileSync(archiveFd, admittedBytes);
|
|
437
|
+
fs.fsyncSync(archiveFd);
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
fs.closeSync(archiveFd);
|
|
441
|
+
}
|
|
442
|
+
syncDirectory(tx);
|
|
443
|
+
journal = { schema: 1, installationId: state.installationId, transactionId: txid, packageRoot: installation.packageRoot, binPath: installation.binPath,
|
|
444
|
+
previous: { version: installation.version, packageDigest: previous.digest, selection, provenance: 'LEGACY_LOCAL_RECOVERY' },
|
|
445
|
+
candidate: { version: info.version, digest: artifact.sha256, commit: info.source.commit }, state: 'SNAPSHOTTED', synthetic };
|
|
446
|
+
const phase = name => { journal.state = name; writeRecord(state.journalPath, journal); options.testing?.onPhase?.(name); };
|
|
447
|
+
phase('SNAPSHOTTED');
|
|
448
|
+
if (!stateMatches(targetState(installation), journal.previous))
|
|
449
|
+
fail('target-drift-before-activation');
|
|
450
|
+
const checked = api.getAdmittedArtifact(admission, 'npm');
|
|
451
|
+
if (checked.path !== artifact.path || checked.sha256 !== artifact.sha256 || hash(readRegular(artifact.path)) !== artifact.sha256)
|
|
452
|
+
fail('artifact-changed-before-activation');
|
|
453
|
+
const npmPath = fs.realpathSync(options.npmPath), npmStat = fs.lstatSync(npmPath);
|
|
454
|
+
if (!npmStat.isFile() || npmStat.mode & 0o022 || !(npmStat.mode & 0o111))
|
|
455
|
+
fail('unsafe-npm-executable');
|
|
456
|
+
if (!synthetic && !/^[a-f0-9]{64}$/.test(options.npmSha256 || ''))
|
|
457
|
+
fail('trusted-npm-digest-required');
|
|
458
|
+
const verifyNpm = () => { if (options.npmSha256 && hash(readRegular(npmPath, { max: 128 * 1024 * 1024, allowRootOwner: true })) !== options.npmSha256)
|
|
459
|
+
fail('trusted-npm-digest-mismatch'); };
|
|
460
|
+
verifyNpm();
|
|
461
|
+
const env = safeEnvironment(tx, npmPath), run = synthetic ? options.testing.run : spawnSync;
|
|
462
|
+
phase('ACTIVATING');
|
|
463
|
+
verifyNpm();
|
|
464
|
+
const finalArtifact = api.getAdmittedArtifact(admission, 'npm');
|
|
465
|
+
if (finalArtifact.sha256 !== artifact.sha256 || hash(readRegular(activationArchive, { max: 128 * 1024 * 1024 })) !== artifact.sha256)
|
|
466
|
+
fail('artifact-changed-at-activation');
|
|
467
|
+
const args = ['install', '--global', activationArchive, '--ignore-scripts', '--offline', '--prefix', installation.prefix, '--package-lock=false', '--no-save', '--audit=false', '--fund=false'];
|
|
468
|
+
const result = run(npmPath, args, { cwd: tx, env, encoding: 'utf8', timeout: 180000, maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
469
|
+
try {
|
|
470
|
+
journal.written = targetState(installation);
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
journal.written = null;
|
|
474
|
+
}
|
|
475
|
+
phase('ACTIVE');
|
|
476
|
+
if (result?.error || result?.signal || result?.status !== 0)
|
|
477
|
+
return rollback(installation, state, journal, context);
|
|
478
|
+
if (!journal.written || journal.written.filesDigest !== expectedCandidate(payload.files))
|
|
479
|
+
return rollback(installation, state, journal, context);
|
|
480
|
+
if (!stateMatches(targetState(installation), journal.written))
|
|
481
|
+
return outcome('RECOVERY_REQUIRED', context, 'target-drift-after-activation');
|
|
482
|
+
for (const smokeArgs of [[installation.binaryPath, '--version'], [installation.binaryPath, 'codex-host', 'list']]) {
|
|
483
|
+
const smoke = run(process.execPath, smokeArgs, { cwd: tx, env, encoding: 'utf8', timeout: 30000, maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
484
|
+
if (smoke?.error || smoke?.signal || smoke?.status !== 0 || (smokeArgs.includes('--version') && String(smoke.stdout || '').trim() !== info.version))
|
|
485
|
+
return rollback(installation, state, journal, context);
|
|
486
|
+
if (!stateMatches(targetState(installation), journal.written))
|
|
487
|
+
return outcome('RECOVERY_REQUIRED', context, 'target-drift-during-smoke');
|
|
488
|
+
}
|
|
489
|
+
writeRecord(state.watermarkPath, { schema: 1, version: info.version, digest: artifact.sha256, synthetic });
|
|
490
|
+
phase('SMOKE_PASSED');
|
|
491
|
+
return outcome('SMOKE_PASSED', context);
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
if (journal && error.activationCrash)
|
|
495
|
+
return outcome('RECOVERY_REQUIRED', context, 'interrupted-activation');
|
|
496
|
+
if (journal && ['ACTIVATING', 'ACTIVE', 'RECOVERY_REQUIRED'].includes(journal.state)) {
|
|
497
|
+
return rollback(installation, state, journal, context);
|
|
498
|
+
}
|
|
499
|
+
if (journal) {
|
|
500
|
+
journal.state = 'REFUSED';
|
|
501
|
+
try {
|
|
502
|
+
writeRecord(state.journalPath, journal);
|
|
503
|
+
}
|
|
504
|
+
catch { /* do not replace a failed writer with a success */ }
|
|
505
|
+
}
|
|
506
|
+
const reason = error.activationReason || 'activation-check-failed';
|
|
507
|
+
return outcome(reason.startsWith('unsupported') ? 'UNSUPPORTED' : 'REFUSED', context, reason);
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
releaseLock(state);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/** Recovery only restores previously captured operator state; it admits no release. */
|
|
514
|
+
export function recoverNpmActivation(options) {
|
|
515
|
+
let state = null, context = {};
|
|
516
|
+
try {
|
|
517
|
+
const prefix = canonicalDirectory(options.prefix), packageRoot = path.join(prefix, 'lib/node_modules/great-cto'), binPath = path.join(prefix, 'bin/great-cto');
|
|
518
|
+
const synthetic = fixtureMode(options.testing, prefix, options.stateRoot);
|
|
519
|
+
context = { synthetic, recovery: true };
|
|
520
|
+
state = privateInstallationState(options.stateRoot, packageRoot, { recovering: true });
|
|
521
|
+
context.installationId = state.installationId;
|
|
522
|
+
const installation = { prefix, packageRoot, binPath }, journal = validJournal(readRecord(state.journalPath), installation, state);
|
|
523
|
+
if (!!journal.synthetic !== synthetic)
|
|
524
|
+
fail('recovery-authority-mismatch');
|
|
525
|
+
context.version = journal.candidate.version;
|
|
526
|
+
context.transactionId = journal.transactionId;
|
|
527
|
+
if (journal.state === 'SMOKE_PASSED') {
|
|
528
|
+
const watermark = readRecord(state.watermarkPath);
|
|
529
|
+
if (!journal.written || !stateMatches(targetState(installation), journal.written) || watermark.version !== journal.candidate.version || watermark.digest !== journal.candidate.digest || !!watermark.synthetic !== synthetic)
|
|
530
|
+
fail('completed-installation-drift');
|
|
531
|
+
return outcome('ALREADY_ACTIVE', context);
|
|
532
|
+
}
|
|
533
|
+
return rollback(installation, state, journal, context);
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
return outcome('RECOVERY_REQUIRED', context, error.activationReason || 'recovery-check-failed');
|
|
537
|
+
}
|
|
538
|
+
finally {
|
|
539
|
+
releaseLock(state);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/** Inspection is not host activation; external runtime is not covered by a snapshot signature. */
|
|
543
|
+
export function proposeManagedPluginActivation(admission) {
|
|
544
|
+
try {
|
|
545
|
+
const info = getAdmissionInfo(admission);
|
|
546
|
+
if (!info.isProduction)
|
|
547
|
+
fail('production-artifact-admission-required');
|
|
548
|
+
const payload = inspectPluginPayload(admission);
|
|
549
|
+
const manifestFile = payload.files.find(file => file.path === '.claude-plugin/plugin.json');
|
|
550
|
+
const manifest = manifestFile ? JSON.parse(manifestFile.bytes.toString('utf8')) : null;
|
|
551
|
+
if (!manifest || (Array.isArray(manifest.dependencies) && manifest.dependencies.length))
|
|
552
|
+
return outcome('UNSUPPORTED', { version: info.version }, 'external-plugin-dependency-admission-required');
|
|
553
|
+
const mcpFile = payload.files.find(file => file.path === '.codex-plugin/mcp.json');
|
|
554
|
+
if (mcpFile && /\b(?:npx|npm|pnpm|bunx|uvx)\b/.test(mcpFile.bytes.toString('utf8')))
|
|
555
|
+
return outcome('UNSUPPORTED', { version: info.version }, 'external-mcp-runtime-admission-required');
|
|
556
|
+
return outcome('UNSUPPORTED', { version: info.version }, 'native-activation-and-recovery-not-established');
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
return outcome('REFUSED', {}, error.activationReason || 'artifact-admission-required');
|
|
560
|
+
}
|
|
561
|
+
}
|