@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.
- package/changelog.json +21 -0
- package/deploy/unraid/Dockerfile +2 -2
- package/deploy/unraid/README.txt +693 -215
- package/deploy/unraid/docker-man-template-transaction.mjs +526 -0
- package/deploy/unraid/docker-man-template-xml.cjs +105 -0
- package/deploy/unraid/migrate-sanctuary-bundle.mjs +56 -17
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +4 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
- package/deploy/unraid/sanctuary.xml +2 -2
- package/dist/heart/daemon/container-healthcheck.js +16 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +23 -13
- package/dist/heart/daemon/container-spec-auditor.js +93 -33
- package/dist/heart/daemon/daemon-bootstrap-startup.js +38 -8
- package/dist/heart/daemon/daemon-entry.js +12 -1
- package/dist/heart/daemon/sanctuary-bundle-migration.js +411 -81
- package/dist/heart/daemon/sanctuary-package-management.js +103 -0
- package/dist/mind/prompt.js +1 -1
- package/dist/repertoire/tools-unraid.js +2 -1
- package/dist/senses/sanctuary-media-catalog-contract.js +3 -0
- package/dist/senses/sanctuary-runtime.js +7 -0
- package/dist/senses/telegram-effect-adapter.js +7 -6
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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.
|
|
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)
|
|
330
|
-
throw new Error("package and agent roots must be
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
334
|
-
throw new Error("package root must be a real directory");
|
|
335
|
-
if (
|
|
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
|
-
|
|
353
|
-
|
|
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
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
}
|