@dsh-enhanced/plugin-control-plane 0.1.6 → 0.1.12

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.
Files changed (67) hide show
  1. package/README.md +153 -32
  2. package/bin/dsh-local-release-adapter.js +1381 -0
  3. package/cordis.patch.yml +1 -0
  4. package/lib/approval.d.ts +13 -0
  5. package/lib/approval.d.ts.map +1 -0
  6. package/lib/approval.js +79 -0
  7. package/lib/approval.js.map +1 -0
  8. package/lib/attestation.d.ts +14 -0
  9. package/lib/attestation.d.ts.map +1 -0
  10. package/lib/attestation.js +222 -0
  11. package/lib/attestation.js.map +1 -0
  12. package/lib/catalog-interpreter.d.ts +12 -0
  13. package/lib/catalog-interpreter.d.ts.map +1 -0
  14. package/lib/catalog-interpreter.js +100 -0
  15. package/lib/catalog-interpreter.js.map +1 -0
  16. package/lib/catalog.d.ts +95 -10
  17. package/lib/catalog.d.ts.map +1 -1
  18. package/lib/catalog.js +1031 -18
  19. package/lib/catalog.js.map +1 -1
  20. package/lib/cli.d.ts +9 -0
  21. package/lib/cli.d.ts.map +1 -1
  22. package/lib/cli.js +1160 -162
  23. package/lib/cli.js.map +1 -1
  24. package/lib/host-attestor.d.ts +13 -0
  25. package/lib/host-attestor.d.ts.map +1 -0
  26. package/lib/host-attestor.js +139 -0
  27. package/lib/host-attestor.js.map +1 -0
  28. package/lib/index.d.ts +9 -0
  29. package/lib/index.d.ts.map +1 -1
  30. package/lib/index.js +9 -0
  31. package/lib/index.js.map +1 -1
  32. package/lib/lockfile.d.ts +7 -0
  33. package/lib/lockfile.d.ts.map +1 -0
  34. package/lib/lockfile.js +271 -0
  35. package/lib/lockfile.js.map +1 -0
  36. package/lib/release.d.ts +51 -0
  37. package/lib/release.d.ts.map +1 -0
  38. package/lib/release.js +1188 -0
  39. package/lib/release.js.map +1 -0
  40. package/lib/service.d.ts +9 -11
  41. package/lib/service.d.ts.map +1 -1
  42. package/lib/service.js +57 -29
  43. package/lib/service.js.map +1 -1
  44. package/lib/sqlite.d.ts +9 -0
  45. package/lib/sqlite.d.ts.map +1 -0
  46. package/lib/sqlite.js +705 -0
  47. package/lib/sqlite.js.map +1 -0
  48. package/lib/store.d.ts +258 -0
  49. package/lib/store.d.ts.map +1 -0
  50. package/lib/store.js +1848 -0
  51. package/lib/store.js.map +1 -0
  52. package/lib/tools.d.ts.map +1 -1
  53. package/lib/tools.js +23 -3
  54. package/lib/tools.js.map +1 -1
  55. package/lib/trust.d.ts +79 -0
  56. package/lib/trust.d.ts.map +1 -0
  57. package/lib/trust.js +477 -0
  58. package/lib/trust.js.map +1 -0
  59. package/lib/types.d.ts +740 -0
  60. package/lib/types.d.ts.map +1 -0
  61. package/lib/types.js +2 -0
  62. package/lib/types.js.map +1 -0
  63. package/lib/version.d.ts +1 -1
  64. package/lib/version.d.ts.map +1 -1
  65. package/lib/version.js +1 -1
  66. package/lib/version.js.map +1 -1
  67. package/package.json +2 -2
