@ouro.bot/cli 0.1.0-alpha.796 → 0.1.0-alpha.799

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.
@@ -34,6 +34,10 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SANCTUARY_BUNDLE_ROLLBACK_FILE = exports.SANCTUARY_PACKAGE_MANAGED_FILES = void 0;
37
+ exports.inspectSanctuaryDirectoryFromBase = inspectSanctuaryDirectoryFromBase;
38
+ exports.sanctuaryDirectoriesShareIdentity = sanctuaryDirectoriesShareIdentity;
39
+ exports.inspectSanctuaryPackageManagedBundle = inspectSanctuaryPackageManagedBundle;
40
+ exports.ensureSanctuaryPackageManagedBundle = ensureSanctuaryPackageManagedBundle;
37
41
  exports.inspectSanctuaryPackageManagedBundleRollback = inspectSanctuaryPackageManagedBundleRollback;
38
42
  exports.rollbackSanctuaryPackageManagedBundle = rollbackSanctuaryPackageManagedBundle;
39
43
  exports.commitSanctuaryPackageManagedBundle = commitSanctuaryPackageManagedBundle;
@@ -42,7 +46,6 @@ const fs = __importStar(require("node:fs"));
42
46
  const path = __importStar(require("node:path"));
43
47
  const node_crypto_1 = require("node:crypto");
44
48
  const runtime_1 = require("../../nerves/runtime");
