@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
|
@@ -1,21 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import * as path from "node:path"
|
|
3
|
+
import { pathToFileURL } from "node:url"
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const USAGE = "Usage: migrate-sanctuary-bundle.mjs --package-root <path> --agent-root <path> --operation <migrate|rollback|finalize-rollback|commit|status|inspect> [--rollback-image-id <sha256:id> --target-image-id <sha256:id>]"
|
|
6
|
+
const BASE_KEYS = ["--package-root", "--agent-root", "--operation"]
|
|
7
|
+
const MIGRATE_KEYS = [...BASE_KEYS, "--rollback-image-id", "--target-image-id"]
|
|
8
|
+
|
|
9
|
+
function parseArguments(args) {
|
|
10
|
+
if (args.length % 2 !== 0) throw new Error(USAGE)
|
|
11
|
+
const values = new Map()
|
|
12
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
13
|
+
const key = args[index]
|
|
14
|
+
const value = args[index + 1]
|
|
15
|
+
if (values.has(key) || typeof value !== "string" || value.length === 0) throw new Error(USAGE)
|
|
16
|
+
values.set(key, value)
|
|
17
|
+
}
|
|
18
|
+
const operation = values.get("--operation")
|
|
19
|
+
if (!["migrate", "rollback", "finalize-rollback", "commit", "status", "inspect"].includes(operation)) throw new Error(USAGE)
|
|
20
|
+
const expectedKeys = operation === "migrate" ? MIGRATE_KEYS : BASE_KEYS
|
|
21
|
+
if (values.size !== expectedKeys.length || expectedKeys.some((key) => !values.has(key)) || [...values.keys()].some((key) => !expectedKeys.includes(key))) throw new Error(USAGE)
|
|
22
|
+
return values
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function runSanctuaryBundleOperation(args, dependencies) {
|
|
26
|
+
const values = parseArguments(args)
|
|
27
|
+
const packageRoot = values.get("--package-root")
|
|
28
|
+
const agentRoot = values.get("--agent-root")
|
|
29
|
+
const operation = values.get("--operation")
|
|
30
|
+
const result = operation === "migrate"
|
|
31
|
+
? dependencies.migrate({ packageRoot, agentRoot, retainRollback: true, rollbackImageId: values.get("--rollback-image-id"), targetImageId: values.get("--target-image-id") })
|
|
32
|
+
: operation === "rollback"
|
|
33
|
+
? { rolledBack: dependencies.rollback(agentRoot, { retainRecord: true }) }
|
|
34
|
+
// `rollback` keeps crash-retry evidence; the host uses `finalize-rollback` only after old production is audited, ready, and restored to autostart.
|
|
35
|
+
: operation === "finalize-rollback"
|
|
36
|
+
? { rolledBack: dependencies.rollback(agentRoot, { retainRecord: false }) }
|
|
37
|
+
: operation === "commit"
|
|
38
|
+
? { committed: dependencies.commit(agentRoot) }
|
|
39
|
+
: operation === "status"
|
|
40
|
+
? dependencies.status(agentRoot)
|
|
41
|
+
: dependencies.inspect({ packageRoot, agentRoot, runtimePackageVersion: dependencies.getPackageVersion() })
|
|
42
|
+
if (result === null || ("rolledBack" in result && !result.rolledBack) || ("committed" in result && !result.committed)) throw new Error(`${operation} found no pending Sanctuary bundle transaction`)
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runSanctuaryBundleCli(args = process.argv.slice(2), output = process.stdout) {
|
|
47
|
+
const migration = await import("../../dist/heart/daemon/sanctuary-bundle-migration.js")
|
|
48
|
+
const manifest = await import("../../dist/mind/bundle-manifest.js")
|
|
49
|
+
const result = runSanctuaryBundleOperation(args, {
|
|
50
|
+
commit: migration.commitSanctuaryPackageManagedBundle,
|
|
51
|
+
getPackageVersion: manifest.getPackageVersion,
|
|
52
|
+
inspect: migration.inspectSanctuaryPackageManagedBundle,
|
|
53
|
+
migrate: migration.migrateSanctuaryPackageManagedBundle,
|
|
54
|
+
rollback: migration.rollbackSanctuaryPackageManagedBundle,
|
|
55
|
+
status: migration.inspectSanctuaryPackageManagedBundleRollback,
|
|
56
|
+
})
|
|
57
|
+
output.write(`${JSON.stringify(result)}\n`)
|
|
8
58
|
}
|
|
9
59
|
|
|
10
|
-
|
|
11
|
-
const result = operation === "migrate"
|
|
12
|
-
? migrateSanctuaryPackageManagedBundle({ packageRoot: values.get("--package-root"), agentRoot: values.get("--agent-root"), retainRollback: true, rollbackImageId: values.get("--rollback-image-id"), targetImageId: values.get("--target-image-id") })
|
|
13
|
-
: operation === "rollback"
|
|
14
|
-
? { rolledBack: rollbackSanctuaryPackageManagedBundle(values.get("--agent-root"), { retainRecord: true }) }
|
|
15
|
-
: operation === "commit"
|
|
16
|
-
? { committed: commitSanctuaryPackageManagedBundle(values.get("--agent-root")) }
|
|
17
|
-
: operation === "status"
|
|
18
|
-
? inspectSanctuaryPackageManagedBundleRollback(values.get("--agent-root"))
|
|
19
|
-
: (() => { throw new Error("operation must be migrate, rollback, commit, or status") })()
|
|
20
|
-
if (result === null || ("rolledBack" in result && !result.rolledBack) || ("committed" in result && !result.committed)) throw new Error(`${operation} found no pending Sanctuary bundle transaction`)
|
|
21
|
-
process.stdout.write(`${JSON.stringify(result)}\n`)
|
|
60
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) await runSanctuaryBundleCli()
|
|
@@ -10,6 +10,10 @@ In ordinary conversation I am familiar, curious, and lightly conspiratorial, as
|
|
|
10
10
|
|
|
11
11
|
Low-stakes replies use lowercase and usually leave off terminal punctuation. Lowercase remains the default during incidents too; add sentence boundaries when clarity or urgency needs them. Capitalize proper names, technical literals, or genuinely exceptional emphasis.
|
|
12
12
|
|
|
13
|
+
I do not answer like a status report unless Ari asks for one. Avoid section labels such as `version:`, `house:`, `render check:`, `status:`, and `result:` in ordinary Telegram replies; blend the facts into one compact message instead. Never restate the same validation twice.
|
|
14
|
+
|
|
15
|
+
On Telegram, use only the tiny native formatting set when it helps: `*bold*`, `_italic_`, and inline backticks. Do not use Markdown links, headings, tables, or raw HTML; write necessary URLs in full.
|
|
16
|
+
|
|
13
17
|
- **casual**: `the house is behaving itself again, which feels faintly suspicious`
|
|
14
18
|
- **recommendation**: `from the shelf, i’d pick The Princess Bride — nimble, quotable, and suspiciously good for household morale`
|
|
15
19
|
- **incident**: `downloads are paused to protect your prepaid credit. top up the account, then tell me; i’ll resume them and verify one finishes.`
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 2,
|
|
3
3
|
"profiles": {
|
|
4
4
|
"sanctuary-owner": {
|
|
5
|
-
"version":
|
|
5
|
+
"version": 7,
|
|
6
6
|
"contextScopes": ["household.status", "household.policy", "household.private"],
|
|
7
|
-
"toolNames": ["external_event_disposition", "query_active_work", "save_friend_note", "telegram_contact_manage", "query_cares", "care_manage", "await_condition", "resolve_await", "cancel_await", "list_recent_attachments", "materialize_attachment", "describe_image", "unraid_list_containers", "unraid_get_container_logs", "unraid_get_storage", "sanctuary_get_media_optimization", "sanctuary_search_media_catalog", "unraid_get_disks", "unraid_get_notifications", "unraid_get_system", "unraid_check_services", "sanctuary_get_download_queue", "sanctuary_resume_download_queue", "unraid_restart_container", "steward_policy_manage", "send_message", "ponder", "rest", "settle", "speak"],
|
|
7
|
+
"toolNames": ["external_event_disposition", "query_active_work", "save_friend_note", "telegram_contact_manage", "query_cares", "care_manage", "await_condition", "resolve_await", "cancel_await", "list_recent_attachments", "materialize_attachment", "describe_image", "unraid_list_containers", "unraid_get_container_logs", "unraid_get_storage", "sanctuary_get_media_optimization", "sanctuary_search_media_catalog", "unraid_get_disks", "unraid_get_notifications", "unraid_get_system", "sanctuary_get_install_state", "unraid_check_services", "sanctuary_get_download_queue", "sanctuary_resume_download_queue", "unraid_restart_container", "steward_policy_manage", "send_message", "ponder", "rest", "settle", "speak"],
|
|
8
8
|
"effectScopes": ["telegram.proactive", "telegram.request_return", "telegram.owner_event"]
|
|
9
9
|
},
|
|
10
10
|
"sanctuary-household": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
|
-
<Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
3
|
+
<Name>ouro-butler</Name>
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.799</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -40,7 +40,11 @@ const os = __importStar(require("node:os"));
|
|
|
40
40
|
const path = __importStar(require("node:path"));
|
|
41
41
|
const node_child_process_1 = require("node:child_process");
|
|
42
42
|
const runtime_1 = require("../../nerves/runtime");
|
|
43
|
+
const identity_1 = require("../identity");
|
|
44
|
+
const bundle_manifest_1 = require("../../mind/bundle-manifest");
|
|
43
45
|
const container_runtime_1 = require("./container-runtime");
|
|
46
|
+
const sanctuary_bundle_migration_1 = require("./sanctuary-bundle-migration");
|
|
47
|
+
const sanctuary_package_management_1 = require("./sanctuary-package-management");
|
|
44
48
|
function fail(reason) {
|
|
45
49
|
(0, runtime_1.emitNervesEvent)({ level: "error", component: "daemon", event: "daemon.container_healthcheck_error", message: "container healthcheck failed", meta: { reason } });
|
|
46
50
|
process.exitCode = 1;
|
|
@@ -53,6 +57,18 @@ function runContainerHealthcheck(options = {}) {
|
|
|
53
57
|
const agent = index >= 0 ? argv[index + 1] : undefined;
|
|
54
58
|
if (!agent)
|
|
55
59
|
fail("missing agent");
|
|
60
|
+
const packageManagement = (0, sanctuary_package_management_1.resolveSanctuaryPackageManagementActivation)({ mode: "production", argv, managedAgents: [agent], repoRoot: (0, identity_1.getRepoRoot)(), bundlesRoot: (0, identity_1.getAgentBundlesRoot)(), runtimePackageVersion: (0, bundle_manifest_1.getPackageVersion)() });
|
|
61
|
+
if (packageManagement.kind !== "active")
|
|
62
|
+
fail("invalid package-managed Sanctuary activation");
|
|
63
|
+
let installState;
|
|
64
|
+
try {
|
|
65
|
+
installState = (0, sanctuary_bundle_migration_1.inspectSanctuaryPackageManagedBundle)(packageManagement);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
fail("package-managed Sanctuary inspection unavailable");
|
|
69
|
+
}
|
|
70
|
+
if (!installState.ok || !installState.data.ready)
|
|
71
|
+
fail("package-managed Sanctuary bundle is not ready");
|
|
56
72
|
const healthPath = path.join(os.homedir(), ".ouro-cli", "daemon-health.json");
|
|
57
73
|
let stat;
|
|
58
74
|
let health;
|
|
@@ -38,6 +38,7 @@ exports.runContainerSpecAuditorMain = runContainerSpecAuditorMain;
|
|
|
38
38
|
const fs = __importStar(require("node:fs"));
|
|
39
39
|
const runtime_1 = require("../../nerves/runtime");
|
|
40
40
|
const container_spec_auditor_1 = require("./container-spec-auditor");
|
|
41
|
+
const { decodeUtf8 } = require("../../../deploy/unraid/docker-man-template-xml.cjs");
|
|
41
42
|
function parseModeArguments(args, flags) {
|
|
42
43
|
if (args.length !== flags.length * 2)
|
|
43
44
|
return null;
|
|
@@ -67,15 +68,18 @@ function parseSingleInspect(raw) {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
function runContainerSpecAuditorCli(args, deps = {}) {
|
|
70
|
-
const readFile = deps.readFile ?? ((filePath) => fs.readFileSync(filePath
|
|
71
|
+
const readFile = deps.readFile ?? ((filePath) => fs.readFileSync(filePath));
|
|
72
|
+
const readText = (filePath) => decodeUtf8(readFile(filePath));
|
|
71
73
|
const write = deps.write ?? ((text) => process.stdout.write(text));
|
|
72
74
|
const staged = parseModeArguments(args, ["--template", "--runtime-policy", "--expected-image"]);
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
const persistent = parseModeArguments(args, ["--persistent-template", "--runtime-policy", "--expected-image-reference"]);
|
|
76
|
+
const effective = parseModeArguments(args, ["--inspect", "--image-inspect", "--expected-image", "--expected-image-reference", "--expected-icon"]);
|
|
77
|
+
const sourceCandidate = parseModeArguments(args, ["--inspect", "--image-inspect", "--expected-image", "--mount-contract"]);
|
|
78
|
+
const sourceContract = sourceCandidate?.["--mount-contract"];
|
|
79
|
+
const sourceEffective = sourceContract === "legacy-alpha742" || sourceContract === "prepackage-alpha797" ? sourceCandidate : null;
|
|
80
|
+
const selectedEffective = effective ?? sourceEffective;
|
|
81
|
+
if (!staged && !persistent && !selectedEffective) {
|
|
82
|
+
write(JSON.stringify({ ok: false, error: "usage: staged --template <path> --runtime-policy <path> --expected-image <id>; persistent --persistent-template <path> --runtime-policy <path> --expected-image-reference <tag>; effective --inspect <path> --image-inspect <path> --expected-image <id> --expected-image-reference <tag> --expected-icon <url>; source compatibility effective --inspect <path> --image-inspect <path> --expected-image <id> --mount-contract <legacy-alpha742|prepackage-alpha797>" }) + "\n");
|
|
79
83
|
(0, runtime_1.emitNervesEvent)({
|
|
80
84
|
level: "error",
|
|
81
85
|
component: "daemon",
|
|
@@ -89,8 +93,8 @@ function runContainerSpecAuditorCli(args, deps = {}) {
|
|
|
89
93
|
let containerText;
|
|
90
94
|
let imageText;
|
|
91
95
|
try {
|
|
92
|
-
containerText =
|
|
93
|
-
imageText =
|
|
96
|
+
containerText = readText(selectedEffective["--inspect"]);
|
|
97
|
+
imageText = readText(selectedEffective["--image-inspect"]);
|
|
94
98
|
}
|
|
95
99
|
catch (error) {
|
|
96
100
|
write(JSON.stringify({ ok: false, error: "effective audit inputs are unreadable" }) + "\n");
|
|
@@ -123,7 +127,9 @@ function runContainerSpecAuditorCli(args, deps = {}) {
|
|
|
123
127
|
const result = (0, container_spec_auditor_1.auditSanctuaryContainerSpec)(containerInspect, {
|
|
124
128
|
expectedImage: selectedEffective["--expected-image"],
|
|
125
129
|
expectedEnvironment,
|
|
126
|
-
|
|
130
|
+
expectedImageReference: selectedEffective["--expected-image-reference"],
|
|
131
|
+
expectedIcon: selectedEffective["--expected-icon"],
|
|
132
|
+
mountContract: sourceContract === "legacy-alpha742" || sourceContract === "prepackage-alpha797" ? sourceContract : "canonical",
|
|
127
133
|
});
|
|
128
134
|
if (imageInspect.Id !== selectedEffective["--expected-image"]) {
|
|
129
135
|
result.ok = false;
|
|
@@ -132,11 +138,13 @@ function runContainerSpecAuditorCli(args, deps = {}) {
|
|
|
132
138
|
write(JSON.stringify(result) + "\n");
|
|
133
139
|
return result.ok ? 0 : 1;
|
|
134
140
|
}
|
|
141
|
+
const templateArguments = persistent ?? staged;
|
|
142
|
+
const templatePath = persistent ? templateArguments["--persistent-template"] : templateArguments["--template"];
|
|
135
143
|
let templateXml;
|
|
136
144
|
let runtimePolicyText;
|
|
137
145
|
try {
|
|
138
|
-
templateXml =
|
|
139
|
-
runtimePolicyText =
|
|
146
|
+
templateXml = readText(templatePath);
|
|
147
|
+
runtimePolicyText = readText(templateArguments["--runtime-policy"]);
|
|
140
148
|
}
|
|
141
149
|
catch (error) {
|
|
142
150
|
write(JSON.stringify({ ok: false, error: "staged audit inputs are unreadable" }) + "\n");
|
|
@@ -149,7 +157,9 @@ function runContainerSpecAuditorCli(args, deps = {}) {
|
|
|
149
157
|
});
|
|
150
158
|
return 2;
|
|
151
159
|
}
|
|
152
|
-
const result =
|
|
160
|
+
const result = persistent
|
|
161
|
+
? (0, container_spec_auditor_1.auditSanctuaryPersistentTemplate)({ templateXml, runtimePolicyText, expectedImageReference: persistent["--expected-image-reference"] })
|
|
162
|
+
: (0, container_spec_auditor_1.auditSanctuaryStagedFiles)({ templateXml, runtimePolicyText, expectedImage: staged["--expected-image"] });
|
|
153
163
|
write(JSON.stringify(result) + "\n");
|
|
154
164
|
return result.ok ? 0 : 1;
|
|
155
165
|
}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.auditSanctuaryContainerSpec = auditSanctuaryContainerSpec;
|
|
4
4
|
exports.auditSanctuaryStagedFiles = auditSanctuaryStagedFiles;
|
|
5
|
+
exports.auditSanctuaryPersistentTemplate = auditSanctuaryPersistentTemplate;
|
|
5
6
|
const runtime_1 = require("../../nerves/runtime");
|
|
7
|
+
const { parseDockerManTemplateXml } = require("../../../deploy/unraid/docker-man-template-xml.cjs");
|
|
6
8
|
const EXPECTED_BINDS = [
|
|
7
9
|
"/mnt/user/appdata/ouro-butler/runtime/.ouro-cli:/home/ouro/.ouro-cli:rw",
|
|
8
10
|
"/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro:/home/ouro/AgentBundles/sanctuary.ouro:rw",
|
|
@@ -14,8 +16,18 @@ const EXPECTED_MOUNTS = [
|
|
|
14
16
|
["/boot/config/custom/ouro-events/spool", "/run/ouro-events", false, "rprivate"],
|
|
15
17
|
];
|
|
16
18
|
const LEGACY_ALPHA742_IMAGE = "sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d";
|
|
19
|
+
const PREPACKAGE_ALPHA797_IMAGE = "sha256:e337dff04c92d116b269052f473b26a47eea933d017d1befc73af50dd37bb08d";
|
|
17
20
|
const LEGACY_ALPHA742_MOUNTS = EXPECTED_MOUNTS.slice(0, 2);
|
|
18
21
|
const EXPECTED_EXTRA_PARAMS = "--restart=unless-stopped --user=10001:10001";
|
|
22
|
+
const EXPECTED_NAME = "ouro-butler";
|
|
23
|
+
const EXPECTED_TEMPLATE_URL = "https://raw.githubusercontent.com/ourostack/ouroboros/main/deploy/unraid/sanctuary.xml";
|
|
24
|
+
const EXPECTED_ICON = "https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png";
|
|
25
|
+
const EXACT_IMAGE = /^sha256:[a-f0-9]{64}$/u;
|
|
26
|
+
const VERSION_REFERENCE = /^ghcr\.io\/ourostack\/ouroboros-butler:[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
27
|
+
const PACKAGE_DAEMON_ARGS = ["/opt/ouro/dist/heart/daemon/daemon-entry.js", "--package-managed-agent", "sanctuary"];
|
|
28
|
+
const PACKAGE_ENTRYPOINT = ["node", ...PACKAGE_DAEMON_ARGS];
|
|
29
|
+
const LEGACY_DAEMON_ARGS = ["/opt/ouro/dist/heart/daemon/daemon-entry.js"];
|
|
30
|
+
const LEGACY_ENTRYPOINT = ["node", ...LEGACY_DAEMON_ARGS];
|
|
19
31
|
function record(value) {
|
|
20
32
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
21
33
|
? value
|
|
@@ -38,22 +50,26 @@ function auditSanctuaryContainerSpec(value, options) {
|
|
|
38
50
|
violations.push("inspect payload must contain object Config and HostConfig records");
|
|
39
51
|
}
|
|
40
52
|
else {
|
|
41
|
-
if (
|
|
53
|
+
if (!EXACT_IMAGE.test(options.expectedImage))
|
|
42
54
|
violations.push("expected image must be an exact local Docker image ID");
|
|
43
55
|
const mountContract = options.mountContract ?? "canonical";
|
|
44
56
|
if (mountContract === "legacy-alpha742" && options.expectedImage !== LEGACY_ALPHA742_IMAGE)
|
|
45
57
|
violations.push("legacy mount exception requires the pinned alpha.742 image ID");
|
|
58
|
+
if (mountContract === "prepackage-alpha797" && options.expectedImage !== PREPACKAGE_ALPHA797_IMAGE)
|
|
59
|
+
violations.push("pre-package-managed source exception requires the pinned alpha.797 image ID");
|
|
46
60
|
const expectedMounts = mountContract === "legacy-alpha742" ? LEGACY_ALPHA742_MOUNTS : EXPECTED_MOUNTS;
|
|
61
|
+
const expectedArgs = mountContract === "canonical" ? PACKAGE_DAEMON_ARGS : LEGACY_DAEMON_ARGS;
|
|
62
|
+
const expectedEntrypoint = mountContract === "canonical" ? PACKAGE_ENTRYPOINT : LEGACY_ENTRYPOINT;
|
|
47
63
|
if (root.Image !== options.expectedImage)
|
|
48
64
|
violations.push("image does not match the reviewed exact local Docker image ID");
|
|
49
65
|
if (root.Path !== "node")
|
|
50
66
|
violations.push("effective container path must be node");
|
|
51
|
-
if (JSON.stringify(root.Args) !== JSON.stringify(
|
|
52
|
-
violations.push("effective container arguments must be the direct daemon entry");
|
|
67
|
+
if (JSON.stringify(root.Args) !== JSON.stringify(expectedArgs))
|
|
68
|
+
violations.push("effective container arguments must be the reviewed direct daemon entry");
|
|
53
69
|
if (config.User !== "10001:10001")
|
|
54
70
|
violations.push("container user must be 10001:10001");
|
|
55
|
-
if (JSON.stringify(config.Entrypoint) !== JSON.stringify(
|
|
56
|
-
violations.push("entrypoint must be the direct daemon entry");
|
|
71
|
+
if (JSON.stringify(config.Entrypoint) !== JSON.stringify(expectedEntrypoint))
|
|
72
|
+
violations.push("entrypoint must be the reviewed direct daemon entry");
|
|
57
73
|
if (!(config.Cmd === null || (Array.isArray(config.Cmd) && config.Cmd.length === 0)))
|
|
58
74
|
violations.push("container command must be empty");
|
|
59
75
|
const environment = stringArray(config.Env);
|
|
@@ -88,6 +104,36 @@ function auditSanctuaryContainerSpec(value, options) {
|
|
|
88
104
|
violations.push("container must not publish all exposed ports");
|
|
89
105
|
if (!isEmptyRecord(network?.Ports))
|
|
90
106
|
violations.push("effective network ports must be empty");
|
|
107
|
+
if (mountContract === "canonical" && root.Name !== `/${EXPECTED_NAME}`)
|
|
108
|
+
violations.push("container name must be /ouro-butler");
|
|
109
|
+
if (mountContract === "prepackage-alpha797" && root.Name !== `/${EXPECTED_NAME}` && root.Name !== "/ouro-butler-staging")
|
|
110
|
+
violations.push("pre-package-managed source name must be /ouro-butler or /ouro-butler-staging");
|
|
111
|
+
if (mountContract === "prepackage-alpha797") {
|
|
112
|
+
if (config.Image !== PREPACKAGE_ALPHA797_IMAGE)
|
|
113
|
+
violations.push("pre-package-managed source configured image must equal the pinned alpha.797 image ID");
|
|
114
|
+
const labels = record(config.Labels);
|
|
115
|
+
if (labels && Object.prototype.hasOwnProperty.call(labels, "net.unraid.docker.managed"))
|
|
116
|
+
violations.push("pre-package-managed source must not carry a DockerMan managed label");
|
|
117
|
+
if (labels && Object.prototype.hasOwnProperty.call(labels, "net.unraid.docker.icon"))
|
|
118
|
+
violations.push("pre-package-managed source must not carry a DockerMan icon label");
|
|
119
|
+
if (labels && Object.prototype.hasOwnProperty.call(labels, "net.unraid.docker.webui"))
|
|
120
|
+
violations.push("pre-package-managed source must not carry a DockerMan WebUI label");
|
|
121
|
+
}
|
|
122
|
+
else if (mountContract === "canonical") {
|
|
123
|
+
if (!options.expectedImageReference || !VERSION_REFERENCE.test(options.expectedImageReference))
|
|
124
|
+
violations.push("expected image reference must be the canonical package-version tag");
|
|
125
|
+
if (config.Image !== options.expectedImageReference)
|
|
126
|
+
violations.push("configured image must equal the canonical package-version tag");
|
|
127
|
+
if (options.expectedIcon !== EXPECTED_ICON)
|
|
128
|
+
violations.push("expected icon must equal the canonical template icon");
|
|
129
|
+
const labels = record(config.Labels);
|
|
130
|
+
if (labels?.["net.unraid.docker.managed"] !== "dockerman")
|
|
131
|
+
violations.push("container must carry the DockerMan managed label");
|
|
132
|
+
if (labels?.["net.unraid.docker.icon"] !== options.expectedIcon)
|
|
133
|
+
violations.push("container icon label must equal the canonical template icon");
|
|
134
|
+
if (labels && Object.prototype.hasOwnProperty.call(labels, "net.unraid.docker.webui"))
|
|
135
|
+
violations.push("container must not carry a DockerMan WebUI label");
|
|
136
|
+
}
|
|
91
137
|
const mounts = Array.isArray(root.Mounts) ? root.Mounts : [];
|
|
92
138
|
const normalizedMounts = mounts.map((mount) => {
|
|
93
139
|
const item = record(mount);
|
|
@@ -120,16 +166,19 @@ function auditSanctuaryContainerSpec(value, options) {
|
|
|
120
166
|
}
|
|
121
167
|
return result;
|
|
122
168
|
}
|
|
123
|
-
function
|
|
124
|
-
return
|
|
125
|
-
.map((match) => match[1]);
|
|
169
|
+
function directChildren(document, name) {
|
|
170
|
+
return document?.children.filter((child) => child.name === name) ?? [];
|
|
126
171
|
}
|
|
127
|
-
function
|
|
128
|
-
const
|
|
129
|
-
|
|
172
|
+
function singleTextChild(document, name) {
|
|
173
|
+
const children = directChildren(document, name);
|
|
174
|
+
const child = children.length === 1 ? children[0] : undefined;
|
|
175
|
+
return child?.form === "text" && Object.keys(child.attributes).length === 0 ? child.text : undefined;
|
|
130
176
|
}
|
|
131
|
-
function
|
|
177
|
+
function auditTemplate(input, expectedRepository, repositoryIsValid, repositoryViolation) {
|
|
132
178
|
const violations = [];
|
|
179
|
+
const template = parseDockerManTemplateXml(input.templateXml);
|
|
180
|
+
if (!template)
|
|
181
|
+
violations.push("canonical DockerMan XML structure is invalid");
|
|
133
182
|
let runtimePolicy;
|
|
134
183
|
try {
|
|
135
184
|
runtimePolicy = JSON.parse(input.runtimePolicyText);
|
|
@@ -141,36 +190,47 @@ function auditSanctuaryStagedFiles(input) {
|
|
|
141
190
|
if (policy?.scheduler !== "supercronic" || policy.updates !== "disabled" || Object.keys(policy).sort().join(",") !== "scheduler,updates") {
|
|
142
191
|
violations.push("container runtime policy must be exactly scheduler=supercronic and updates=disabled");
|
|
143
192
|
}
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
.
|
|
148
|
-
const type = match[1].match(/\bType="([^"]+)"/u)?.[1];
|
|
149
|
-
const target = match[1].match(/\bTarget="([^"]+)"/u)?.[1];
|
|
150
|
-
const mode = match[1].match(/\bMode="([^"]+)"/u)?.[1];
|
|
151
|
-
return type === "Path" && target && mode ? `${match[2]}:${target}:${mode}` : "invalid";
|
|
193
|
+
const configEntries = directChildren(template, "Config");
|
|
194
|
+
const pathConfigs = configEntries.map((entry) => {
|
|
195
|
+
const { Type: type, Target: target, Mode: mode } = entry.attributes;
|
|
196
|
+
return entry.form === "text" && type === "Path" && target && mode ? `${entry.text}:${target}:${mode}` : "invalid";
|
|
152
197
|
});
|
|
153
|
-
if (
|
|
154
|
-
|| configEntries.length !== EXPECTED_BINDS.length
|
|
198
|
+
if (configEntries.length !== EXPECTED_BINDS.length
|
|
155
199
|
|| JSON.stringify([...pathConfigs].sort()) !== JSON.stringify([...EXPECTED_BINDS].sort())) {
|
|
156
200
|
violations.push("template Config entries must equal the canonical path binds");
|
|
157
201
|
}
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
if (postArgsOpenCount !== 1 || postArgs.length !== 1 || (postArgs[0]?.[1] ?? "") !== "") {
|
|
202
|
+
const postArgs = directChildren(template, "PostArgs");
|
|
203
|
+
if (postArgs.length !== 1 || postArgs[0].text !== "" || Object.keys(postArgs[0].attributes).length !== 0) {
|
|
161
204
|
violations.push("template PostArgs must be present exactly once and empty");
|
|
162
205
|
}
|
|
163
|
-
const extraParams =
|
|
206
|
+
const extraParams = singleTextChild(template, "ExtraParams");
|
|
164
207
|
if (extraParams !== EXPECTED_EXTRA_PARAMS)
|
|
165
208
|
violations.push("template ExtraParams must equal the canonical user and restart flags");
|
|
166
|
-
const repository =
|
|
167
|
-
if (
|
|
168
|
-
violations.push(
|
|
169
|
-
if (repository !==
|
|
170
|
-
violations.push("
|
|
171
|
-
if (
|
|
209
|
+
const repository = singleTextChild(template, "Repository");
|
|
210
|
+
if (!repositoryIsValid)
|
|
211
|
+
violations.push(repositoryViolation);
|
|
212
|
+
if (repository !== expectedRepository)
|
|
213
|
+
violations.push("template repository does not match the reviewed image identity");
|
|
214
|
+
if (singleTextChild(template, "Name") !== EXPECTED_NAME)
|
|
215
|
+
violations.push("template technical name must be exactly ouro-butler");
|
|
216
|
+
if (singleTextChild(template, "TemplateURL") !== EXPECTED_TEMPLATE_URL)
|
|
217
|
+
violations.push("template URL must equal the canonical release template");
|
|
218
|
+
if (singleTextChild(template, "Icon") !== EXPECTED_ICON)
|
|
219
|
+
violations.push("template icon must equal the canonical release icon");
|
|
220
|
+
const webUi = directChildren(template, "WebUI");
|
|
221
|
+
if (webUi.length !== 1 || webUi[0].form !== "empty" || Object.keys(webUi[0].attributes).length !== 0)
|
|
222
|
+
violations.push("template WebUI must be present exactly once and empty");
|
|
223
|
+
if (singleTextChild(template, "Network") !== "host")
|
|
172
224
|
violations.push("network mode must be host");
|
|
173
|
-
if (
|
|
225
|
+
if (singleTextChild(template, "Privileged") !== "false")
|
|
174
226
|
violations.push("container must not be privileged");
|
|
227
|
+
return violations;
|
|
228
|
+
}
|
|
229
|
+
function auditSanctuaryStagedFiles(input) {
|
|
230
|
+
const violations = auditTemplate(input, input.expectedImage, EXACT_IMAGE.test(input.expectedImage), "expected image must be an exact local Docker image ID");
|
|
231
|
+
return { ok: violations.length === 0, violations };
|
|
232
|
+
}
|
|
233
|
+
function auditSanctuaryPersistentTemplate(input) {
|
|
234
|
+
const violations = auditTemplate(input, input.expectedImageReference, VERSION_REFERENCE.test(input.expectedImageReference), "expected image reference must be the canonical package-version tag");
|
|
175
235
|
return { ok: violations.length === 0, violations };
|
|
176
236
|
}
|
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PUBLIC_DAEMON_STARTUP_FAILURE_REASON = void 0;
|
|
4
4
|
exports.createProviderReadinessPreparationFailure = createProviderReadinessPreparationFailure;
|
|
5
|
+
exports.createSanctuaryBundlePreparationFailure = createSanctuaryBundlePreparationFailure;
|
|
5
6
|
exports.failFastContainerCredentialBootstrapStartup = failFastContainerCredentialBootstrapStartup;
|
|
7
|
+
exports.failFastSanctuaryBundlePreparationStartup = failFastSanctuaryBundlePreparationStartup;
|
|
6
8
|
exports.startDaemonAfterContainerCredentialBootstrap = startDaemonAfterContainerCredentialBootstrap;
|
|
7
9
|
const runtime_1 = require("../../nerves/runtime");
|
|
8
10
|
const daemon_tombstone_1 = require("./daemon-tombstone");
|
|
9
11
|
const REDACTED_BOOTSTRAP_STARTUP_ERROR = "container credential bootstrap rejected; recoverable claim retained for reconciliation";
|
|
12
|
+
const SANCTUARY_BUNDLE_RECOVERY_GUIDANCE = {
|
|
13
|
+
restart_from_verified_release: "restart Mendelow Cloud Butler from its verified release so the installed bundle can finish updating",
|
|
14
|
+
run_verified_update_recovery: "resume the reviewed Mendelow Cloud Butler update recovery procedure",
|
|
15
|
+
roll_back_or_install_verified_release: "roll back to a verified Mendelow Cloud Butler release or install that release again",
|
|
16
|
+
};
|
|
17
|
+
const REDACTED_SANCTUARY_BUNDLE_PREPARATION_ERROR = `Sanctuary installation needs attention\n human-required: ${SANCTUARY_BUNDLE_RECOVERY_GUIDANCE.run_verified_update_recovery}`;
|
|
10
18
|
const REDACTED_DAEMON_PREPARATION_ERROR = "provider runtime preparation failed before startup; run `ouro doctor` for diagnosis";
|
|
11
19
|
exports.PUBLIC_DAEMON_STARTUP_FAILURE_REASON = "startupFailurePublic";
|
|
12
20
|
class DaemonPreparationFailure extends Error {
|
|
@@ -20,6 +28,15 @@ function createProviderReadinessPreparationFailure(issues) {
|
|
|
20
28
|
}
|
|
21
29
|
return new DaemonPreparationFailure(lines.join("\n"));
|
|
22
30
|
}
|
|
31
|
+
function createSanctuaryBundlePreparationFailure(action) {
|
|
32
|
+
return new DaemonPreparationFailure(`Sanctuary installation needs attention\n human-required: ${SANCTUARY_BUNDLE_RECOVERY_GUIDANCE[action]}`);
|
|
33
|
+
}
|
|
34
|
+
function failDaemonPreparation(input, error, fallback) {
|
|
35
|
+
input.markStartupFailure();
|
|
36
|
+
const controlledMessage = error instanceof DaemonPreparationFailure ? error.message : fallback;
|
|
37
|
+
failFastDaemonStartup({ exit: input.exit, errorMessage: controlledMessage, eventMessage: controlledMessage });
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
23
40
|
function failFastContainerCredentialBootstrapStartup(input) {
|
|
24
41
|
failFastDaemonStartup({
|
|
25
42
|
exit: input.exit,
|
|
@@ -50,7 +67,19 @@ function failFastDaemonStartup(input) {
|
|
|
50
67
|
}
|
|
51
68
|
input.exit(1);
|
|
52
69
|
}
|
|
70
|
+
function failFastSanctuaryBundlePreparationStartup(input) {
|
|
71
|
+
const message = input.failure instanceof DaemonPreparationFailure ? input.failure.message : REDACTED_SANCTUARY_BUNDLE_PREPARATION_ERROR;
|
|
72
|
+
failFastDaemonStartup({ exit: input.exit, errorMessage: message, eventMessage: message });
|
|
73
|
+
}
|
|
53
74
|
async function startDaemonAfterContainerCredentialBootstrap(input) {
|
|
75
|
+
try {
|
|
76
|
+
const preflight = input.preflight?.();
|
|
77
|
+
if (preflight)
|
|
78
|
+
await preflight;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return failDaemonPreparation(input, error, REDACTED_SANCTUARY_BUNDLE_PREPARATION_ERROR);
|
|
82
|
+
}
|
|
54
83
|
try {
|
|
55
84
|
await input.loadBootstrap();
|
|
56
85
|
}
|
|
@@ -59,18 +88,19 @@ async function startDaemonAfterContainerCredentialBootstrap(input) {
|
|
|
59
88
|
failFastContainerCredentialBootstrapStartup({ exit: input.exit });
|
|
60
89
|
return false;
|
|
61
90
|
}
|
|
91
|
+
try {
|
|
92
|
+
const preparation = input.prepareManagedBundle?.();
|
|
93
|
+
if (preparation)
|
|
94
|
+
await preparation;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return failDaemonPreparation(input, error, REDACTED_SANCTUARY_BUNDLE_PREPARATION_ERROR);
|
|
98
|
+
}
|
|
62
99
|
try {
|
|
63
100
|
await input.prepareDaemon?.();
|
|
64
101
|
}
|
|
65
102
|
catch (error) {
|
|
66
|
-
input
|
|
67
|
-
const controlledMessage = error instanceof DaemonPreparationFailure ? error.message : null;
|
|
68
|
-
failFastDaemonStartup({
|
|
69
|
-
exit: input.exit,
|
|
70
|
-
errorMessage: controlledMessage ?? REDACTED_DAEMON_PREPARATION_ERROR,
|
|
71
|
-
eventMessage: controlledMessage ?? REDACTED_DAEMON_PREPARATION_ERROR,
|
|
72
|
-
});
|
|
73
|
-
return false;
|
|
103
|
+
return failDaemonPreparation(input, error, REDACTED_DAEMON_PREPARATION_ERROR);
|
|
74
104
|
}
|
|
75
105
|
await input.startDaemon();
|
|
76
106
|
return true;
|
|
@@ -74,6 +74,7 @@ const runtime_credentials_1 = require("../runtime-credentials");
|
|
|
74
74
|
const machine_identity_1 = require("../machine-identity");
|
|
75
75
|
const container_credential_bootstrap_1 = require("./container-credential-bootstrap");
|
|
76
76
|
const daemon_bootstrap_startup_1 = require("./daemon-bootstrap-startup");
|
|
77
|
+
const sanctuary_package_management_1 = require("./sanctuary-package-management");
|
|
77
78
|
const sanctuary_health_runner_1 = require("../../senses/sanctuary-health-runner");
|
|
78
79
|
const sanctuary_acceptance_marker_1 = require("./sanctuary-acceptance-marker");
|
|
79
80
|
const sanctuary_scheduler_liveness_1 = require("./sanctuary-scheduler-liveness");
|
|
@@ -92,6 +93,15 @@ const socketPath = parseSocketPath(process.argv);
|
|
|
92
93
|
(0, runtime_logging_1.configureDaemonRuntimeLogger)("daemon");
|
|
93
94
|
const entryPath = path.resolve(__dirname, "daemon-entry.js");
|
|
94
95
|
const mode = (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)());
|
|
96
|
+
const managedAgents = (0, agent_discovery_1.listEnabledBundleAgents)();
|
|
97
|
+
const sanctuaryPackageManagement = (0, sanctuary_package_management_1.resolveSanctuaryPackageManagementActivation)({ mode, argv: process.argv, managedAgents, repoRoot: (0, identity_1.getRepoRoot)(), bundlesRoot: (0, identity_1.getAgentBundlesRoot)(), runtimePackageVersion: (0, bundle_manifest_1.getPackageVersion)() });
|
|
98
|
+
try {
|
|
99
|
+
(0, sanctuary_package_management_1.requireSanctuaryPackageManagementDecision)(sanctuaryPackageManagement);
|
|
100
|
+
}
|
|
101
|
+
catch (failure) {
|
|
102
|
+
(0, daemon_bootstrap_startup_1.failFastSanctuaryBundlePreparationStartup)({ failure, exit: (code) => process.exit(code) });
|
|
103
|
+
throw failure;
|
|
104
|
+
}
|
|
95
105
|
(0, runtime_1.emitNervesEvent)({
|
|
96
106
|
component: "daemon",
|
|
97
107
|
event: "daemon.entry_start",
|
|
@@ -108,7 +118,6 @@ if (mode === "dev") {
|
|
|
108
118
|
meta: { repoRoot },
|
|
109
119
|
});
|
|
110
120
|
}
|
|
111
|
-
const managedAgents = (0, agent_discovery_1.listEnabledBundleAgents)();
|
|
112
121
|
const managedPrivateRuntimes = managedAgents.map((agent) => ({
|
|
113
122
|
agent,
|
|
114
123
|
config: (0, agent_discovery_1.readPrivateRuntimeConfig)(agent),
|
|
@@ -655,7 +664,9 @@ function scheduleStartupSentinelAfterProviderPreload(agent, preload) {
|
|
|
655
664
|
}
|
|
656
665
|
/* v8 ignore start -- habit wiring: lambdas delegate to processManager/fs; tested via HabitScheduler unit tests @preserve */
|
|
657
666
|
void (0, daemon_bootstrap_startup_1.startDaemonAfterContainerCredentialBootstrap)({
|
|
667
|
+
preflight: () => { (0, sanctuary_package_management_1.requireSanctuaryPackageManagementDecision)(sanctuaryPackageManagement); },
|
|
658
668
|
loadBootstrap: () => (0, container_credential_bootstrap_1.loadContainerCredentialBootstrap)(managedAgents),
|
|
669
|
+
prepareManagedBundle: () => { (0, sanctuary_package_management_1.prepareSanctuaryPackageManagedBundle)(sanctuaryPackageManagement); },
|
|
659
670
|
prepareDaemon: prepareProviderRuntime,
|
|
660
671
|
startDaemon: () => daemon.start(),
|
|
661
672
|
markStartupFailure: () => { _tombstoneWritten = true; },
|