package/lib/cli.js CHANGED
@@ -1,15 +1,35 @@
1
- import { cp, mkdir, readFile, rename, rm, stat, unlink, writeFile } from 'node:fs/promises';
2
- import { dirname, isAbsolute, join, resolve } from 'node:path';
3
- import { spawnSync } from 'node:child_process';
4
- import { candidateDigest, discover, loadCatalogWithFirstPartyFallback, parseCatalog } from './catalog.js';
5
- const profilePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
6
- const planIdPattern = /^plugin-[a-f0-9]{24}$/u;
7
- const maxPlanLifetimeMs = 86_400_000;
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { constants as fsConstants, lstatSync } from 'node:fs';
4
+ import { cp, lstat, mkdir, mkdtemp, open, readFile, realpath, rename, rm, stat } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
+ import { Ed25519ApprovalAuthority, loadPrivateApprovalInput, parseApprovalReceipt } from './approval.js';
9
+ import { Ed25519HostAttestationAuthority, parseHostAttestationReceipt } from './attestation.js';
10
+ import { invokeConfiguredHostAttestor, prepareConfiguredHostAttestation, prepareManualHostAttestation } from './host-attestor.js';
11
+ import { discover, loadCatalogWithMetadata, previewCatalogAdmission } from './catalog.js';
12
+ import { verifyApprovedPackagesInLockfile } from './lockfile.js';
13
+ import { Ed25519SourcePublishReconciliationAuthority, Ed25519SourceReleaseAuthority, Ed25519SourceReleaseAuthorizationAuthority, invokeSourcePublishReconciliationAdapter, invokeSourceReleaseAdapter, parseSourcePublishReconciliationReceipt, parseSourceReleaseAuthorization, parseSourceReleaseReceipt } from './release.js';
14
+ import { ControlPlaneStore, expectedSourceRelease } from './store.js';
15
+ import { inheritedEnvironment, loadTrustConfig, openTrustedExecutable, resolveTrustKey, verifyOpenTrustedExecutable } from './trust.js';
16
+ const pluginPattern = /^(?=.{1,64}$)[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
17
+ const leaseMs = 30_000;
18
+ const pluginCatalogScope = 'plugins/README.md';
19
+ const maximumActivationArtifactBytes = 268_435_456;
20
+ export class ControlPlaneCliError extends Error {
21
+ code;
22
+ constructor(code, message) {
23
+ super(`plugin-control-plane[${code}]: ${message}`);
24
+ this.code = code;
25
+ this.name = 'ControlPlaneCliError';
26
+ }
27
+ }
8
28
  function option(argv, name) {
9
29
  const index = argv.indexOf(name);
10
30
  const value = index === -1 ? undefined : argv[index + 1];
11
31
  if (value === undefined || value.startsWith('--'))
12
- throw new Error(`plugin-control-plane: ${name} is required`);
32
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', `${name} is required`);
13
33
  return value;
14
34
  }
15
35
  function optionalOption(argv, name) {
@@ -18,196 +38,1174 @@ function optionalOption(argv, name) {
18
38
  return undefined;
19
39
  const value = argv[index + 1];
20
40
  if (value === undefined || value.startsWith('--'))
21
- throw new Error(`plugin-control-plane: ${name} requires a value`);
41
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', `${name} requires a value`);
22
42
  return value;
23
43
  }
24
- function defaultCatalogPath() {
25
- const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? '', '.dsh');
26
- if (!isAbsolute(dshHome))
27
- throw new Error('plugin-control-plane: DSH_HOME must be absolute');
28
- return join(dshHome, 'plugin-control', 'catalog.json');
29
- }
30
- function exactPackages(candidate) {
31
- return [
32
- { package: candidate.package, version: candidate.version, integrity: candidate.integrity },
33
- ...candidate.requires,
34
- ];
35
- }
36
- function validApproval(value) {
37
- if (typeof value !== 'object' || value === null || Array.isArray(value))
38
- return false;
39
- const approval = value;
40
- return typeof approval.principal === 'string' && approval.principal.normalize('NFC').trim() !== ''
41
- && approval.principal.length <= 256 && Number.isSafeInteger(approval.approvedAt);
42
- }
43
- async function atomicJsonWrite(path, value) {
44
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
45
- const temporary = `${path}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
46
- try {
47
- await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
48
- await rename(temporary, path);
49
- }
50
- finally {
51
- await unlink(temporary).catch(() => undefined);
52
- }
53
- }
54
- function run(command, args, cwd, env) {
55
- const outcome = spawnSync(command, [...args], { cwd, env, encoding: 'utf8', maxBuffer: 1_048_576 });
56
- if (outcome.status !== 0)
57
- throw new Error(`plugin-control-plane: ${command} failed: ${(outcome.stderr || outcome.stdout || '').trim().slice(0, 1_000)}`);
58
- return outcome.stdout;
59
- }
60
- async function planFromFile(path) {
61
- const value = JSON.parse(await readFile(path, 'utf8'));
62
- if (typeof value !== 'object' || value === null || Array.isArray(value))
63
- throw new Error('plugin-control-plane: invalid plan');
64
- const plan = value;
65
- const createdAt = plan.createdAt;
66
- const expiresAt = plan.expiresAt;
67
- if (plan.schemaVersion !== 1 || typeof plan.id !== 'string' || typeof plan.profile !== 'string'
68
- || typeof plan.digest !== 'string' || typeof createdAt !== 'number' || typeof expiresAt !== 'number'
69
- || !Number.isSafeInteger(createdAt) || !Number.isSafeInteger(expiresAt)) {
70
- throw new Error('plugin-control-plane: invalid plan');
71
- }
72
- if (!planIdPattern.test(plan.id) || !profilePattern.test(plan.profile)
73
- || !/^[a-f0-9]{64}$/u.test(plan.digest)
74
- || expiresAt <= createdAt || expiresAt - createdAt > maxPlanLifetimeMs) {
75
- throw new Error('plugin-control-plane: invalid plan');
76
- }
77
- const candidate = parseCatalog({ schemaVersion: 1, entries: [plan.candidate] }).entries[0];
78
- if (candidate === undefined || candidateDigest(candidate, plan.profile) !== plan.digest)
79
- throw new Error('plugin-control-plane: plan digest mismatch');
80
- if (Date.now() > expiresAt)
81
- throw new Error('plugin-control-plane: plan has expired');
82
- if (plan.status !== 'pending-owner-approval' && plan.status !== 'owner-approved')
83
- throw new Error('plugin-control-plane: invalid plan status');
84
- if (plan.status === 'owner-approved' && !validApproval(plan.approval))
85
- throw new Error('plugin-control-plane: invalid plan approval');
86
- return {
87
- schemaVersion: 1,
88
- id: plan.id,
89
- status: plan.status,
90
- createdAt,
91
- expiresAt,
92
- profile: plan.profile,
93
- candidate,
94
- digest: plan.digest,
95
- ...(plan.approval === undefined ? {} : { approval: plan.approval }),
96
- };
44
+ function integerOption(argv, name) {
45
+ const result = Number(option(argv, name));
46
+ if (!Number.isSafeInteger(result) || result < 1)
47
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', `${name} must be a positive integer`);
48
+ return result;
97
49
  }
98
- async function approve(argv) {
99
- const path = resolve(option(argv, '--plan'));
100
- const principal = option(argv, '--approved-by').normalize('NFC').trim();
101
- if (principal === '' || principal.length > 256)
102
- throw new Error('plugin-control-plane: invalid approving principal');
103
- const plan = await planFromFile(path);
104
- if (plan.status !== 'pending-owner-approval')
105
- throw new Error('plugin-control-plane: plan is not pending approval');
106
- const approved = { ...plan, status: 'owner-approved', approval: { principal, approvedAt: Date.now() } };
107
- await atomicJsonWrite(path, approved);
108
- process.stdout.write(`Approved ${approved.id}; activation remains a separate command.\n`);
50
+ function defaultDshHome() {
51
+ const value = process.env.DSH_HOME ?? join(process.env.HOME ?? '', '.dsh');
52
+ if (!isAbsolute(value))
53
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', 'DSH_HOME must be absolute');
54
+ return resolve(value);
109
55
  }
110
- async function activate(argv) {
111
- const plan = await planFromFile(resolve(option(argv, '--plan')));
112
- if (plan.status !== 'owner-approved' || plan.approval === undefined)
113
- throw new Error('plugin-control-plane: owner approval is required');
114
- const dshHome = resolve(option(argv, '--dsh-home'));
115
- if (!isAbsolute(dshHome) || !profilePattern.test(plan.profile))
116
- throw new Error('plugin-control-plane: invalid DSH home or profile');
117
- const profilePath = join(dshHome, 'profiles', plan.profile);
118
- const stageProfile = `stage-${plan.id.slice(-24)}`;
119
- const stagePath = join(dshHome, 'profiles', stageProfile);
120
- const backupPath = join(dshHome, 'profiles', `.${plan.profile}.plugin-backup-${plan.id.slice(-12)}`);
121
- await rm(stagePath, { recursive: true, force: true });
122
- await rm(backupPath, { recursive: true, force: true });
56
+ function defaultCatalogPath() { return join(defaultDshHome(), 'plugin-control', 'catalog.json'); }
57
+ function defaultTrustPath(dshHome = defaultDshHome()) { return join(dshHome, 'plugin-control', 'trust.json'); }
58
+ function rejectCommandSuppliedTrust(argv) {
59
+ const forbidden = ['--trust', '--approval-public-key', '--authority', '--key-id', '--state', '--dsh-home', '--attestor',
60
+ '--attestor-path', '--credential', '--password', '--private-key', '--registry-token', '--signing-key', '--token'];
61
+ if (forbidden.some(name => argv.includes(name))) {
62
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', 'command-supplied trust roots or ledgers are forbidden');
63
+ }
64
+ }
65
+ async function commandTrust(argv) {
66
+ rejectCommandSuppliedTrust(argv);
67
+ return loadTrustConfig(defaultTrustPath());
68
+ }
69
+ function exactPackages(plan) { return [...plan.dossier.packages]; }
70
+ function localArtifactReference(item) {
71
+ if (item.registry === undefined)
72
+ return undefined;
123
73
  try {
74
+ return new URL(item.registry.reference).protocol === 'file:' ? item.registry.reference : undefined;
75
+ }
76
+ catch {
77
+ return undefined;
78
+ }
79
+ }
80
+ async function assertDirectory(path, allowMissing = false) {
81
+ try {
82
+ const value = await lstat(path);
83
+ if (!value.isDirectory() || value.isSymbolicLink() || await realpath(path) !== resolve(path))
84
+ throw new ControlPlaneCliError('FILESYSTEM_STATE', 'activation directory is not canonical');
85
+ }
86
+ catch (error) {
87
+ if (allowMissing && typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
88
+ return;
89
+ throw error;
90
+ }
91
+ }
92
+ async function directoryExists(path) {
93
+ try {
94
+ await assertDirectory(path);
95
+ return true;
96
+ }
97
+ catch (error) {
98
+ if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
99
+ return false;
100
+ throw error;
101
+ }
102
+ }
103
+ async function readSafeFile(path, maximum) {
104
+ const value = await lstat(path);
105
+ const uid = process.getuid?.();
106
+ if (!value.isFile() || value.isSymbolicLink() || value.nlink !== 1 || value.size > maximum
107
+ || (uid !== undefined && value.uid !== uid && value.uid !== 0) || await realpath(path) !== resolve(path)) {
108
+ throw new ControlPlaneCliError('FILESYSTEM_STATE', 'input must be one bounded trusted regular file');
109
+ }
110
+ return readFile(path, 'utf8');
111
+ }
112
+ async function readOwnerPrivateFile(path, maximum) {
113
+ const value = await lstat(path);
114
+ const uid = process.getuid?.();
115
+ if (!value.isFile() || value.isSymbolicLink() || value.nlink !== 1 || (value.mode & 0o077) !== 0
116
+ || (uid !== undefined && value.uid !== uid) || await realpath(path) !== resolve(path)) {
117
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'release input must be one canonical owner-private regular file');
118
+ }
119
+ const source = await readFile(path, 'utf8');
120
+ if (Buffer.byteLength(source) > maximum)
121
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'release input exceeds its size bound');
122
+ return source;
123
+ }
124
+ function processIsAlive(pid) {
125
+ if (!Number.isSafeInteger(pid) || pid < 1)
126
+ return true;
127
+ try {
128
+ process.kill(pid, 0);
129
+ return true;
130
+ }
131
+ catch (error) {
132
+ return !(typeof error === 'object' && error !== null && 'code' in error && error.code === 'ESRCH');
133
+ }
134
+ }
135
+ async function acquireProfileLock(store, plan) {
136
+ if (plan.activation === undefined)
137
+ throw new ControlPlaneCliError('LOCK_CONFLICT', 'activation has no fence');
138
+ const lockPath = join(plan.target.dshHome, 'profiles', `.plugin-control-${plan.target.profile}.lock`);
139
+ const payload = `${JSON.stringify({ schemaVersion: 1, planId: plan.id, activationId: plan.activation.id,
140
+ fence: plan.activation.fence, pid: process.pid, nonce: randomUUID() })}\n`;
141
+ const create = async () => {
142
+ const handle = await open(lockPath, 'wx', 0o600);
124
143
  try {
125
- await stat(profilePath);
126
- await cp(profilePath, stagePath, { recursive: true, force: true });
144
+ await handle.writeFile(payload, 'utf8');
145
+ await handle.sync();
146
+ const metadata = await handle.stat({ bigint: true });
147
+ return { path: lockPath, handle, payload, device: metadata.dev, inode: metadata.ino };
127
148
  }
128
149
  catch (error) {
129
- if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT')
130
- throw error;
150
+ await handle.close();
151
+ await rm(lockPath, { force: true });
152
+ throw error;
131
153
  }
132
- const env = { ...process.env, DSH_HOME: dshHome };
133
- const installedDshVersion = run('dsh', ['--version'], undefined, env).trim();
134
- if (installedDshVersion !== plan.candidate.dshBaseline) {
135
- throw new Error(`plugin-control-plane: candidate requires DSH ${plan.candidate.dshBaseline}, found ${installedDshVersion || 'unknown'}`);
154
+ };
155
+ return store.withActivationFileSystemGuard({ planId: plan.id, expectedRevision: plan.revision,
156
+ fence: plan.activation.fence, status: plan.status, leaseMs }, async () => {
157
+ try {
158
+ return await create();
136
159
  }
137
- const packages = exactPackages(plan.candidate);
138
- run('dsh', ['plugin', '--profile', stageProfile, 'add', ...packages.map(item => `${item.package}@${item.version}`)], undefined, env);
139
- const stagedLock = await readFile(join(stagePath, 'pnpm-lock.yaml'), 'utf8');
140
- if (packages.some(item => !stagedLock.includes(item.integrity))) {
141
- throw new Error('plugin-control-plane: staged lockfile integrity does not match the approved catalog');
160
+ catch (error) {
161
+ if (!(typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST'))
162
+ throw error;
163
+ const metadata = await lstat(lockPath, { bigint: true });
164
+ const uid = process.getuid?.();
165
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1n || (metadata.mode & 63n) !== 0n
166
+ || (uid !== undefined && metadata.uid !== BigInt(uid)))
167
+ throw new ControlPlaneCliError('LOCK_CONFLICT', 'profile lock is unsafe');
168
+ let old;
169
+ try {
170
+ old = JSON.parse(await readFile(lockPath, 'utf8'));
171
+ }
172
+ catch {
173
+ throw new ControlPlaneCliError('LOCK_CONFLICT', 'profile lock is corrupt');
174
+ }
175
+ const pid = typeof old === 'object' && old !== null && 'pid' in old ? Number(old.pid) : Number.NaN;
176
+ if (!Number.isSafeInteger(pid) || processIsAlive(pid))
177
+ throw new ControlPlaneCliError('LOCK_CONFLICT', 'profile lock owner may still be executing');
178
+ const stale = `${lockPath}.stale-${randomUUID()}`;
179
+ await rename(lockPath, stale);
180
+ const moved = await lstat(stale, { bigint: true });
181
+ if (moved.dev !== metadata.dev || moved.ino !== metadata.ino)
182
+ throw new ControlPlaneCliError('LOCK_CONFLICT', 'profile lock identity changed during stale recovery');
183
+ try {
184
+ return await create();
185
+ }
186
+ finally {
187
+ await rm(stale, { force: true });
188
+ }
142
189
  }
143
- run('dsh', ['--profile', stageProfile, '--dump-config'], undefined, env);
190
+ });
191
+ }
192
+ async function releaseProfileLock(store, lock) {
193
+ try {
194
+ await store.withExclusiveWrite(async () => {
195
+ const current = await lstat(lock.path, { bigint: true });
196
+ const held = await lock.handle.stat({ bigint: true });
197
+ if (current.dev !== held.dev || current.ino !== held.ino || current.dev !== lock.device || current.ino !== lock.inode
198
+ || current.nlink !== 1n || await readFile(lock.path, 'utf8') !== lock.payload) {
199
+ throw new ControlPlaneCliError('LOCK_CONFLICT', 'profile lock identity changed before release');
200
+ }
201
+ await rm(lock.path);
202
+ });
203
+ }
204
+ finally {
205
+ await lock.handle.close();
206
+ }
207
+ }
208
+ function assertPlanTrust(plan, trust) {
209
+ if (plan.installationId !== trust.installationId || plan.target.dshHome !== trust.dshHome
210
+ || plan.executor.id !== trust.executor.id || plan.executor.version !== trust.executor.version
211
+ || plan.executor.path !== trust.executor.path || plan.executor.sha256 !== trust.executor.sha256
212
+ || plan.ledger.id !== trust.ledger.id || plan.ledger.path !== trust.ledger.path
213
+ || plan.target.profilePath !== join(trust.dshHome, 'profiles', plan.profile)) {
214
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'plan does not match the registered installation, ledger, target, and executor');
215
+ }
216
+ }
217
+ async function openCurrentTrustedExecutable(path, trustedRunningNode) {
218
+ const canonical = await realpath(path);
219
+ const metadata = await lstat(canonical);
220
+ if (!metadata.isFile() || metadata.size > maximumActivationArtifactBytes) {
221
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'registered executor interpreter is not a bounded regular file');
222
+ }
223
+ if (!trustedRunningNode && metadata.uid !== 0) {
224
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'registered executor interpreter must be root-owned');
225
+ }
226
+ const expectedSha256 = createHash('sha256').update(await readFile(canonical)).digest('hex');
227
+ const executable = await openTrustedExecutable(canonical, expectedSha256);
228
+ if (trustedRunningNode) {
229
+ const running = await stat('/proc/self/exe', { bigint: true });
230
+ if (executable.snapshot.device !== running.dev || executable.snapshot.inode !== running.ino) {
231
+ await executable.handle.close();
232
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'registered executor Node interpreter is not the running trusted runtime');
233
+ }
234
+ }
235
+ return executable;
236
+ }
237
+ async function verifyPinnedDescriptor(value) {
238
+ const metadata = await value.handle.stat({ bigint: true });
239
+ if (!metadata.isFile() || metadata.nlink > 1n || metadata.dev !== value.snapshot.device || metadata.ino !== value.snapshot.inode
240
+ || metadata.size > BigInt(maximumActivationArtifactBytes) || (metadata.mode & 73n) === 0n || (metadata.mode & 18n) !== 0n) {
241
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'registered executable descriptor changed while a command was running');
242
+ }
243
+ const bytes = Buffer.alloc(Number(metadata.size));
244
+ let offset = 0;
245
+ while (offset < bytes.length) {
246
+ const result = await value.handle.read(bytes, offset, bytes.length - offset, offset);
247
+ if (result.bytesRead === 0)
248
+ break;
249
+ offset += result.bytesRead;
250
+ }
251
+ if (offset !== bytes.length || createHash('sha256').update(bytes).digest('hex') !== value.snapshot.sha256) {
252
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'registered executable descriptor bytes changed while a command was running');
253
+ }
254
+ }
255
+ async function executorInterpreter(executable) {
256
+ const prefix = Buffer.alloc(512);
257
+ const { bytesRead } = await executable.handle.read(prefix, 0, prefix.length, 0);
258
+ const line = prefix.subarray(0, bytesRead).toString('utf8').split('\n', 1)[0] ?? '';
259
+ if (!line.startsWith('#!'))
260
+ return undefined;
261
+ const declaration = line.slice(2).trim();
262
+ const separator = declaration.search(/\s/u);
263
+ let path = separator === -1 ? declaration : declaration.slice(0, separator);
264
+ let argument = separator === -1 ? undefined : declaration.slice(separator).trim();
265
+ if (!isAbsolute(path) || path.includes('\0') || argument?.includes('\0')) {
266
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'registered executor has an unsupported interpreter declaration');
267
+ }
268
+ if (path === '/usr/bin/env') {
269
+ if (argument === 'node')
270
+ path = process.execPath;
271
+ else if (argument === 'bash')
272
+ path = '/usr/bin/bash';
273
+ else
274
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'registered executor env interpreter is unsupported');
275
+ argument = undefined;
276
+ }
277
+ else {
278
+ path = await realpath(path);
279
+ }
280
+ const interpreter = await openCurrentTrustedExecutable(path, path === process.execPath);
281
+ const interpreterPrefix = Buffer.alloc(2);
282
+ const current = await interpreter.handle.read(interpreterPrefix, 0, 2, 0);
283
+ if (current.bytesRead === 2 && interpreterPrefix.toString('utf8') === '#!') {
284
+ await interpreter.handle.close();
285
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'registered executor interpreter must be a native executable');
286
+ }
287
+ return { executable: interpreter, arguments: argument === undefined ? [] : [argument] };
288
+ }
289
+ async function runBounded(input) {
290
+ const execute = async () => {
291
+ const ownedExecutable = input.plan === undefined || input.pinnedExecutable !== undefined ? undefined
292
+ : await openTrustedExecutable(input.executable, input.plan.executor.sha256);
293
+ const executable = input.pinnedExecutable ?? ownedExecutable;
294
+ let interpreter = input.pinnedInterpreter;
295
+ let result = '';
296
+ let failure;
144
297
  try {
145
- await rename(profilePath, backupPath);
298
+ if (executable !== undefined && interpreter === undefined)
299
+ interpreter = await executorInterpreter(executable);
300
+ result = await new Promise((resolvePromise, reject) => {
301
+ let command = input.executable;
302
+ let stdio = ['ignore', 'pipe', 'ignore'];
303
+ if (executable !== undefined) {
304
+ assertDescriptorFileSystem('registered executor');
305
+ const executableFd = stdio.length;
306
+ stdio = [...stdio, executable.handle.fd];
307
+ if (interpreter === undefined)
308
+ command = `/proc/self/fd/${executableFd}`;
309
+ else {
310
+ const interpreterFd = stdio.length;
311
+ stdio = [...stdio, interpreter.executable.handle.fd];
312
+ command = `/proc/self/fd/${interpreterFd}`;
313
+ }
314
+ }
315
+ let commandArguments = [...input.args];
316
+ if (executable !== undefined && interpreter !== undefined) {
317
+ commandArguments = [...interpreter.arguments, `/proc/self/fd/${3}`, ...commandArguments];
318
+ }
319
+ const child = spawn(command, commandArguments, { cwd: input.cwd, env: input.environment, stdio, shell: false });
320
+ const chunks = [];
321
+ let bytes = 0;
322
+ let outputLimit = false;
323
+ let timedOut = false;
324
+ child.stdout.on('data', (chunk) => {
325
+ if (!input.capture)
326
+ return;
327
+ bytes += chunk.length;
328
+ if (bytes > (input.maximumOutput ?? 65_536)) {
329
+ outputLimit = true;
330
+ child.kill('SIGKILL');
331
+ }
332
+ else
333
+ chunks.push(chunk);
334
+ });
335
+ const timeout = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, 60_000);
336
+ child.once('error', () => { clearTimeout(timeout); reject(new ControlPlaneCliError('EXECUTOR_FAILED', 'registered executor could not start')); });
337
+ child.once('close', code => {
338
+ clearTimeout(timeout);
339
+ if (timedOut)
340
+ reject(new ControlPlaneCliError('EXECUTOR_TIMEOUT', 'registered executor exceeded its deadline'));
341
+ else if (outputLimit)
342
+ reject(new ControlPlaneCliError('EXECUTOR_OUTPUT_LIMIT', 'registered executor exceeded its output bound'));
343
+ else if (code !== 0)
344
+ reject(new ControlPlaneCliError('EXECUTOR_FAILED', 'registered executor returned a non-zero status'));
345
+ else
346
+ resolvePromise(input.capture ? Buffer.concat(chunks).toString('utf8') : '');
347
+ });
348
+ });
146
349
  }
147
350
  catch (error) {
148
- if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT')
149
- throw error;
351
+ failure = error;
352
+ }
353
+ try {
354
+ if (executable !== undefined)
355
+ await verifyPinnedDescriptor(executable);
356
+ if (interpreter !== undefined)
357
+ await verifyOpenTrustedExecutable(interpreter.executable);
358
+ }
359
+ catch {
360
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'registered executor changed while a command was running');
361
+ }
362
+ finally {
363
+ if (input.pinnedInterpreter === undefined && interpreter !== undefined)
364
+ await interpreter.executable.handle.close();
365
+ await ownedExecutable?.handle.close();
366
+ }
367
+ if (failure !== undefined)
368
+ throw failure;
369
+ return result;
370
+ };
371
+ return input.store !== undefined && input.plan !== undefined
372
+ ? input.store.withActivationFileSystemGuard({ planId: input.plan.id, expectedRevision: input.plan.revision,
373
+ fence: input.plan.activation.fence, status: input.plan.status, leaseMs }, execute)
374
+ : execute();
375
+ }
376
+ function assertDescriptorFileSystem(label) {
377
+ if (process.platform !== 'linux')
378
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', `${label} requires Linux descriptor pinning`);
379
+ try {
380
+ const metadata = lstatSync('/proc/self/fd');
381
+ if (!metadata.isDirectory())
382
+ throw new Error('not a directory');
383
+ }
384
+ catch {
385
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', `${label} requires /proc/self/fd`);
386
+ }
387
+ }
388
+ function paths(plan) {
389
+ if (plan.activation === undefined)
390
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'activation identity is missing');
391
+ const suffix = plan.activation.id.replace(/[^A-Za-z0-9-]/gu, '').slice(-36);
392
+ const stageProfile = `stage-${suffix}`;
393
+ return { stageProfile, stagePath: join(plan.target.dshHome, 'profiles', stageProfile),
394
+ backupPath: join(plan.target.dshHome, 'profiles', `.${plan.profile}.plugin-backup-${suffix}`) };
395
+ }
396
+ function within(root, path) {
397
+ const suffix = relative(root, path);
398
+ return suffix !== '' && suffix !== '..' && !suffix.startsWith(`..${sep}`) && !isAbsolute(suffix);
399
+ }
400
+ async function syncDirectory(path) {
401
+ const directory = await open(path, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
402
+ try {
403
+ await directory.sync();
404
+ }
405
+ finally {
406
+ await directory.close();
407
+ }
408
+ }
409
+ function descriptorReference(handle) {
410
+ assertDescriptorFileSystem('local artifact activation');
411
+ return `/proc/${process.pid}/fd/${handle.fd}`;
412
+ }
413
+ async function verifyActivationArtifactSnapshot(snapshot) {
414
+ const before = await snapshot.handle.stat({ bigint: true });
415
+ if (!before.isFile() || before.nlink > 1n || before.dev !== snapshot.device || before.ino !== snapshot.inode
416
+ || before.size !== snapshot.size || before.mtimeNs !== snapshot.mtimeNs || (before.mode & 146n) !== 0n) {
417
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'activation artifact changed while the executor was reading it');
418
+ }
419
+ const bytes = Buffer.alloc(Number(snapshot.size));
420
+ let offset = 0;
421
+ while (offset < bytes.length) {
422
+ const result = await snapshot.handle.read(bytes, offset, bytes.length - offset, offset);
423
+ if (result.bytesRead === 0)
424
+ break;
425
+ offset += result.bytesRead;
426
+ }
427
+ const after = await snapshot.handle.stat({ bigint: true });
428
+ if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.mtimeNs !== before.mtimeNs
429
+ || after.ctimeNs !== before.ctimeNs || after.mode !== before.mode || after.nlink !== before.nlink || BigInt(offset) !== snapshot.size
430
+ || !createHash('sha512').update(bytes).digest().equals(snapshot.sha512)) {
431
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'activation artifact bytes changed while the executor was reading them');
432
+ }
433
+ }
434
+ async function closeActivationArtifactSnapshots(snapshots) {
435
+ await Promise.all(snapshots.map(async (snapshot) => snapshot.handle.close()));
436
+ }
437
+ async function snapshotLocalArtifact(plan, item) {
438
+ const registry = item.registry;
439
+ if (registry === undefined)
440
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'local artifact registry binding is missing');
441
+ let sourcePath;
442
+ let registryRoot;
443
+ try {
444
+ sourcePath = fileURLToPath(registry.reference);
445
+ registryRoot = fileURLToPath(registry.locator);
446
+ }
447
+ catch {
448
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'local artifact reference must be a canonical file URL');
449
+ }
450
+ if (pathToFileURL(sourcePath).href !== registry.reference || pathToFileURL(registryRoot).href !== registry.locator
451
+ || !isAbsolute(sourcePath) || resolve(sourcePath) !== sourcePath || !isAbsolute(registryRoot) || resolve(registryRoot) !== registryRoot
452
+ || !within(registryRoot, sourcePath)) {
453
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'local artifact escapes its bound registry');
454
+ }
455
+ const handle = await open(sourcePath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
456
+ let bytes;
457
+ try {
458
+ const before = await handle.stat({ bigint: true });
459
+ const uid = process.getuid?.();
460
+ if (!before.isFile() || before.nlink !== 1n || before.size < 1n || before.size > BigInt(maximumActivationArtifactBytes)
461
+ || (before.mode & 18n) !== 0n || (uid !== undefined && before.uid !== BigInt(uid) && before.uid !== 0n)) {
462
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'local artifact is not one bounded non-writable regular file');
463
+ }
464
+ const parentPath = dirname(sourcePath);
465
+ const canonicalParent = await realpath(parentPath);
466
+ const parent = await lstat(parentPath);
467
+ if (canonicalParent !== parentPath || !parent.isDirectory() || parent.isSymbolicLink() || (parent.mode & 0o022) !== 0
468
+ || (uid !== undefined && parent.uid !== uid && parent.uid !== 0)) {
469
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'local artifact parent directory is not trusted');
470
+ }
471
+ bytes = await handle.readFile();
472
+ const after = await handle.stat({ bigint: true });
473
+ const pathAfter = await lstat(sourcePath, { bigint: true });
474
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs
475
+ || before.ctimeNs !== after.ctimeNs || pathAfter.isSymbolicLink() || pathAfter.dev !== before.dev || pathAfter.ino !== before.ino
476
+ || BigInt(bytes.length) !== before.size)
477
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'local artifact changed during verification');
478
+ }
479
+ finally {
480
+ await handle.close();
481
+ }
482
+ const integrity = `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
483
+ if (integrity !== item.integrity)
484
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'local artifact bytes do not match the approved integrity');
485
+ const cacheRoot = join(plan.target.dshHome, 'plugin-control', 'activation-artifacts');
486
+ await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
487
+ await assertDirectory(cacheRoot);
488
+ const cacheMetadata = await lstat(cacheRoot);
489
+ const uid = process.getuid?.();
490
+ if ((cacheMetadata.mode & 0o077) !== 0 || (uid !== undefined && cacheMetadata.uid !== uid)) {
491
+ throw new ControlPlaneCliError('FILESYSTEM_STATE', 'activation artifact cache is not owner-private');
492
+ }
493
+ const activationId = plan.activation?.id;
494
+ if (activationId === undefined)
495
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'activation identity is missing');
496
+ const activationDirectory = join(cacheRoot, activationId.replace(/[^A-Za-z0-9-]/gu, ''));
497
+ try {
498
+ await mkdir(activationDirectory, { mode: 0o700 });
499
+ }
500
+ catch (error) {
501
+ if (error.code !== 'EEXIST')
502
+ throw error;
503
+ }
504
+ await assertDirectory(activationDirectory);
505
+ const directoryMetadata = await lstat(activationDirectory);
506
+ if ((directoryMetadata.mode & 0o077) !== 0 || (uid !== undefined && directoryMetadata.uid !== uid)) {
507
+ throw new ControlPlaneCliError('FILESYSTEM_STATE', 'activation artifact directory is not owner-private');
508
+ }
509
+ const destination = join(activationDirectory, `${createHash('sha256').update(item.package).digest('hex')}.tgz`);
510
+ let output;
511
+ try {
512
+ output = await open(destination, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW, 0o400);
513
+ await output.writeFile(bytes);
514
+ await output.sync();
515
+ await output.close();
516
+ output = undefined;
517
+ await syncDirectory(activationDirectory);
518
+ await syncDirectory(cacheRoot);
519
+ }
520
+ catch (error) {
521
+ await output?.close().catch(() => undefined);
522
+ if (error.code !== 'EEXIST') {
523
+ await rm(destination, { force: true });
524
+ throw error;
150
525
  }
151
- await rename(stagePath, profilePath);
526
+ const existing = await open(destination, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
152
527
  try {
153
- run('dsh', ['--profile', plan.profile, '--dump-config'], undefined, env);
528
+ const metadata = await existing.stat();
529
+ const current = await existing.readFile();
530
+ if (!metadata.isFile() || metadata.nlink !== 1 || (metadata.mode & 0o222) !== 0 || current.length !== bytes.length
531
+ || !createHash('sha512').update(current).digest().equals(createHash('sha512').update(bytes).digest())) {
532
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'activation artifact cache conflicts with the approved bytes');
533
+ }
534
+ }
535
+ finally {
536
+ await existing.close();
537
+ }
538
+ }
539
+ const pinned = await open(destination, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
540
+ try {
541
+ const metadata = await pinned.stat({ bigint: true });
542
+ const current = await pinned.readFile();
543
+ if (!metadata.isFile() || metadata.nlink !== 1n || metadata.size !== BigInt(bytes.length) || (metadata.mode & 146n) !== 0n
544
+ || !createHash('sha512').update(current).digest().equals(createHash('sha512').update(bytes).digest())) {
545
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'activation artifact cache does not retain the approved bytes');
546
+ }
547
+ const reference = pathToFileURL(descriptorReference(pinned)).href;
548
+ return { package: { ...item, registry: { ...registry, reference } }, handle: pinned, path: destination, device: metadata.dev,
549
+ inode: metadata.ino, size: metadata.size, mtimeNs: metadata.mtimeNs, ctimeNs: metadata.ctimeNs,
550
+ sha512: createHash('sha512').update(current).digest() };
551
+ }
552
+ catch (error) {
553
+ await pinned.close();
554
+ throw error;
555
+ }
556
+ }
557
+ async function activationPackages(plan) {
558
+ const snapshots = [];
559
+ try {
560
+ for (const item of exactPackages(plan)) {
561
+ if (localArtifactReference(item) !== undefined)
562
+ snapshots.push(await snapshotLocalArtifact(plan, item));
563
+ }
564
+ const byPackage = new Map(snapshots.map(snapshot => [snapshot.package.package, snapshot]));
565
+ return { packages: exactPackages(plan).map(item => byPackage.get(item.package)?.package ?? item), snapshots };
566
+ }
567
+ catch (error) {
568
+ await closeActivationArtifactSnapshots(snapshots);
569
+ throw error;
570
+ }
571
+ }
572
+ function installSpec(item) {
573
+ return localArtifactReference(item) ?? `${item.package}@${item.version}`;
574
+ }
575
+ async function verifyInstalledPackages(profilePath, packages) {
576
+ for (const item of packages) {
577
+ const manifestPath = join(profilePath, 'node_modules', ...item.package.split('/'), 'package.json');
578
+ let raw;
579
+ try {
580
+ const canonical = await realpath(manifestPath);
581
+ const modulesRoot = join(profilePath, 'node_modules');
582
+ if (!within(modulesRoot, canonical))
583
+ throw new Error('manifest escapes node_modules');
584
+ const handle = await open(canonical, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
585
+ try {
586
+ const before = await handle.stat({ bigint: true });
587
+ const uid = process.getuid?.();
588
+ if (!before.isFile() || before.size < 1n || before.size > 1048576n || (before.mode & 18n) !== 0n
589
+ || (uid !== undefined && before.uid !== BigInt(uid) && before.uid !== 0n))
590
+ throw new Error('manifest is unsafe');
591
+ const bytes = await handle.readFile();
592
+ const after = await handle.stat({ bigint: true });
593
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs
594
+ || before.ctimeNs !== after.ctimeNs || BigInt(bytes.length) !== before.size)
595
+ throw new Error('manifest changed');
596
+ raw = JSON.parse(bytes.toString('utf8'));
597
+ }
598
+ finally {
599
+ await handle.close();
600
+ }
601
+ }
602
+ catch {
603
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', `installed package ${item.package} has no trusted manifest`);
604
+ }
605
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw) || !('name' in raw) || !('version' in raw)
606
+ || raw.name !== item.package || raw.version !== item.version) {
607
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', `installed package ${item.package} has the wrong name or version`);
608
+ }
609
+ }
610
+ }
611
+ async function fencedMutation(store, plan, action) {
612
+ return store.withActivationFileSystemGuard({ planId: plan.id, expectedRevision: plan.revision,
613
+ fence: plan.activation.fence, status: plan.status, leaseMs }, action);
614
+ }
615
+ async function restoreTarget(store, plan, backupPath) {
616
+ const originallyExisted = plan.activation?.targetOriginallyExisted;
617
+ if (originallyExisted === undefined)
618
+ throw new ControlPlaneCliError('FILESYSTEM_STATE', 'activation target baseline is missing');
619
+ const backupExists = await directoryExists(backupPath);
620
+ const targetExists = await directoryExists(plan.target.profilePath);
621
+ if (originallyExisted) {
622
+ if (backupExists) {
623
+ await fencedMutation(store, plan, async () => {
624
+ await rm(plan.target.profilePath, { recursive: true, force: true });
625
+ await rename(backupPath, plan.target.profilePath);
626
+ });
627
+ }
628
+ else if (!targetExists)
629
+ throw new ControlPlaneCliError('FILESYSTEM_STATE', 'original target and its backup are both missing');
630
+ }
631
+ else {
632
+ if (backupExists)
633
+ throw new ControlPlaneCliError('FILESYSTEM_STATE', 'a backup exists for a target recorded as originally absent');
634
+ if (targetExists)
635
+ await fencedMutation(store, plan, () => rm(plan.target.profilePath, { recursive: true, force: true }));
636
+ }
637
+ }
638
+ function advance(store, plan, to, failureCode) {
639
+ return store.advanceActivation({ planId: plan.id, expectedRevision: plan.revision, fence: plan.activation.fence,
640
+ from: plan.status, to, ...(failureCode === undefined ? {} : { failureCode }) });
641
+ }
642
+ async function finishRollback(store, plan, lock) {
643
+ const activationPaths = paths(plan);
644
+ await restoreTarget(store, plan, activationPaths.backupPath);
645
+ await fencedMutation(store, plan, () => rm(activationPaths.stagePath, { recursive: true, force: true }));
646
+ const terminal = advance(store, plan, 'rolled-back');
647
+ await releaseProfileLock(store, lock);
648
+ return terminal;
649
+ }
650
+ async function approve(argv) {
651
+ if (argv.includes('--approved-by')) {
652
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', 'approval trust roots cannot be supplied by the approving command');
653
+ }
654
+ const trust = await commandTrust(argv);
655
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
656
+ try {
657
+ const receipt = parseApprovalReceipt(JSON.parse(await loadPrivateApprovalInput(resolve(option(argv, '--approval-receipt')), 32_768)));
658
+ const kind = option(argv, '--kind');
659
+ const common = { planId: option(argv, '--plan-id'), expectedRevision: integerOption(argv, '--expected-revision'), receipt,
660
+ resolveAuthority: (value) => { const key = resolveTrustKey(trust, 'approval', value.authority, value.keyId); return new Ed25519ApprovalAuthority(key.publicKeyPem, key.authority, key.keyId); },
661
+ idempotencyKey: `approval:${receipt.approvalId}` };
662
+ const result = kind === 'activation' ? await store.approve(common) : kind === 'source' ? await store.approveSource(common)
663
+ : (() => { throw new ControlPlaneCliError('INVALID_ARGUMENT', '--kind must be activation or source'); })();
664
+ process.stdout.write(`${JSON.stringify(result)}\n`);
665
+ }
666
+ finally {
667
+ store.close();
668
+ }
669
+ }
670
+ function activationApprovalAuthority(trust, receipt) {
671
+ const key = resolveTrustKey(trust, 'approval', receipt.authority, receipt.keyId);
672
+ return new Ed25519ApprovalAuthority(key.publicKeyPem, key.authority, key.keyId, () => receipt.decidedAt);
673
+ }
674
+ async function activate(argv) {
675
+ const trust = await commandTrust(argv);
676
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
677
+ let lock;
678
+ try {
679
+ let plan = store.getPlan(option(argv, '--plan-id'));
680
+ assertPlanTrust(plan, trust);
681
+ if (plan.status === 'activated') {
682
+ process.stdout.write(`${JSON.stringify(plan)}\n`);
683
+ return;
684
+ }
685
+ plan = await store.claimActivation({ planId: plan.id, expectedRevision: integerOption(argv, '--expected-revision'), leaseMs,
686
+ resolveApprovalAuthority: receipt => activationApprovalAuthority(trust, receipt) });
687
+ lock = await acquireProfileLock(store, plan);
688
+ const activationPaths = paths(plan);
689
+ if (plan.status === 'rollback-pending') {
690
+ process.stdout.write(`${JSON.stringify(await finishRollback(store, plan, lock))}\n`);
691
+ lock = undefined;
692
+ return;
693
+ }
694
+ if (plan.status === 'commit-pending') {
695
+ await fencedMutation(store, plan, () => rm(activationPaths.backupPath, { recursive: true, force: true }));
696
+ await fencedMutation(store, plan, () => rm(activationPaths.stagePath, { recursive: true, force: true }));
697
+ process.stdout.write(`${JSON.stringify(advance(store, plan, 'activated'))}\n`);
698
+ return;
699
+ }
700
+ if (plan.status !== 'staging')
701
+ throw new ControlPlaneCliError('HOST_ATTESTATION_REQUIRED', 'activation is awaiting a signed Host attestation');
702
+ await assertDirectory(trust.dshHome);
703
+ await assertDirectory(join(trust.dshHome, 'profiles'));
704
+ await assertDirectory(plan.target.profilePath, true);
705
+ const environment = inheritedEnvironment(trust);
706
+ try {
707
+ if (plan.activation?.targetOriginallyExisted === undefined) {
708
+ if (await directoryExists(activationPaths.backupPath) || await directoryExists(activationPaths.stagePath)) {
709
+ throw new ControlPlaneCliError('FILESYSTEM_STATE', 'unbound activation residue requires owner recovery');
710
+ }
711
+ plan = store.recordActivationTargetBaseline({ planId: plan.id, expectedRevision: plan.revision,
712
+ fence: plan.activation.fence, existed: await directoryExists(plan.target.profilePath) });
713
+ }
714
+ await restoreTarget(store, plan, activationPaths.backupPath);
715
+ await fencedMutation(store, plan, () => rm(activationPaths.stagePath, { recursive: true, force: true }));
716
+ if (plan.activation.targetOriginallyExisted) {
717
+ await stat(plan.target.profilePath);
718
+ await fencedMutation(store, plan, () => cp(plan.target.profilePath, activationPaths.stagePath, { recursive: true, force: false, errorOnExist: true }));
719
+ }
720
+ const activationArtifacts = await fencedMutation(store, plan, () => activationPackages(plan));
721
+ let pinnedExecutor;
722
+ let pinnedInterpreter;
723
+ try {
724
+ pinnedExecutor = await openTrustedExecutable(trust.executor.path, plan.executor.sha256);
725
+ pinnedInterpreter = await executorInterpreter(pinnedExecutor);
726
+ const executor = { pinnedExecutable: pinnedExecutor, pinnedInterpreter };
727
+ const version = (await runBounded({ executable: trust.executor.path, args: ['--version'], environment, store, plan, capture: true, ...executor })).trim();
728
+ if (version !== plan.candidate.dshBaseline)
729
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'registered executor baseline differs from the approved dossier');
730
+ await runBounded({ executable: trust.executor.path, args: ['plugin', '--profile', activationPaths.stageProfile, 'add',
731
+ ...activationArtifacts.packages.map(installSpec)], environment, store, plan, ...executor });
732
+ await Promise.all(activationArtifacts.snapshots.map(verifyActivationArtifactSnapshot));
733
+ const lockfileSource = await readSafeFile(join(activationPaths.stagePath, 'pnpm-lock.yaml'), 8 * 1024 * 1024);
734
+ verifyApprovedPackagesInLockfile(lockfileSource, activationArtifacts.packages, activationPaths.stagePath);
735
+ await verifyInstalledPackages(activationPaths.stagePath, activationArtifacts.packages);
736
+ // Configuration materialization is a staging integrity check only. It is
737
+ // deliberately not called readiness, reload, shadow, canary or health.
738
+ await runBounded({ executable: trust.executor.path, args: ['--profile', activationPaths.stageProfile, '--dump-config'],
739
+ environment, store, plan, ...executor });
740
+ }
741
+ finally {
742
+ await closeActivationArtifactSnapshots(activationArtifacts.snapshots);
743
+ if (pinnedInterpreter !== undefined)
744
+ await pinnedInterpreter.executable.handle.close();
745
+ await pinnedExecutor?.handle.close();
746
+ }
747
+ if (plan.activation.targetOriginallyExisted)
748
+ await fencedMutation(store, plan, () => rename(plan.target.profilePath, activationPaths.backupPath));
749
+ await fencedMutation(store, plan, () => rename(activationPaths.stagePath, plan.target.profilePath));
750
+ plan = advance(store, plan, 'awaiting-reload');
751
+ process.stdout.write(`${JSON.stringify(plan)}\n`);
154
752
  }
155
753
  catch (error) {
156
- await rm(profilePath, { recursive: true, force: true });
157
- await rename(backupPath, profilePath);
754
+ plan = advance(store, plan, 'rollback-pending', error instanceof ControlPlaneCliError ? error.code.toLowerCase().replaceAll('_', '-') : 'activation-failed');
755
+ await finishRollback(store, plan, lock);
756
+ lock = undefined;
158
757
  throw error;
159
758
  }
160
- await rm(backupPath, { recursive: true, force: true });
161
- process.stdout.write(`Activated ${packages.map(item => `${item.package}@${item.version}`).join(', ')} atomically; restart the resident service with scripts/install/restart.sh ${plan.profile}.\n`);
162
759
  }