45
- const steward_policy_1 = require("../steward-policy");
46
49
  exports.SANCTUARY_PACKAGE_MANAGED_FILES = [
47
50
  "provider-readiness.json",
48
51
  "tool-profiles.json",
@@ -56,12 +59,83 @@ exports.SANCTUARY_PACKAGE_MANAGED_FILES = [
56
59
  exports.SANCTUARY_BUNDLE_ROLLBACK_FILE = ".sanctuary-package-managed-rollback.json";
57
60
  const SANCTUARY_BUNDLE_COMMITTING_FILE = `${exports.SANCTUARY_BUNDLE_ROLLBACK_FILE}.committing`;
58
61
  const BUNDLE_META_VERSION_FIELDS = ["runtimeVersion", "bundleSchemaVersion", "lastUpdated"];
62
+ class SanctuaryInspectionFault extends Error {
63
+ code;
64
+ constructor(code) {
65
+ super(code);
66
+ this.code = code;
67
+ }
68
+ }
69
+ const CANONICAL_EMPTY_PACKAGED_POLICY = { schemaVersion: 1, version: 0, desiredStates: {}, routineActionGrants: {}, updatedAt: null };
59
70
  function readObject(filePath) {
60
71
  const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
61
72
  if (!value || typeof value !== "object" || Array.isArray(value))
62
73
  throw new Error(`${path.basename(filePath)} must contain an object`);
63
74
  return value;
64
75
  }
76
+ function isMissing(error) {
77
+ return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
78
+ }
79
+ function isFilesystemFailure(error) {
80
+ return !!error && typeof error === "object" && "code" in error && typeof error.code === "string";
81
+ }
82
+ function inspectSanctuaryDirectoryFromBase(baseRoot, segments) {
83
+ if (!path.isAbsolute(baseRoot))
84
+ return null;
85
+ let current = path.resolve(baseRoot);
86
+ let stat = null;
87
+ for (const candidate of [current, ...segments.map((segment) => { current = path.join(current, segment); return current; })]) {
88
+ try {
89
+ stat = fs.lstatSync(candidate);
90
+ }
91
+ catch (error) {
92
+ if (isMissing(error))
93
+ return null;
94
+ throw error;
95
+ }
96
+ if (stat.isSymbolicLink() || !stat.isDirectory())
97
+ return null;
98
+ }
99
+ const realPath = fs.realpathSync(current);
100
+ if (realPath !== path.resolve(current))
101
+ return null;
102
+ return { realPath, device: stat.dev, inode: stat.ino };
103
+ }
104
+ function sanctuaryDirectoriesShareIdentity(left, right) {
105
+ return left.realPath === right.realPath || (left.device === right.device && left.inode === right.inode);
106
+ }
107
+ function inspectStandaloneSanctuaryDirectory(root) {
108
+ if (!path.isAbsolute(root))
109
+ return null;
110
+ const resolved = path.resolve(root);
111
+ return inspectSanctuaryDirectoryFromBase(path.dirname(resolved), [path.basename(resolved)]);
112
+ }
113
+ function isCanonicalEmptyPackagedPolicy(value) {
114
+ return JSON.stringify(Object.keys(value).sort()) === JSON.stringify(Object.keys(CANONICAL_EMPTY_PACKAGED_POLICY).sort())
115
+ && value.schemaVersion === 1
116
+ && value.version === 0
117
+ && value.updatedAt === null
118
+ && !!value.desiredStates
119
+ && typeof value.desiredStates === "object"
120
+ && !Array.isArray(value.desiredStates)
121
+ && Object.keys(value.desiredStates).length === 0
122
+ && !!value.routineActionGrants
123
+ && typeof value.routineActionGrants === "object"
124
+ && !Array.isArray(value.routineActionGrants)
125
+ && Object.keys(value.routineActionGrants).length === 0;
126
+ }
127
+ function validatePackagedPolicy(packageRoot) {
128
+ const relative = "state/policy/steward.json";
129
+ validateDestination(packageRoot, relative);
130
+ requirePlainFile(path.join(packageRoot, relative), "packaged steward policy");
131
+ const value = readObject(path.join(packageRoot, relative));
132
+ if (value.routineActionGrants && typeof value.routineActionGrants === "object" && !Array.isArray(value.routineActionGrants) && Object.keys(value.routineActionGrants).length > 0)
133
+ throw new Error("packaged steward policy must not carry routine action grants; authorize them through an authenticated owner session");
134
+ if (value.desiredStates && typeof value.desiredStates === "object" && !Array.isArray(value.desiredStates) && Object.keys(value.desiredStates).length > 0)
135
+ throw new Error("packaged steward policy must not carry desired state");
136
+ if (!isCanonicalEmptyPackagedPolicy(value))
137
+ throw new Error("packaged steward policy must be the canonical empty policy");
138
+ }
65
139
  function requirePlainFile(filePath, label) {
66
140
  let stat;
67
141
  try {
@@ -102,13 +176,29 @@ function validateDestination(agentRoot, relative) {
102
176
  throw new Error(`package-managed destination must be a regular file: ${relative}`);
103
177
  }
104
178
  }
105
- function writeAtomic(filePath, content) {
179
+ function syncDirectory(directoryPath) {
180
+ const directory = fs.openSync(directoryPath, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
181
+ try {
182
+ fs.fsyncSync(directory);
183
+ }
184
+ finally {
185
+ fs.closeSync(directory);
186
+ }
187
+ }
188
+ function syncNearestExistingDirectory(directoryPath, agentRoot) {
189
+ let current = directoryPath;
190
+ while (current !== agentRoot && !fs.existsSync(current))
191
+ current = path.dirname(current);
192
+ syncDirectory(current);
193
+ }
194
+ function writeAtomic(filePath, content, mode = 0o600) {
106
195
  const stagingDirectory = fs.mkdtempSync(`${filePath}.package-migration.`);
107
196
  const temporary = path.join(stagingDirectory, "value");
108
197
  try {
109
198
  const fd = fs.openSync(temporary, "wx", 0o600);
110
199
  try {
111
200
  fs.writeFileSync(fd, content);
201
+ fs.fchmodSync(fd, mode);
112
202
  fs.fsyncSync(fd);
113
203
  }
114
204
  finally {
@@ -120,41 +210,60 @@ function writeAtomic(filePath, content) {
120
210
  fs.rmSync(temporary, { force: true });
121
211
  fs.rmdirSync(stagingDirectory);
122
212
  }
123
- fs.chmodSync(filePath, 0o600);
124
- const directory = fs.openSync(path.dirname(filePath), fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
125
- try {
126
- fs.fsyncSync(directory);
127
- }
128
- finally {
129
- fs.closeSync(directory);
130
- }
213
+ syncDirectory(path.dirname(filePath));
131
214
  }
132
- function cleanupInterruptedWrites(filePath) {
133
- const parent = path.dirname(filePath);
134
- const prefix = `${path.basename(filePath)}.package-migration.`;
135
- if (!fs.existsSync(parent))
136
- return;
137
- for (const name of fs.readdirSync(parent)) {
138
- if (!name.startsWith(prefix))
139
- continue;
140
- const stagingDirectory = path.join(parent, name);
141
- const stagingStat = fs.lstatSync(stagingDirectory);
142
- if (stagingStat.isSymbolicLink() || !stagingStat.isDirectory())
143
- throw new Error(`interrupted package migration stage is invalid: ${name}`);
144
- const entries = fs.readdirSync(stagingDirectory);
145
- if (entries.some((entry) => entry !== "value"))
146
- throw new Error(`interrupted package migration stage contains unexpected entries: ${name}`);
147
- const temporary = path.join(stagingDirectory, "value");
148
- if (fs.existsSync(temporary)) {
149
- const temporaryStat = fs.lstatSync(temporary);
150
- if (temporaryStat.isSymbolicLink() || !temporaryStat.isFile())
151
- throw new Error(`interrupted package migration value is invalid: ${name}`);
152
- fs.unlinkSync(temporary);
215
+ const SNAPSHOT_RELATIVES = [...exports.SANCTUARY_PACKAGE_MANAGED_FILES, "bundle-meta.json"];
216
+ function packageManagedArtifactPaths(agentRoot) {
217
+ return SNAPSHOT_RELATIVES.map((relative) => path.join(agentRoot, relative));
218
+ }
219
+ function inspectInterruptedWrites(filePaths, restoredModes) {
220
+ const stages = [];
221
+ for (const filePath of [...filePaths].sort()) {
222
+ const parent = path.dirname(filePath);
223
+ const prefix = `${path.basename(filePath)}.package-migration.`;
224
+ let names;
225
+ try {
226
+ names = fs.readdirSync(parent).sort();
227
+ }
228
+ catch (error) {
229
+ if (isMissing(error))
230
+ continue;
231
+ throw error;
232
+ }
233
+ for (const name of names) {
234
+ if (!name.startsWith(prefix))
235
+ continue;
236
+ const stagingDirectory = path.join(parent, name);
237
+ const parentStat = fs.lstatSync(parent);
238
+ const stagingStat = fs.lstatSync(stagingDirectory);
239
+ if (parentStat.isSymbolicLink() || !parentStat.isDirectory() || stagingStat.isSymbolicLink() || !stagingStat.isDirectory() || (stagingStat.mode & 0o777) !== 0o700 || stagingStat.uid !== parentStat.uid || stagingStat.gid !== parentStat.gid)
240
+ throw new Error(`interrupted package migration stage is invalid: ${name}`);
241
+ const entries = fs.readdirSync(stagingDirectory);
242
+ if (entries.some((entry) => entry !== "value"))
243
+ throw new Error(`interrupted package migration stage contains unexpected entries: ${name}`);
244
+ const temporary = entries.includes("value") ? path.join(stagingDirectory, "value") : null;
245
+ if (temporary) {
246
+ const temporaryStat = fs.lstatSync(temporary);
247
+ const temporaryMode = temporaryStat.mode & 0o777;
248
+ if (temporaryStat.isSymbolicLink() || !temporaryStat.isFile() || (temporaryMode !== 0o600 && temporaryMode !== restoredModes?.get(filePath)) || temporaryStat.uid !== stagingStat.uid || temporaryStat.gid !== stagingStat.gid)
249
+ throw new Error(`interrupted package migration value is invalid: ${name}`);
250
+ }
251
+ stages.push({ stagingDirectory, temporary });
153
252
  }
154
- fs.rmdirSync(stagingDirectory);
253
+ }
254
+ return stages;
255
+ }
256
+ function cleanupInterruptedWrites(filePaths, restoredModes) {
257
+ const stages = inspectInterruptedWrites(filePaths, restoredModes);
258
+ for (const stage of stages) {
259
+ if (stage.temporary)
260
+ fs.unlinkSync(stage.temporary);
261
+ fs.rmdirSync(stage.stagingDirectory);
262
+ }
263
+ for (const parent of [...new Set(stages.map((stage) => path.dirname(stage.stagingDirectory)))].sort()) {
264
+ syncDirectory(parent);
155
265
  }
156
266
  }
157
- const SNAPSHOT_RELATIVES = [...exports.SANCTUARY_PACKAGE_MANAGED_FILES, "bundle-meta.json"];
158
267
  function snapshotDirectoryRelatives() {
159
268
  const directories = new Set();
160
269
  for (const relative of SNAPSHOT_RELATIVES) {
@@ -197,20 +306,33 @@ function durableSnapshot(agentRoot, snapshot, rollbackImageId, targetImageId) {
197
306
  return { ...payload, snapshotDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(payload)).digest("hex") };
198
307
  }
199
308
  function readDurableRecord(agentRoot) {
200
- const candidates = [rollbackPath(agentRoot), path.join(agentRoot, SANCTUARY_BUNDLE_COMMITTING_FILE)].filter((candidate) => fs.existsSync(candidate));
309
+ const names = fs.readdirSync(agentRoot);
310
+ const stagePrefixes = [`${exports.SANCTUARY_BUNDLE_ROLLBACK_FILE}.package-migration.`, `${SANCTUARY_BUNDLE_COMMITTING_FILE}.package-migration.`];
311
+ if (names.some((name) => stagePrefixes.some((prefix) => name.startsWith(prefix))))
312
+ throw new Error("Sanctuary bundle journal staging residue requires verified recovery");
313
+ const candidates = [rollbackPath(agentRoot), path.join(agentRoot, SANCTUARY_BUNDLE_COMMITTING_FILE)].flatMap((filePath) => {
314
+ try {
315
+ return [{ filePath, stat: fs.lstatSync(filePath) }];
316
+ }
317
+ catch (error) {
318
+ if (isMissing(error))
319
+ return [];
320
+ throw error;
321
+ }
322
+ });
201
323
  if (candidates.length === 0)
202
324
  return null;
203
325
  if (candidates.length !== 1)
204
326
  throw new Error("Sanctuary bundle rollback record state is ambiguous");
205
- const [filePath] = candidates;
206
- const stat = fs.lstatSync(filePath);
327
+ const [{ filePath, stat }] = candidates;
207
328
  if (stat.isSymbolicLink())
208
329
  throw new Error("Sanctuary bundle rollback record must not be a symlink");
209
330
  if (!stat.isFile() || (stat.mode & 0o777) !== 0o600)
210
331
  throw new Error("Sanctuary bundle rollback record must be a mode-0600 regular file");
332
+ const serialized = fs.readFileSync(filePath, "utf8");
211
333
  let value;
212
334
  try {
213
- value = JSON.parse(fs.readFileSync(filePath, "utf8"));
335
+ value = JSON.parse(serialized);
214
336
  }
215
337
  catch {
216
338
  throw new Error("Sanctuary bundle rollback record is invalid JSON");
@@ -248,20 +370,210 @@ function readDurableRecord(agentRoot) {
248
370
  }
249
371
  function removeRollbackRecord(agentRoot, filePath) {
250
372
  fs.unlinkSync(filePath);
251
- const root = fs.openSync(agentRoot, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
252
- try {
253
- fs.fsyncSync(root);
254
- }
255
- finally {
256
- fs.closeSync(root);
257
- }
373
+ syncDirectory(agentRoot);
258
374
  }
259
375
  function validateAgentRoot(agentRoot) {
260
376
  if (!path.isAbsolute(agentRoot))
261
377
  throw new Error("agent root must be an absolute path");
262
- const stat = fs.lstatSync(agentRoot);
263
- if (stat.isSymbolicLink() || !stat.isDirectory())
264
- throw new Error("agent root must be a real directory");
378
+ if (!inspectStandaloneSanctuaryDirectory(agentRoot))
379
+ throw new Error("agent root must be a canonical real directory");
380
+ }
381
+ function inspectionError(code) {
382
+ if (code === "invalid_package_root" || code === "invalid_package_source" || code === "packaged_policy_not_empty" || code === "package_version_mismatch") {
383
+ return { ok: false, error: { code, message: "verified release contents are invalid", degraded: true, repair: { actor: "human-required", action: "roll_back_or_install_verified_release" } } };
384
+ }
385
+ if (code === "invalid_live_root" || code === "invalid_live_bundle") {
386
+ return { ok: false, error: { code, message: "installed Sanctuary bundle is invalid", degraded: true, repair: { actor: "human-required", action: "roll_back_or_install_verified_release" } } };
387
+ }
388
+ if (code === "invalid_journal") {
389
+ return { ok: false, error: { code, message: "Sanctuary update recovery is required", degraded: true, repair: { actor: "human-required", action: "run_verified_update_recovery" } } };
390
+ }
391
+ return { ok: false, error: { code, message: "Sanctuary install state is unavailable", degraded: true, repair: { actor: "human-required", action: "run_verified_update_recovery" } } };
392
+ }
393
+ function validateInspectionRoot(root, code) {
394
+ const identity = inspectStandaloneSanctuaryDirectory(root);
395
+ if (!identity)
396
+ throw new SanctuaryInspectionFault(code);
397
+ return identity;
398
+ }
399
+ function inspectRelativeFile(root, relative, code) {
400
+ let current = root;
401
+ let result = null;
402
+ const segments = relative.split(path.sep);
403
+ for (let index = 0; index < segments.length; index += 1) {
404
+ current = path.join(current, segments[index]);
405
+ let stat;
406
+ try {
407
+ stat = fs.lstatSync(current);
408
+ }
409
+ catch (error) {
410
+ if (isMissing(error))
411
+ return null;
412
+ throw error;
413
+ }
414
+ if (stat.isSymbolicLink())
415
+ throw new SanctuaryInspectionFault(code);
416
+ if (index < segments.length - 1 && !stat.isDirectory())
417
+ throw new SanctuaryInspectionFault(code);
418
+ if (index === segments.length - 1 && !stat.isFile())
419
+ throw new SanctuaryInspectionFault(code);
420
+ if (index === segments.length - 1)
421
+ result = stat;
422
+ }
423
+ return result;
424
+ }
425
+ function readInspectionObject(filePath, code) {
426
+ let value;
427
+ try {
428
+ value = JSON.parse(fs.readFileSync(filePath, "utf8"));
429
+ }
430
+ catch (error) {
431
+ if (error instanceof SyntaxError)
432
+ throw new SanctuaryInspectionFault(code);
433
+ throw error;
434
+ }
435
+ if (!value || typeof value !== "object" || Array.isArray(value))
436
+ throw new SanctuaryInspectionFault(code);
437
+ return value;
438
+ }
439
+ function inspectPackageSources(packageRoot, runtimePackageVersion) {
440
+ const managed = new Map();
441
+ for (const relative of exports.SANCTUARY_PACKAGE_MANAGED_FILES) {
442
+ if (!inspectRelativeFile(packageRoot, relative, "invalid_package_source"))
443
+ throw new SanctuaryInspectionFault("invalid_package_source");
444
+ managed.set(relative, fs.readFileSync(path.join(packageRoot, relative)));
445
+ }
446
+ if (!inspectRelativeFile(packageRoot, "bundle-meta.json", "invalid_package_source"))
447
+ throw new SanctuaryInspectionFault("invalid_package_source");
448
+ const metadata = readInspectionObject(path.join(packageRoot, "bundle-meta.json"), "invalid_package_source");
449
+ if (typeof metadata.runtimeVersion !== "string" || metadata.runtimeVersion.length === 0 || !Number.isInteger(metadata.bundleSchemaVersion) || typeof metadata.lastUpdated !== "string" || metadata.lastUpdated.length === 0)
450
+ throw new SanctuaryInspectionFault("invalid_package_source");
451
+ if (!inspectRelativeFile(packageRoot, "state/policy/steward.json", "invalid_package_source"))
452
+ throw new SanctuaryInspectionFault("invalid_package_source");
453
+ const policy = readInspectionObject(path.join(packageRoot, "state/policy/steward.json"), "invalid_package_source");
454
+ if (!isCanonicalEmptyPackagedPolicy(policy))
455
+ throw new SanctuaryInspectionFault("packaged_policy_not_empty");
456
+ if (runtimePackageVersion.length === 0 || metadata.runtimeVersion !== runtimePackageVersion)
457
+ throw new SanctuaryInspectionFault("package_version_mismatch");
458
+ return { managed, metadata, packagedBundleVersion: metadata.runtimeVersion };
459
+ }
460
+ function journalRepair(journalState, parity) {
461
+ if (parity === "exact" && journalState !== "committing")
462
+ return { actor: "none", action: "none" };
463
+ if (parity === "mismatch" && journalState === "absent")
464
+ return { actor: "human-required", action: "restart_from_verified_release" };
465
+ return { actor: "human-required", action: "run_verified_update_recovery" };
466
+ }
467
+ function inspectSanctuaryPackageManagedBundle(input) {
468
+ try {
469
+ const packageIdentity = validateInspectionRoot(input.packageRoot, "invalid_package_root");
470
+ const packaged = inspectPackageSources(input.packageRoot, input.runtimePackageVersion);
471
+ const agentIdentity = validateInspectionRoot(input.agentRoot, "invalid_live_root");
472
+ if (sanctuaryDirectoriesShareIdentity(packageIdentity, agentIdentity))
473
+ throw new SanctuaryInspectionFault("invalid_live_root");
474
+ let managedMissing = false;
475
+ let managedContent = false;
476
+ let managedMode = false;
477
+ for (const relative of exports.SANCTUARY_PACKAGE_MANAGED_FILES) {
478
+ const stat = inspectRelativeFile(input.agentRoot, relative, "invalid_live_bundle");
479
+ if (!stat) {
480
+ managedMissing = true;
481
+ continue;
482
+ }
483
+ if (!fs.readFileSync(path.join(input.agentRoot, relative)).equals(packaged.managed.get(relative)))
484
+ managedContent = true;
485
+ if ((stat.mode & 0o777) !== 0o600)
486
+ managedMode = true;
487
+ }
488
+ let liveBundleVersion = null;
489
+ let bundleMetaMissing = false;
490
+ let bundleMetaField = false;
491
+ let bundleMetaMode = false;
492
+ const liveMetaStat = inspectRelativeFile(input.agentRoot, "bundle-meta.json", "invalid_live_bundle");
493
+ if (!liveMetaStat) {
494
+ bundleMetaMissing = true;
495
+ }
496
+ else {
497
+ const liveMeta = readInspectionObject(path.join(input.agentRoot, "bundle-meta.json"), "invalid_live_bundle");
498
+ liveBundleVersion = typeof liveMeta.runtimeVersion === "string" ? liveMeta.runtimeVersion : null;
499
+ bundleMetaField = BUNDLE_META_VERSION_FIELDS.some((field) => liveMeta[field] !== packaged.metadata[field]);
500
+ bundleMetaMode = (liveMetaStat.mode & 0o777) !== 0o600;
501
+ }
502
+ let journalState = "absent";
503
+ try {
504
+ if (inspectInterruptedWrites(packageManagedArtifactPaths(input.agentRoot)).length > 0)
505
+ throw new Error("Sanctuary bundle staging residue requires verified recovery");
506
+ journalState = readDurableRecord(input.agentRoot)?.state ?? "absent";
507
+ }
508
+ catch (error) {
509
+ if (isFilesystemFailure(error))
510
+ throw error;
511
+ throw new SanctuaryInspectionFault("invalid_journal");
512
+ }
513
+ const mismatchCodes = [];
514
+ if (managedMissing)
515
+ mismatchCodes.push("managed_file_missing");
516
+ if (managedContent)
517
+ mismatchCodes.push("managed_file_content");
518
+ if (managedMode)
519
+ mismatchCodes.push("managed_file_mode");
520
+ if (bundleMetaMissing)
521
+ mismatchCodes.push("bundle_meta_missing");
522
+ if (bundleMetaField)
523
+ mismatchCodes.push("bundle_meta_field");
524
+ if (bundleMetaMode)
525
+ mismatchCodes.push("bundle_meta_mode");
526
+ const parity = mismatchCodes.length === 0 ? "exact" : "mismatch";
527
+ const ready = parity === "exact" && journalState !== "committing";
528
+ return {
529
+ ok: true,
530
+ data: {
531
+ runtimePackageVersion: input.runtimePackageVersion,
532
+ packagedBundleVersion: packaged.packagedBundleVersion,
533
+ liveBundleVersion,
534
+ parity,
535
+ mismatchCodes,
536
+ journalState,
537
+ ready,
538
+ repair: journalRepair(journalState, parity),
539
+ },
540
+ };
541
+ }
542
+ catch (error) {
543
+ return inspectionError(error instanceof SanctuaryInspectionFault ? error.code : "inspection_unavailable");
544
+ }
545
+ }
546
+ function ensureSanctuaryPackageManagedBundle(input, deps = {}) {
547
+ const inspect = deps.inspect ?? inspectSanctuaryPackageManagedBundle;
548
+ const before = inspect(input);
549
+ if (!before.ok) {
550
+ if (before.error.code !== "invalid_journal")
551
+ return before;
552
+ try {
553
+ (deps.migrate ?? migrateSanctuaryPackageManagedBundle)({ packageRoot: input.packageRoot, agentRoot: input.agentRoot });
554
+ }
555
+ catch {
556
+ return before;
557
+ }
558
+ }
559
+ else if (before.data.ready) {
560
+ return before;
561
+ }
562
+ else if (before.data.parity === "mismatch" && before.data.journalState !== "absent") {
563
+ return before;
564
+ }
565
+ else if (before.data.parity === "exact" && before.data.journalState === "committing") {
566
+ (deps.commit ?? commitSanctuaryPackageManagedBundle)(input.agentRoot);
567
+ }
568
+ else {
569
+ (deps.migrate ?? migrateSanctuaryPackageManagedBundle)({ packageRoot: input.packageRoot, agentRoot: input.agentRoot });
570
+ }
571
+ const after = inspect(input);
572
+ if (!after.ok || !after.data.ready)
573
+ return after;
574
+ if (after.data.parity !== "exact" || after.data.journalState !== "absent")
575
+ throw new Error("Sanctuary package-managed bundle did not converge");
576
+ return after;
265
577
  }
266
578
  function inspectSanctuaryPackageManagedBundleRollback(agentRoot) {
267
579
  validateAgentRoot(agentRoot);
@@ -275,7 +587,7 @@ function rollbackSanctuaryPackageManagedBundle(agentRoot, options = {}) {
275
587
  return false;
276
588
  if (record.state === "committing")
277
589
  throw new Error("Sanctuary bundle commit is pending and cannot be rolled back");
278
- restoreMigrationSnapshot(record.snapshot);
590
+ restoreMigrationSnapshot(record.snapshot, agentRoot);
279
591
  if (!options.retainRecord)
280
592
  removeRollbackRecord(agentRoot, record.filePath);
281
593
  return true;
@@ -288,37 +600,45 @@ function commitSanctuaryPackageManagedBundle(agentRoot) {
288
600
  const committingPath = path.join(agentRoot, SANCTUARY_BUNDLE_COMMITTING_FILE);
289
601
  if (record.filePath !== committingPath) {
290
602
  fs.renameSync(record.filePath, committingPath);
291
- const root = fs.openSync(agentRoot, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
292
- try {
293
- fs.fsyncSync(root);
294
- }
295
- finally {
296
- fs.closeSync(root);
297
- }
603
+ syncDirectory(agentRoot);
298
604
  }
299
605
  removeRollbackRecord(agentRoot, committingPath);
300
606
  return true;
301
607
  }
302
- function restoreMigrationSnapshot(snapshot) {
608
+ function restoreMigrationSnapshot(snapshot, agentRoot) {
609
+ const restoredModes = new Map();
610
+ for (const file of snapshot.files)
611
+ if (file.mode !== null)
612
+ restoredModes.set(file.path, file.mode);
613
+ cleanupInterruptedWrites(snapshot.files.map((file) => file.path), restoredModes);
303
614
  for (const file of snapshot.files) {
304
- cleanupInterruptedWrites(file.path);
305
615
  if (file.content === null) {
306
616
  fs.rmSync(file.path, { force: true });
617
+ syncNearestExistingDirectory(path.dirname(file.path), agentRoot);
307
618
  }
308
619
  else {
309
- writeAtomic(file.path, file.content);
310
- fs.chmodSync(file.path, file.mode);
620
+ writeAtomic(file.path, file.content, file.mode);
311
621
  }
312
622
  }
313
623
  for (const directory of [...snapshot.directories].reverse()) {
314
624
  if (directory.mode === null) {
315
- if (!fs.existsSync(directory.path))
316
- continue;
317
- if (fs.readdirSync(directory.path).length === 0)
625
+ if (!fs.existsSync(directory.path)) {
626
+ syncNearestExistingDirectory(path.dirname(directory.path), agentRoot);
627
+ }
628
+ else if (fs.readdirSync(directory.path).length === 0) {
318
629
  fs.rmdirSync(directory.path);
630
+ syncNearestExistingDirectory(path.dirname(directory.path), agentRoot);
631
+ }
319
632
  }
320
633
  else {
321
- fs.chmodSync(directory.path, directory.mode);
634
+ const handle = fs.openSync(directory.path, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
635
+ try {
636
+ fs.fchmodSync(handle, directory.mode);
637
+ fs.fsyncSync(handle);
638
+ }
639
+ finally {
640
+ fs.closeSync(handle);
641
+ }
322
642
  }
323
643
  }
324
644
  }
@@ -326,14 +646,16 @@ function migrateSanctuaryPackageManagedBundle(input) {
326
646
  (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_bundle_migration_start", message: "starting Sanctuary package-managed bundle migration", meta: { agentRoot: input.agentRoot } });
327
647
  let snapshot = null;
328
648
  try {
329
- if (!path.isAbsolute(input.packageRoot) || !path.isAbsolute(input.agentRoot) || input.packageRoot === input.agentRoot)
330
- throw new Error("package and agent roots must be distinct absolute paths");
331
- const packageStat = fs.lstatSync(input.packageRoot);
332
- const agentStat = fs.lstatSync(input.agentRoot);
333
- if (packageStat.isSymbolicLink() || !packageStat.isDirectory())
334
- throw new Error("package root must be a real directory");
335
- if (agentStat.isSymbolicLink() || !agentStat.isDirectory())
336
- throw new Error("agent root must be a real directory");
649
+ if (!path.isAbsolute(input.packageRoot) || !path.isAbsolute(input.agentRoot))
650
+ throw new Error("package and agent roots must be absolute paths");
651
+ const packageIdentity = inspectStandaloneSanctuaryDirectory(input.packageRoot);
652
+ const agentIdentity = inspectStandaloneSanctuaryDirectory(input.agentRoot);
653
+ if (!packageIdentity)
654
+ throw new Error("package root must be a canonical real directory");
655
+ if (!agentIdentity)
656
+ throw new Error("agent root must be a canonical real directory");
657
+ if (sanctuaryDirectoriesShareIdentity(packageIdentity, agentIdentity))
658
+ throw new Error("package and agent roots must be distinct real directories");
337
659
  if (input.retainRollback && (!input.rollbackImageId || !/^sha256:[0-9a-f]{64}$/u.test(input.rollbackImageId) || !input.targetImageId || !/^sha256:[0-9a-f]{64}$/u.test(input.targetImageId) || input.targetImageId === input.rollbackImageId))
338
660
  throw new Error("retained rollback requires distinct exact rollback and target image IDs");
339
661
  const existingRollback = rollbackPath(input.agentRoot);
@@ -349,13 +671,10 @@ function migrateSanctuaryPackageManagedBundle(input) {
349
671
  for (const field of BUNDLE_META_VERSION_FIELDS)
350
672
  if (!(field in packagedMeta))
351
673
  throw new Error(`packaged bundle-meta.json is missing ${field}`);
352
- const packagedPolicy = (0, steward_policy_1.readStewardPolicy)(input.packageRoot);
353
- if (Object.keys(packagedPolicy.routineActionGrants).length > 0) {
354
- throw new Error("packaged steward policy must not carry routine action grants; authorize them through an authenticated owner session");
355
- }
674
+ validatePackagedPolicy(input.packageRoot);
675
+ cleanupInterruptedWrites(packageManagedArtifactPaths(input.agentRoot));
356
676
  snapshot = captureMigrationSnapshot(input.agentRoot);
357
677
  if (input.retainRollback) {
358
- cleanupInterruptedWrites(existingRollback);
359
678
  writeAtomic(existingRollback, `${JSON.stringify(durableSnapshot(input.agentRoot, snapshot, input.rollbackImageId, input.targetImageId))}\n`);
360
679
  }
361
680
  let managedFilesUpdated = 0;
@@ -369,13 +688,24 @@ function migrateSanctuaryPackageManagedBundle(input) {
369
688
  managedFilesUpdated += 1;
370
689
  }
371
690
  const metaPath = path.join(input.agentRoot, "bundle-meta.json");
372
- const currentMeta = fs.existsSync(metaPath) ? readObject(metaPath) : {};
691
+ const metaExists = fs.existsSync(metaPath);
692
+ const currentMeta = metaExists ? readObject(metaPath) : {};
373
693
  const nextMeta = { ...currentMeta };
374
694
  for (const field of BUNDLE_META_VERSION_FIELDS)
375
695
  nextMeta[field] = packagedMeta[field];
376
- const nextMetaText = `${JSON.stringify(nextMeta, null, 2)}\n`;
377
- if (!fs.existsSync(metaPath) || fs.readFileSync(metaPath, "utf8") !== nextMetaText)
378
- writeAtomic(metaPath, nextMetaText);
696
+ if (!metaExists || BUNDLE_META_VERSION_FIELDS.some((field) => currentMeta[field] !== packagedMeta[field])) {
697
+ writeAtomic(metaPath, `${JSON.stringify(nextMeta, null, 2)}\n`);
698
+ }
699
+ else if ((fs.statSync(metaPath).mode & 0o777) !== 0o600) {
700
+ fs.chmodSync(metaPath, 0o600);
701
+ const meta = fs.openSync(metaPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
702
+ try {
703
+ fs.fsyncSync(meta);
704
+ }
705
+ finally {
706
+ fs.closeSync(meta);
707
+ }
708
+ }
379
709
  const result = { managedFilesUpdated };
380
710
  (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_bundle_migration_end", message: "completed Sanctuary package-managed bundle migration", meta: result });
381
711
  return result;
@@ -384,7 +714,7 @@ function migrateSanctuaryPackageManagedBundle(input) {
384
714
  let failure = error;
385
715
  if (snapshot) {
386
716
  try {
387
- restoreMigrationSnapshot(snapshot);
717
+ restoreMigrationSnapshot(snapshot, input.agentRoot);
388
718
  if (fs.existsSync(rollbackPath(input.agentRoot)))
389
719
  removeRollbackRecord(input.agentRoot, rollbackPath(input.agentRoot));
390
720
  }