163
760
  finally {
164
- await rm(stagePath, { recursive: true, force: true });
761
+ if (lock !== undefined)
762
+ await releaseProfileLock(store, lock);
763
+ store.close();
764
+ }
765
+ }
766
+ async function attest(argv) {
767
+ const trust = await commandTrust(argv);
768
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
769
+ let lock;
770
+ try {
771
+ const receipt = parseHostAttestationReceipt(JSON.parse(await loadPrivateApprovalInput(resolve(option(argv, '--receipt')), 32_768)));
772
+ const planId = option(argv, '--plan-id');
773
+ const expectedRevision = integerOption(argv, '--expected-revision');
774
+ const expectedFence = integerOption(argv, '--expected-fence');
775
+ const initial = store.getPlan(planId);
776
+ assertPlanTrust(initial, trust);
777
+ const resolveAuthority = (value) => {
778
+ const key = resolveTrustKey(trust, 'host-attestation', value.authority, value.keyId);
779
+ return new Ed25519HostAttestationAuthority(key.publicKeyPem, key.authority, key.keyId);
780
+ };
781
+ await store.runHostAttestationOperation({ operationId: receipt.operationId, expectedRevision, expectedFence,
782
+ execute: async () => receipt, resolveAuthority });
783
+ const result = await store.applyHostAttestation({ planId, expectedRevision, expectedFence, receipt,
784
+ idempotencyKey: `host-attestation:${receipt.operationId}`, resolveAuthority });
785
+ let plan = store.getPlan(result.result.id);
786
+ assertPlanTrust(plan, trust);
787
+ if (plan.status === 'rollback-pending' || plan.status === 'commit-pending') {
788
+ plan = await store.claimActivation({ planId: plan.id, expectedRevision: plan.revision, leaseMs,
789
+ resolveApprovalAuthority: receipt => activationApprovalAuthority(trust, receipt) });
790
+ lock = await acquireProfileLock(store, plan);
791
+ if (plan.status === 'rollback-pending') {
792
+ plan = await finishRollback(store, plan, lock);
793
+ lock = undefined;
794
+ }
795
+ else {
796
+ const activationPaths = paths(plan);
797
+ await fencedMutation(store, plan, () => rm(activationPaths.backupPath, { recursive: true, force: true }));
798
+ await fencedMutation(store, plan, () => rm(activationPaths.stagePath, { recursive: true, force: true }));
799
+ plan = advance(store, plan, 'activated');
800
+ }
801
+ }
802
+ process.stdout.write(`${JSON.stringify({ ...result, result: plan })}\n`);
803
+ }
804
+ finally {
805
+ if (lock !== undefined)
806
+ await releaseProfileLock(store, lock);
807
+ store.close();
808
+ }
809
+ }
810
+ async function hostRequest(argv) {
811
+ const trust = await commandTrust(argv);
812
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
813
+ try {
814
+ const plan = store.getPlan(option(argv, '--plan-id'));
815
+ assertPlanTrust(plan, trust);
816
+ if (plan.revision !== integerOption(argv, '--expected-revision') || plan.activation?.fence !== integerOption(argv, '--expected-fence')) {
817
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'manual Host request targets a stale revision/fence');
818
+ }
819
+ process.stdout.write(`${JSON.stringify(prepareManualHostAttestation(store, plan, trust).request)}\n`);
820
+ }
821
+ finally {
822
+ store.close();
823
+ }
824
+ }
825
+ async function probe(argv) {
826
+ const trust = await commandTrust(argv);
827
+ if (trust.hostAttestor === undefined)
828
+ throw new ControlPlaneCliError('HOST_ATTESTOR_NOT_CONFIGURED', 'deployment has no owner-configured Host attestor; activation remains awaiting its current phase');
829
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
830
+ let lock;
831
+ try {
832
+ const plan = store.getPlan(option(argv, '--plan-id'));
833
+ assertPlanTrust(plan, trust);
834
+ const expectedRevision = integerOption(argv, '--expected-revision');
835
+ const expectedFence = integerOption(argv, '--expected-fence');
836
+ if (plan.revision !== expectedRevision || plan.activation?.fence !== expectedFence) {
837
+ throw new ControlPlaneCliError('ACTIVATION_BINDING', 'configured Host probe targets a stale revision/fence');
838
+ }
839
+ const operation = prepareConfiguredHostAttestation(store, plan, trust);
840
+ const resolveAuthority = (value) => {
841
+ const key = resolveTrustKey(trust, 'host-attestation', value.authority, value.keyId);
842
+ return new Ed25519HostAttestationAuthority(key.publicKeyPem, key.authority, key.keyId);
843
+ };
844
+ const receipt = await store.runHostAttestationOperation({ operationId: operation.operationId, expectedRevision, expectedFence,
845
+ execute: request => invokeConfiguredHostAttestor(trust, request), resolveAuthority });
846
+ const result = await store.applyHostAttestation({ planId: plan.id, expectedRevision, expectedFence, receipt,
847
+ idempotencyKey: `host-attestation:${operation.operationId}`, resolveAuthority });
848
+ let output = result.result;
849
+ if (output.status === 'rollback-pending' || output.status === 'commit-pending') {
850
+ output = await store.claimActivation({ planId: output.id, expectedRevision: output.revision, leaseMs,
851
+ resolveApprovalAuthority: receipt => activationApprovalAuthority(trust, receipt) });
852
+ lock = await acquireProfileLock(store, output);
853
+ if (output.status === 'rollback-pending') {
854
+ output = await finishRollback(store, output, lock);
855
+ lock = undefined;
856
+ }
857
+ else {
858
+ const activationPaths = paths(output);
859
+ await fencedMutation(store, output, () => rm(activationPaths.backupPath, { recursive: true, force: true }));
860
+ await fencedMutation(store, output, () => rm(activationPaths.stagePath, { recursive: true, force: true }));
861
+ output = advance(store, output, 'activated');
862
+ }
863
+ }
864
+ process.stdout.write(`${JSON.stringify({ ...result, result: output })}\n`);
865
+ }
866
+ finally {
867
+ if (lock !== undefined)
868
+ await releaseProfileLock(store, lock);
869
+ store.close();
870
+ }
871
+ }
872
+ async function executable(command, environment) {
873
+ const candidates = command === 'git' ? ['/usr/bin/git', '/bin/git'] : [join(dirname(process.execPath), 'pnpm'),
874
+ ...(environment.PATH ?? '').split(delimiter).filter(isAbsolute).map(directory => join(directory, 'pnpm'))];
875
+ const uid = process.getuid?.();
876
+ for (const candidate of candidates) {
877
+ try {
878
+ const canonical = await realpath(candidate);
879
+ const value = await lstat(canonical);
880
+ const directory = await lstat(dirname(canonical));
881
+ if (value.isFile() && !value.isSymbolicLink() && (value.mode & 0o111) !== 0 && (value.mode & 0o022) === 0
882
+ && (uid === undefined || value.uid === 0 || value.uid === uid) && directory.isDirectory() && (directory.mode & 0o002) === 0)
883
+ return canonical;
884
+ }
885
+ catch { /* next */ }
886
+ }
887
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', `registered local ${command} executable is unavailable`);
888
+ }
889
+ async function localCommand(command, args, cwd, environment, capture = false, maximumOutput) {
890
+ return runBounded({ executable: await executable(command, environment), args, cwd, environment, capture,
891
+ ...(maximumOutput === undefined ? {} : { maximumOutput }) });
892
+ }
893
+ function sourceScope(name) { return [pluginCatalogScope, `plugins/${name}`].sort(); }
894
+ function assertExactSourceScope(plan) {
895
+ const expected = sourceScope(plan.name);
896
+ if (plan.scope.length !== expected.length || plan.scope.some((value, index) => value !== expected[index])) {
897
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source plan scope does not match the exact generator outputs');
898
+ }
899
+ }
900
+ async function changedSourcePaths(worktree, baseCommit, environment) {
901
+ const tracked = await localCommand('git', ['--literal-pathspecs', '-c', 'core.quotepath=false', 'diff', '--no-renames',
902
+ '--name-only', '-z', baseCommit, '--'], worktree, environment, true, 8_388_608);
903
+ const untracked = await localCommand('git', ['--literal-pathspecs', '-c', 'core.quotepath=false', 'ls-files', '--others',
904
+ '--exclude-standard', '-z'], worktree, environment, true, 8_388_608);
905
+ return [...new Set(`${tracked}${untracked}`.split('\0').filter(Boolean))].sort();
906
+ }
907
+ function sourcePathAllowed(path, plan) {
908
+ const pluginRoot = `plugins/${plan.name}`;
909
+ return path === pluginCatalogScope || path === pluginRoot || path.startsWith(`${pluginRoot}/`);
910
+ }
911
+ export async function checkedSourceSnapshot(worktree, baseCommit, scopeInput, environment) {
912
+ const temporary = await mkdtemp(join(tmpdir(), 'dsh-plugin-control-index-'));
913
+ try {
914
+ const scope = [...new Set(scopeInput.map(value => value.normalize('NFC').trim()))].sort();
915
+ if (scope.length === 0 || scope.some(value => value === ''))
916
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'checked source scope is invalid');
917
+ const snapshotEnvironment = { ...environment, GIT_INDEX_FILE: join(temporary, 'index') };
918
+ await localCommand('git', ['read-tree', baseCommit], worktree, snapshotEnvironment);
919
+ await localCommand('git', ['--literal-pathspecs', 'add', '--all', '--', ...scope], worktree, snapshotEnvironment);
920
+ const tree = await localCommand('git', ['--literal-pathspecs', '-c', 'core.quotepath=false', 'ls-files', '--stage', '-z',
921
+ '--', ...scope], worktree, snapshotEnvironment, true, 8_388_608);
922
+ const patch = await localCommand('git', ['--literal-pathspecs', '-c', 'core.quotepath=false', 'diff', '--cached', '--binary',
923
+ '--full-index', '--no-color', baseCommit, '--', ...scope], worktree, snapshotEnvironment, true, 8_388_608);
924
+ const binding = `${baseCommit}\0${JSON.stringify(scope)}\0`;
925
+ return {
926
+ checkedTreeDigest: createHash('sha256').update('dsh-source-tree-v2\0').update(binding).update(tree).digest('hex'),
927
+ checkedPatchDigest: createHash('sha256').update('dsh-source-patch-v2\0').update(binding).update(patch).digest('hex'),
928
+ };
929
+ }
930
+ finally {
931
+ await rm(temporary, { recursive: true, force: true });
932
+ }
933
+ }
934
+ async function sourcePlan(argv) {
935
+ const trust = await commandTrust(argv);
936
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
937
+ try {
938
+ const repository = await realpath(resolve(option(argv, '--repository')));
939
+ const worktree = await realpath(resolve(option(argv, '--worktree')));
940
+ const name = option(argv, '--name');
941
+ if (!pluginPattern.test(name))
942
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', 'plugin name is invalid');
943
+ const environment = inheritedEnvironment(trust);
944
+ const worktrees = (await localCommand('git', ['worktree', 'list', '--porcelain'], repository, environment, true)).split('\n')
945
+ .filter(line => line.startsWith('worktree ')).map(line => resolve(line.slice('worktree '.length)));
946
+ if (worktrees.length < 2 || worktrees[0] === worktree || !worktrees.includes(worktree))
947
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source plan requires a linked non-primary worktree');
948
+ const baseCommit = (await localCommand('git', ['rev-parse', 'HEAD'], worktree, environment, true)).trim();
949
+ const generator = join(repository, 'scripts', 'create-plugin.mjs');
950
+ const generatorDigest = createHash('sha256').update(await readSafeFile(generator, 1_048_576)).digest('hex');
951
+ const output = store.createSourcePlan({ gapId: option(argv, '--gap-id'), repository, worktree, baseCommit, name,
952
+ generatorDigest, scope: sourceScope(name), ttlMs: 900_000, idempotencyKey: option(argv, '--idempotency-key') });
953
+ process.stdout.write(`${JSON.stringify(output)}\n`);
954
+ }
955
+ finally {
956
+ store.close();
165
957
  }
166
958
  }
167
959
  async function scaffold(argv) {
168
- if (!argv.includes('--owner-approved'))
169
- throw new Error('plugin-control-plane: scaffold requires --owner-approved');
170
- const repository = resolve(option(argv, '--repository'));
171
- const name = option(argv, '--name');
172
- const worktrees = run('git', ['worktree', 'list', '--porcelain'], repository).split('\n')
173
- .filter(line => line.startsWith('worktree ')).map(line => resolve(line.slice('worktree '.length)));
174
- if (worktrees.length < 2 || worktrees[0] === repository || !worktrees.includes(repository)) {
175
- throw new Error('plugin-control-plane: source creation is allowed only in a linked Git worktree, never the primary checkout');
176
- }
177
- if (run('git', ['status', '--porcelain'], repository).trim() !== '')
178
- throw new Error('plugin-control-plane: worktree must be clean before scaffolding');
179
- run('pnpm', ['create:plugin', name], repository);
180
- run('pnpm', 'check'.split(' '), repository);
181
- process.stdout.write(`Scaffolded ${name} in an isolated clean worktree and completed pnpm check. Submit it for review/PR; it was not activated.\n`);
182
- }
183
- async function verifyWorktree(argv) {
184
- if (!argv.includes('--owner-approved'))
185
- throw new Error('plugin-control-plane: verify-worktree requires --owner-approved');
186
- const repository = resolve(option(argv, '--repository'));
187
- const worktrees = run('git', ['worktree', 'list', '--porcelain'], repository).split('\n')
188
- .filter(line => line.startsWith('worktree ')).map(line => resolve(line.slice('worktree '.length)));
189
- if (worktrees.length < 2 || worktrees[0] === repository || !worktrees.includes(repository)) {
190
- throw new Error('plugin-control-plane: plugin modification verification is allowed only in a linked Git worktree');
191
- }
192
- run('git', ['diff', '--check'], repository);
193
- run('pnpm', ['check'], repository);
194
- process.stdout.write('Isolated worktree passed pnpm check. Review and submit a PR before publishing or activating it.\n');
960
+ if (argv.includes('--owner-approved'))
961
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', 'source execution requires a signed source plan');
962
+ const trust = await commandTrust(argv);
963
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
964
+ let plan;
965
+ try {
966
+ plan = store.getSourcePlan(option(argv, '--plan-id'));
967
+ if (plan.status !== 'approved' || plan.revision !== integerOption(argv, '--expected-revision'))
968
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'exact approved source plan revision is required');
969
+ assertExactSourceScope(plan);
970
+ if (await realpath(plan.repository) !== plan.repository || await realpath(plan.worktree) !== plan.worktree)
971
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source paths changed');
972
+ const environment = inheritedEnvironment(trust);
973
+ if ((await localCommand('git', ['rev-parse', 'HEAD'], plan.worktree, environment, true)).trim() !== plan.baseCommit
974
+ || createHash('sha256').update(await readSafeFile(join(plan.repository, 'scripts', 'create-plugin.mjs'), 1_048_576)).digest('hex') !== plan.generatorDigest
975
+ || (await localCommand('git', ['status', '--porcelain'], plan.worktree, environment, true)).trim() !== '')
976
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source plan base, generator or clean-worktree binding changed');
977
+ plan = store.beginSourceChecks({ planId: plan.id, expectedRevision: plan.revision });
978
+ try {
979
+ await localCommand('pnpm', ['create:plugin', plan.name], plan.worktree, environment);
980
+ await localCommand('pnpm', ['check'], plan.worktree, environment);
981
+ const changes = await changedSourcePaths(plan.worktree, plan.baseCommit, environment);
982
+ const outsideScope = changes.find(path => !sourcePathAllowed(path, plan));
983
+ if (outsideScope !== undefined)
984
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', `source generator changed files outside its approved scope: ${JSON.stringify(outsideScope)}`);
985
+ const checked = await checkedSourceSnapshot(plan.worktree, plan.baseCommit, plan.scope, environment);
986
+ plan = store.finishSourceChecks({ planId: plan.id, expectedRevision: plan.revision, succeeded: true, ...checked });
987
+ }
988
+ catch (error) {
989
+ plan = store.finishSourceChecks({ planId: plan.id, expectedRevision: plan.revision, succeeded: false });
990
+ throw error;
991
+ }
992
+ process.stdout.write(`${JSON.stringify(plan)}\n`);
993
+ }
994
+ finally {
995
+ store.close();
996
+ }
997
+ }
998
+ function assertReleaseCommand(argv) {
999
+ if (argv.includes('--phase'))
1000
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', 'release phase is derived from durable plan status');
1001
+ }
1002
+ function assertSourceReleaseTrust(plan, trust) {
1003
+ if (trust.schemaVersion !== 4 || trust.releaseRegistry === undefined || plan.release === undefined) {
1004
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source release requires schema-v4 owner trust, exact catalog, registry, and durable release identity');
1005
+ }
1006
+ }
1007
+ function releaseAuthority(trust, receipt) {
1008
+ const key = resolveTrustKey(trust, 'release', receipt.authority, receipt.keyId);
1009
+ return new Ed25519SourceReleaseAuthority(key.publicKeyPem, key.authority, key.keyId, Date.now, (authority, keyId) => {
1010
+ const verifier = trust.releaseAdapters?.['registry-verify'];
1011
+ if (verifier === undefined || verifier.authority !== authority || verifier.keyId !== keyId)
1012
+ return undefined;
1013
+ return resolveTrustKey(trust, 'release', authority, keyId).publicKeyPem;
1014
+ });
1015
+ }
1016
+ function reconciliationAuthority(trust, receipt) {
1017
+ const key = resolveTrustKey(trust, 'release', receipt.authority, receipt.keyId);
1018
+ return new Ed25519SourcePublishReconciliationAuthority(key.publicKeyPem, key.authority, key.keyId);
1019
+ }
1020
+ function releaseAuthorizationAuthority(trust, authorization) {
1021
+ if (authorization === undefined)
1022
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source release authorization is missing');
1023
+ const key = resolveTrustKey(trust, 'release-authorization', authorization.authority, authorization.keyId);
1024
+ const authority = new Ed25519SourceReleaseAuthorizationAuthority(key.publicKeyPem, key.authority, key.keyId);
1025
+ return { verify: (value, plan) => {
1026
+ const { signatureDigest: _signatureDigest, ...signed } = value;
1027
+ return authority.verify(signed, plan);
1028
+ } };
1029
+ }
1030
+ async function prepareRelease(store, trust, plan) {
1031
+ assertSourceReleaseTrust(plan, trust);
1032
+ const expected = expectedSourceRelease(plan.status);
1033
+ if (expected === undefined)
1034
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source plan is not awaiting a release phase');
1035
+ const adapter = trust.releaseAdapters?.[expected.phase];
1036
+ if (adapter === undefined)
1037
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', `no owner-configured adapter is registered for ${expected.phase}`);
1038
+ const adapterIdentity = { id: adapter.id, version: adapter.version, path: adapter.path, sha256: adapter.sha256,
1039
+ interpreter: adapter.interpreter, authority: adapter.authority, keyId: adapter.keyId };
1040
+ let catalog = { id: trust.catalog.id, path: trust.catalog.path };
1041
+ if (expected.phase === 'catalog-admission') {
1042
+ const loaded = await loadCatalogWithMetadata(trust.catalog.path);
1043
+ const preview = previewCatalogAdmission(loaded.catalog, store.sourceReleaseCandidate(plan.id));
1044
+ catalog = { ...catalog, expectedBeforeDigest: preview.beforeCatalogDigest, expectedAfterDigest: preview.afterCatalogDigest };
1045
+ }
1046
+ return store.prepareSourceReleaseOperation({ planId: plan.id, expectedRevision: plan.revision, expectedFence: plan.release.fence,
1047
+ installationId: trust.installationId, ledger: trust.ledger, registry: trust.releaseRegistry, catalog, adapter: adapterIdentity,
1048
+ receiptTtlMs: trust.releaseReceiptTtlMs, resolveAuthorizationAuthority: value => releaseAuthorizationAuthority(trust, value) });
1049
+ }
1050
+ async function releaseStart(argv) {
1051
+ assertReleaseCommand(argv);
1052
+ const trust = await commandTrust(argv);
1053
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
1054
+ try {
1055
+ const authorization = parseSourceReleaseAuthorization(JSON.parse(await readOwnerPrivateFile(resolve(option(argv, '--authorization')), 65_536)));
1056
+ const result = await store.startSourceRelease({ planId: option(argv, '--plan-id'), expectedRevision: integerOption(argv, '--expected-revision'),
1057
+ authorization, resolveAuthority: value => releaseAuthorizationAuthority(trust, value),
1058
+ idempotencyKey: `source-release-authorization:${authorization.authorizationId}` });
1059
+ process.stdout.write(`${JSON.stringify(result)}\n`);
1060
+ }
1061
+ finally {
1062
+ store.close();
1063
+ }
1064
+ }
1065
+ async function releaseRequest(argv) {
1066
+ assertReleaseCommand(argv);
1067
+ const trust = await commandTrust(argv);
1068
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
1069
+ try {
1070
+ const plan = store.getSourcePlan(option(argv, '--plan-id'));
1071
+ if (plan.revision !== integerOption(argv, '--expected-revision') || plan.release?.fence !== integerOption(argv, '--expected-fence')) {
1072
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'release request targets a stale revision/fence');
1073
+ }
1074
+ process.stdout.write(`${JSON.stringify((await prepareRelease(store, trust, plan)).request)}\n`);
1075
+ }
1076
+ finally {
1077
+ store.close();
1078
+ }
1079
+ }
1080
+ async function releaseStep(argv) {
1081
+ assertReleaseCommand(argv);
1082
+ const trust = await commandTrust(argv);
1083
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
1084
+ try {
1085
+ const plan = store.getSourcePlan(option(argv, '--plan-id'));
1086
+ const expectedRevision = integerOption(argv, '--expected-revision');
1087
+ const expectedFence = integerOption(argv, '--expected-fence');
1088
+ if (plan.revision !== expectedRevision || plan.release?.fence !== expectedFence)
1089
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'release step targets a stale revision/fence');
1090
+ const operation = await prepareRelease(store, trust, plan);
1091
+ const receipt = await store.runSourceReleaseOperation({ operationId: operation.operationId, expectedRevision, expectedFence,
1092
+ execute: request => invokeSourceReleaseAdapter(trust, request), resolveAuthority: value => releaseAuthority(trust, value),
1093
+ resolveAuthorizationAuthority: value => releaseAuthorizationAuthority(trust, value) });
1094
+ const result = await store.applySourceRelease({ planId: plan.id, expectedRevision, expectedFence, receipt,
1095
+ resolveAuthority: value => releaseAuthority(trust, value), idempotencyKey: `source-release:${operation.operationId}` });
1096
+ process.stdout.write(`${JSON.stringify(result)}\n`);
1097
+ }
1098
+ finally {
1099
+ store.close();
1100
+ }
1101
+ }
1102
+ async function releaseAttest(argv) {
1103
+ assertReleaseCommand(argv);
1104
+ const trust = await commandTrust(argv);
1105
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
1106
+ try {
1107
+ const plan = store.getSourcePlan(option(argv, '--plan-id'));
1108
+ const expectedRevision = integerOption(argv, '--expected-revision');
1109
+ const expectedFence = integerOption(argv, '--expected-fence');
1110
+ if (plan.revision !== expectedRevision || plan.release?.fence !== expectedFence)
1111
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'release receipt targets a stale revision/fence');
1112
+ const operation = await prepareRelease(store, trust, plan);
1113
+ const receipt = parseSourceReleaseReceipt(JSON.parse(await readOwnerPrivateFile(resolve(option(argv, '--receipt')), 262_144)));
1114
+ if (receipt.operationId !== operation.operationId)
1115
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'release receipt does not target the durable operation');
1116
+ await store.runSourceReleaseOperation({ operationId: operation.operationId, expectedRevision, expectedFence, execute: async () => receipt,
1117
+ resolveAuthority: value => releaseAuthority(trust, value),
1118
+ resolveAuthorizationAuthority: value => releaseAuthorizationAuthority(trust, value) });
1119
+ const result = await store.applySourceRelease({ planId: plan.id, expectedRevision, expectedFence, receipt,
1120
+ resolveAuthority: value => releaseAuthority(trust, value), idempotencyKey: `source-release:${operation.operationId}` });
1121
+ process.stdout.write(`${JSON.stringify(result)}\n`);
1122
+ }
1123
+ finally {
1124
+ store.close();
1125
+ }
1126
+ }
1127
+ async function releaseReconcile(argv) {
1128
+ assertReleaseCommand(argv);
1129
+ if (argv.includes('--observation'))
1130
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', 'unsigned registry observations are forbidden');
1131
+ const trust = await commandTrust(argv);
1132
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
1133
+ try {
1134
+ const plan = store.getSourcePlan(option(argv, '--plan-id'));
1135
+ const expectedRevision = integerOption(argv, '--expected-revision');
1136
+ const expectedFence = integerOption(argv, '--expected-fence');
1137
+ assertSourceReleaseTrust(plan, trust);
1138
+ if (plan.revision !== expectedRevision || plan.release?.fence !== expectedFence || plan.status !== 'publish-ambiguous') {
1139
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'publish reconciliation targets a stale or non-ambiguous release');
1140
+ }
1141
+ const adapter = trust.releaseAdapters?.['registry-verify'];
1142
+ if (adapter === undefined)
1143
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'publish reconciliation requires the independent registry verifier');
1144
+ const adapterIdentity = { id: adapter.id, version: adapter.version, path: adapter.path, sha256: adapter.sha256,
1145
+ interpreter: adapter.interpreter, authority: adapter.authority, keyId: adapter.keyId };
1146
+ const operation = await store.prepareSourcePublishReconciliation({ planId: plan.id, expectedRevision, expectedFence,
1147
+ installationId: trust.installationId, ledger: trust.ledger, registry: trust.releaseRegistry, adapter: adapterIdentity,
1148
+ receiptTtlMs: trust.releaseReceiptTtlMs, resolveAuthorizationAuthority: value => releaseAuthorizationAuthority(trust, value) });
1149
+ const receiptPath = optionalOption(argv, '--receipt');
1150
+ const supplied = receiptPath === undefined ? undefined : parseSourcePublishReconciliationReceipt(JSON.parse(await readOwnerPrivateFile(resolve(receiptPath), 262_144)));
1151
+ if (supplied !== undefined && supplied.operationId !== operation.operationId) {
1152
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'publish reconciliation receipt does not target the durable operation');
1153
+ }
1154
+ const receipt = await store.runSourcePublishReconciliation({ operationId: operation.operationId, expectedRevision, expectedFence,
1155
+ execute: supplied === undefined ? request => invokeSourcePublishReconciliationAdapter(trust, request) : async () => supplied,
1156
+ resolveAuthority: value => reconciliationAuthority(trust, value),
1157
+ resolveAuthorizationAuthority: value => releaseAuthorizationAuthority(trust, value) });
1158
+ const result = await store.reconcileSourcePublish({ planId: plan.id, expectedRevision, expectedFence, receipt,
1159
+ resolveAuthority: value => reconciliationAuthority(trust, value), idempotencyKey: `source-publish-reconciliation:${operation.operationId}` });
1160
+ process.stdout.write(`${JSON.stringify(result)}\n`);
1161
+ }
1162
+ finally {
1163
+ store.close();
1164
+ }
195
1165
  }
196
1166
  export async function runPluginControl(argv = process.argv.slice(2)) {
1167
+ rejectCommandSuppliedTrust(argv);
197
1168
  const command = argv[0];
198
1169
  if (command === 'discover') {
199
- const catalog = await loadCatalogWithFirstPartyFallback(resolve(optionalOption(argv, '--catalog') ?? defaultCatalogPath()));
200
- process.stdout.write(`${JSON.stringify(discover(catalog, option(argv, '--capability')), null, 2)}\n`);
1170
+ const loaded = await loadCatalogWithMetadata(resolve(optionalOption(argv, '--catalog') ?? defaultCatalogPath()));
1171
+ process.stdout.write(`${JSON.stringify({ provenance: loaded.provenance, digest: loaded.digest, candidates: discover(loaded.catalog, option(argv, '--capability')) })}\n`);
1172
+ return;
1173
+ }
1174
+ if (command === 'show') {
1175
+ const trust = await commandTrust(argv);
1176
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
1177
+ try {
1178
+ process.stdout.write(`${JSON.stringify(option(argv, '--kind') === 'source' ? store.getSourcePlan(option(argv, '--plan-id')) : store.getPlan(option(argv, '--plan-id')))}\n`);
1179
+ }
1180
+ finally {
1181
+ store.close();
1182
+ }
201
1183
  return;
202
1184
  }
203
1185
  if (command === 'approve')
204
1186
  return approve(argv);
205
1187
  if (command === 'activate')
206
1188
  return activate(argv);
1189
+ if (command === 'host-request')
1190
+ return hostRequest(argv);
1191
+ if (command === 'probe')
1192
+ return probe(argv);
1193
+ if (command === 'attest')
1194
+ return attest(argv);
1195
+ if (command === 'source-plan')
1196
+ return sourcePlan(argv);
207
1197
  if (command === 'scaffold')
208
1198
  return scaffold(argv);
209
- if (command === 'verify-worktree')
210
- return verifyWorktree(argv);
211
- throw new Error('Usage: dsh-plugin-control <discover|approve|activate|scaffold|verify-worktree> ...');
1199
+ if (command === 'release-start')
1200
+ return releaseStart(argv);
1201
+ if (command === 'release-request')
1202
+ return releaseRequest(argv);
1203
+ if (command === 'release-step')
1204
+ return releaseStep(argv);
1205
+ if (command === 'release-attest')
1206
+ return releaseAttest(argv);
1207
+ if (command === 'release-reconcile')
1208
+ return releaseReconcile(argv);
1209
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', 'usage: dsh-plugin-control <discover|show|approve|activate|host-request|probe|attest|source-plan|scaffold|release-start|release-request|release-step|release-attest|release-reconcile>');
212
1210
  }
213
1211
  //# sourceMappingURL=cli.js.map