@ouro.bot/cli 0.1.0-alpha.732 → 0.1.0-alpha.734
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/changelog.json +34 -0
- package/deploy/unraid/Dockerfile +27 -0
- package/deploy/unraid/README.txt +1368 -0
- package/deploy/unraid/audit-container-spec.sh +4 -0
- package/deploy/unraid/container-runtime.json +1 -0
- package/deploy/unraid/sanctuary-acceptance-adapter.sh +37 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +200 -0
- package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
- package/deploy/unraid/sanctuary-deployment-target.mjs +705 -0
- package/deploy/unraid/sanctuary-safe-rename.py +24 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +1777 -0
- package/deploy/unraid/sanctuary-unit16-run.sh +502 -0
- package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
- package/deploy/unraid/sanctuary.ouro/agent.json +24 -0
- package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
- package/deploy/unraid/sanctuary.ouro/provider-readiness.json +18 -0
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -0
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -0
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +3 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +5 -0
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +21 -0
- package/deploy/unraid/sanctuary.xml +25 -0
- package/dist/heart/approval-files.js +113 -0
- package/dist/heart/approval-store.js +168 -9
- package/dist/heart/auth/auth-flow.js +17 -1
- package/dist/heart/autonomy-budget.js +44 -1
- package/dist/heart/awaiting/await-scheduler.js +6 -2
- package/dist/heart/config.js +8 -0
- package/dist/heart/core.js +117 -132
- package/dist/heart/daemon/cli-exec.js +74 -2
- package/dist/heart/daemon/cli-help.js +6 -1
- package/dist/heart/daemon/cli-parse.js +6 -4
- package/dist/heart/daemon/container-credential-bootstrap.js +185 -0
- package/dist/heart/daemon/container-healthcheck.js +94 -0
- package/dist/heart/daemon/container-runtime.js +84 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +156 -0
- package/dist/heart/daemon/container-spec-auditor.js +156 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +41 -0
- package/dist/heart/daemon/daemon-entry.js +116 -28
- package/dist/heart/daemon/daemon-health.js +10 -0
- package/dist/heart/daemon/daemon.js +92 -5
- package/dist/heart/daemon/doctor.js +56 -7
- package/dist/heart/daemon/runtime-logging.js +6 -0
- package/dist/heart/daemon/runtime-mode.js +13 -2
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2365 -0
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1624 -0
- package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +985 -0
- package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
- package/dist/heart/daemon/sanctuary-scheduler-origin.js +126 -0
- package/dist/heart/daemon/sense-manager.js +25 -6
- package/dist/heart/daemon/supercronic-supervisor.js +267 -0
- package/dist/heart/habits/habit-scheduler.js +7 -3
- package/dist/heart/identity.js +13 -3
- package/dist/heart/provider-credentials.js +9 -1
- package/dist/heart/provider-models.js +8 -0
- package/dist/heart/provider-ping.js +5 -2
- package/dist/heart/providers/openai-compatible.js +177 -0
- package/dist/heart/runtime-credentials.js +217 -8
- package/dist/heart/sense-truth.js +2 -0
- package/dist/heart/tool-approval.js +1 -0
- package/dist/heart/turn-context.js +8 -0
- package/dist/mailroom/blob-store.js +97 -19
- package/dist/mailroom/cache-sync-cli.js +5 -1
- package/dist/mailroom/core.js +23 -8
- package/dist/mailroom/hosted-cache-sync.js +179 -44
- package/dist/mailroom/search-cache.js +109 -0
- package/dist/mind/prompt.js +9 -0
- package/dist/nerves/coverage/source-scanner.js +4 -4
- package/dist/nerves/index.js +75 -9
- package/dist/nerves/runtime.js +12 -0
- package/dist/repertoire/tool-arguments.js +39 -0
- package/dist/repertoire/tools-unraid.js +353 -0
- package/dist/repertoire/tools.js +19 -1
- package/dist/repertoire/unraid-client.js +155 -0
- package/dist/repertoire/unraid-restart.js +130 -0
- package/dist/senses/private-runtime-worker.js +3 -2
- package/dist/senses/sanctuary-grounding.js +118 -0
- package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
- package/dist/senses/sanctuary-health-acceptance-probe.js +772 -0
- package/dist/senses/sanctuary-health-runner.js +146 -0
- package/dist/senses/sanctuary-health.js +447 -0
- package/dist/senses/sanctuary-interactive-control.js +306 -0
- package/dist/senses/sanctuary-runtime.js +206 -0
- package/dist/senses/shared-turn.js +11 -3
- package/dist/senses/telegram-approval-runtime.js +526 -0
- package/dist/senses/telegram-audit-ledger.js +220 -0
- package/dist/senses/telegram-client.js +1471 -0
- package/dist/senses/telegram-entry.js +80 -0
- package/dist/senses/telegram.js +1232 -0
- package/npm-shrinkwrap.json +6558 -0
- package/package.json +4 -2
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getDefaultContainerCredentialBootstrapPath = getDefaultContainerCredentialBootstrapPath;
|
|
37
|
+
exports.loadContainerCredentialBootstrap = loadContainerCredentialBootstrap;
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const os = __importStar(require("node:os"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const runtime_credentials_1 = require("../runtime-credentials");
|
|
42
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
43
|
+
const machine_identity_1 = require("../machine-identity");
|
|
44
|
+
const MAX_BOOTSTRAP_BYTES = 128 * 1024;
|
|
45
|
+
function isRecord(value) {
|
|
46
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
function parseEnvelope(raw) {
|
|
49
|
+
const value = JSON.parse(raw);
|
|
50
|
+
if (!isRecord(value) || value.schemaVersion !== 1 || !Array.isArray(value.credentials)) {
|
|
51
|
+
throw new Error("container credential bootstrap envelope is invalid");
|
|
52
|
+
}
|
|
53
|
+
if (Object.keys(value).sort().join(",") !== "credentials,schemaVersion") {
|
|
54
|
+
throw new Error("container credential bootstrap has unsupported fields");
|
|
55
|
+
}
|
|
56
|
+
if (value.credentials.length === 0) {
|
|
57
|
+
throw new Error("container credential bootstrap must contain at least one credential");
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function fsyncDirectory(directoryPath) {
|
|
62
|
+
const descriptor = fs.openSync(directoryPath, "r");
|
|
63
|
+
try {
|
|
64
|
+
fs.fsyncSync(descriptor);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
fs.closeSync(descriptor);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function deleteDurably(filePath) {
|
|
71
|
+
fs.unlinkSync(filePath);
|
|
72
|
+
fsyncDirectory(path.dirname(filePath));
|
|
73
|
+
}
|
|
74
|
+
function assertSafeBootstrapFile(stat, label) {
|
|
75
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
76
|
+
throw new Error(`container credential ${label} must be a regular file`);
|
|
77
|
+
}
|
|
78
|
+
if ((stat.mode & 0o077) !== 0)
|
|
79
|
+
throw new Error("container credential bootstrap must have mode 0600");
|
|
80
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
|
|
81
|
+
throw new Error("container credential bootstrap must be owned by the runtime user");
|
|
82
|
+
}
|
|
83
|
+
if (stat.size > MAX_BOOTSTRAP_BYTES)
|
|
84
|
+
throw new Error("container credential bootstrap is too large");
|
|
85
|
+
}
|
|
86
|
+
function getDefaultContainerCredentialBootstrapPath() {
|
|
87
|
+
return path.join(os.homedir(), ".ouro-cli", "container-credentials.json");
|
|
88
|
+
}
|
|
89
|
+
function optionalStat(filePath) {
|
|
90
|
+
try {
|
|
91
|
+
return fs.lstatSync(filePath);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
95
|
+
return null;
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function loadContainerCredentialBootstrap(enabledAgents, options = {}) {
|
|
100
|
+
const filePath = options.path ?? getDefaultContainerCredentialBootstrapPath();
|
|
101
|
+
const consumingPath = `${filePath}.consuming`;
|
|
102
|
+
let sourceStat = optionalStat(filePath);
|
|
103
|
+
const claimedStat = optionalStat(consumingPath);
|
|
104
|
+
if (sourceStat && claimedStat) {
|
|
105
|
+
assertSafeBootstrapFile(sourceStat, "bootstrap");
|
|
106
|
+
assertSafeBootstrapFile(claimedStat, "consuming state");
|
|
107
|
+
const sourceBytes = fs.readFileSync(filePath);
|
|
108
|
+
const claimedBytes = fs.readFileSync(consumingPath);
|
|
109
|
+
if (!sourceBytes.equals(claimedBytes)) {
|
|
110
|
+
throw new Error("human-required: container credential source and claim differ; securely compare and quarantine them without printing their contents");
|
|
111
|
+
}
|
|
112
|
+
deleteDurably(filePath);
|
|
113
|
+
sourceStat = null;
|
|
114
|
+
}
|
|
115
|
+
if (claimedStat)
|
|
116
|
+
assertSafeBootstrapFile(claimedStat, "consuming state");
|
|
117
|
+
let stat;
|
|
118
|
+
if (sourceStat) {
|
|
119
|
+
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
120
|
+
throw new Error("container credential bootstrap must be a regular file");
|
|
121
|
+
}
|
|
122
|
+
fs.renameSync(filePath, consumingPath);
|
|
123
|
+
fsyncDirectory(path.dirname(filePath));
|
|
124
|
+
stat = sourceStat;
|
|
125
|
+
}
|
|
126
|
+
else if (claimedStat) {
|
|
127
|
+
stat = claimedStat;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
assertSafeBootstrapFile(stat, claimedStat ? "consuming state" : "bootstrap");
|
|
133
|
+
const envelope = parseEnvelope(fs.readFileSync(consumingPath, "utf8"));
|
|
134
|
+
const allowed = new Set(enabledAgents);
|
|
135
|
+
const loaded = new Set();
|
|
136
|
+
for (const message of envelope.credentials) {
|
|
137
|
+
if (!isRecord(message) || typeof message.agentName !== "string" || !allowed.has(message.agentName)) {
|
|
138
|
+
throw new Error("container credential bootstrap agent is not enabled");
|
|
139
|
+
}
|
|
140
|
+
if (loaded.has(message.agentName))
|
|
141
|
+
throw new Error("container credential bootstrap contains a duplicate agent");
|
|
142
|
+
if (!(0, runtime_credentials_1.isRuntimeCredentialBootstrapMessage)(message))
|
|
143
|
+
throw new Error("container credential bootstrap message is invalid");
|
|
144
|
+
loaded.add(message.agentName);
|
|
145
|
+
}
|
|
146
|
+
const machineId = (0, machine_identity_1.loadOrCreateMachineIdentity)().machineId;
|
|
147
|
+
for (const message of envelope.credentials) {
|
|
148
|
+
if (isRecord(message) && typeof message.machineId === "string" && message.machineId.trim() !== machineId) {
|
|
149
|
+
throw new Error("container credential bootstrap machineId does not match this machine");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const persist = options.persist ?? ((message) => (0, runtime_credentials_1.persistRuntimeCredentialBootstrapMessage)(message, {
|
|
153
|
+
machineId,
|
|
154
|
+
}));
|
|
155
|
+
try {
|
|
156
|
+
for (const message of envelope.credentials) {
|
|
157
|
+
if (!await persist(message))
|
|
158
|
+
throw new Error("invalid bootstrap message");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
(0, runtime_1.emitNervesEvent)({
|
|
163
|
+
level: "error",
|
|
164
|
+
component: "config/identity",
|
|
165
|
+
event: "config.container_credentials_persist_error",
|
|
166
|
+
message: "container credential bootstrap persistence failed; recoverable claim retained",
|
|
167
|
+
meta: { agents: [...loaded].sort(), count: loaded.size, claimedPath: consumingPath },
|
|
168
|
+
});
|
|
169
|
+
throw new Error("container credential bootstrap persistence failed; recoverable claim retained for reconciliation");
|
|
170
|
+
}
|
|
171
|
+
if (options.apply) {
|
|
172
|
+
for (const message of envelope.credentials) {
|
|
173
|
+
if (!options.apply(message))
|
|
174
|
+
throw new Error("container credential bootstrap message is invalid");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
deleteDurably(consumingPath);
|
|
178
|
+
(0, runtime_1.emitNervesEvent)({
|
|
179
|
+
component: "config/identity",
|
|
180
|
+
event: "config.container_credentials_loaded",
|
|
181
|
+
message: "loaded container credential bootstrap",
|
|
182
|
+
meta: { agents: [...loaded].sort(), count: loaded.size },
|
|
183
|
+
});
|
|
184
|
+
return [...loaded].sort();
|
|
185
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runContainerHealthcheck = runContainerHealthcheck;
|
|
37
|
+
exports.runContainerHealthcheckMain = runContainerHealthcheckMain;
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const os = __importStar(require("node:os"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const node_child_process_1 = require("node:child_process");
|
|
42
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
43
|
+
const container_runtime_1 = require("./container-runtime");
|
|
44
|
+
function fail(reason) {
|
|
45
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "daemon", event: "daemon.container_healthcheck_error", message: "container healthcheck failed", meta: { reason } });
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
throw new Error(reason);
|
|
48
|
+
}
|
|
49
|
+
function runContainerHealthcheck(options = {}) {
|
|
50
|
+
const argv = options.argv ?? process.argv;
|
|
51
|
+
const now = options.now ?? Date.now;
|
|
52
|
+
const index = argv.indexOf("--agent");
|
|
53
|
+
const agent = index >= 0 ? argv[index + 1] : undefined;
|
|
54
|
+
if (!agent)
|
|
55
|
+
fail("missing agent");
|
|
56
|
+
const healthPath = path.join(os.homedir(), ".ouro-cli", "daemon-health.json");
|
|
57
|
+
let stat;
|
|
58
|
+
let health;
|
|
59
|
+
try {
|
|
60
|
+
stat = fs.statSync(healthPath);
|
|
61
|
+
health = JSON.parse(fs.readFileSync(healthPath, "utf8"));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
fail("health state unavailable");
|
|
65
|
+
}
|
|
66
|
+
const checkedAt = now();
|
|
67
|
+
if (checkedAt - stat.mtimeMs > 90_000 || stat.mtimeMs > checkedAt + 1_000)
|
|
68
|
+
fail("health state stale");
|
|
69
|
+
if (health.pid !== 1 || !fs.existsSync("/proc/1"))
|
|
70
|
+
fail("daemon is not PID 1");
|
|
71
|
+
if (health.status !== "healthy" && health.status !== "partial")
|
|
72
|
+
fail("daemon status is not ready");
|
|
73
|
+
const agents = health.agents;
|
|
74
|
+
const managed = agents?.[agent];
|
|
75
|
+
if (!managed || managed.status !== "running" || typeof managed.pid !== "number" || managed.pid <= 0 || !fs.existsSync(`/proc/${managed.pid}`))
|
|
76
|
+
fail("managed agent is not running");
|
|
77
|
+
let processArguments = "";
|
|
78
|
+
try {
|
|
79
|
+
processArguments = (0, node_child_process_1.execFileSync)("ps", ["-eo", "args="], { encoding: "utf8", timeout: 2_000 });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
fail("managed process inventory is unavailable");
|
|
83
|
+
}
|
|
84
|
+
if (!(0, container_runtime_1.hasManagedTelegramProcess)(processArguments, agent))
|
|
85
|
+
fail("managed Telegram sense is not running exactly once");
|
|
86
|
+
if (!(0, container_runtime_1.hasManagedSupercronicProcess)(processArguments, agent))
|
|
87
|
+
fail("managed Supercronic scheduler is not running exactly once");
|
|
88
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.container_healthcheck_end", message: "container healthcheck passed", meta: { agent } });
|
|
89
|
+
}
|
|
90
|
+
function runContainerHealthcheckMain(isMain, runner) {
|
|
91
|
+
if (isMain)
|
|
92
|
+
runner();
|
|
93
|
+
}
|
|
94
|
+
runContainerHealthcheckMain(require.main === module, runContainerHealthcheck);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.hasManagedTelegramProcess = hasManagedTelegramProcess;
|
|
37
|
+
exports.hasManagedSupercronicProcess = hasManagedSupercronicProcess;
|
|
38
|
+
exports.readContainerRuntimePolicy = readContainerRuntimePolicy;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
41
|
+
function hasManagedTelegramProcess(processArguments, agent) {
|
|
42
|
+
const wanted = agent.trim();
|
|
43
|
+
if (!wanted)
|
|
44
|
+
return false;
|
|
45
|
+
const matches = processArguments.split("\n").filter((line) => {
|
|
46
|
+
const fields = line.trim().split(/\s+/u);
|
|
47
|
+
const entryIndex = fields.findIndex((field) => field === "/opt/ouro/dist/senses/telegram-entry.js");
|
|
48
|
+
const agentIndex = fields.findIndex((field) => field === "--agent");
|
|
49
|
+
return entryIndex >= 0 && agentIndex >= 0 && fields[agentIndex + 1] === wanted;
|
|
50
|
+
});
|
|
51
|
+
return matches.length === 1;
|
|
52
|
+
}
|
|
53
|
+
function hasManagedSupercronicProcess(processArguments, agent) {
|
|
54
|
+
const expected = [
|
|
55
|
+
"/usr/local/bin/supercronic",
|
|
56
|
+
"-split-logs",
|
|
57
|
+
"-inotify",
|
|
58
|
+
`/home/ouro/.ouro-cli/scheduler/${agent}.crontab`,
|
|
59
|
+
];
|
|
60
|
+
const matches = processArguments.split("\n").filter((line) => {
|
|
61
|
+
const fields = line.trim().split(/\s+/u);
|
|
62
|
+
return fields.length === expected.length && fields.every((field, index) => field === expected[index]);
|
|
63
|
+
});
|
|
64
|
+
return matches.length === 1;
|
|
65
|
+
}
|
|
66
|
+
function readContainerRuntimePolicy(options = {}) {
|
|
67
|
+
const filePath = options.path ?? "/opt/ouro/container-runtime.json";
|
|
68
|
+
const readFile = options.readFile ?? ((target) => fs.readFileSync(target, "utf8"));
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = readFile(filePath);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
75
|
+
return null;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
const value = JSON.parse(raw);
|
|
79
|
+
if (value.scheduler !== "supercronic" || value.updates !== "disabled" || Object.keys(value).sort().join(",") !== "scheduler,updates") {
|
|
80
|
+
throw new Error("container runtime policy is invalid");
|
|
81
|
+
}
|
|
82
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.container_runtime_policy_loaded", message: "container runtime policy loaded", meta: { scheduler: value.scheduler, updates: value.updates } });
|
|
83
|
+
return { scheduler: "supercronic", updates: "disabled" };
|
|
84
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runContainerSpecAuditorCli = runContainerSpecAuditorCli;
|
|
37
|
+
exports.runContainerSpecAuditorMain = runContainerSpecAuditorMain;
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
40
|
+
const container_spec_auditor_1 = require("./container-spec-auditor");
|
|
41
|
+
function parseModeArguments(args, flags) {
|
|
42
|
+
if (args.length !== flags.length * 2)
|
|
43
|
+
return null;
|
|
44
|
+
const allowed = new Set(flags);
|
|
45
|
+
const parsed = {};
|
|
46
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
47
|
+
const flag = args[index];
|
|
48
|
+
const value = args[index + 1];
|
|
49
|
+
if (!allowed.has(flag) || Object.prototype.hasOwnProperty.call(parsed, flag) || !value)
|
|
50
|
+
return null;
|
|
51
|
+
parsed[flag] = value;
|
|
52
|
+
}
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
function parseSingleInspect(raw) {
|
|
56
|
+
try {
|
|
57
|
+
const value = JSON.parse(raw);
|
|
58
|
+
if (!Array.isArray(value) || value.length !== 1)
|
|
59
|
+
return null;
|
|
60
|
+
const item = value[0];
|
|
61
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
62
|
+
return null;
|
|
63
|
+
return item;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function runContainerSpecAuditorCli(args, deps = {}) {
|
|
70
|
+
const readFile = deps.readFile ?? ((filePath) => fs.readFileSync(filePath, "utf8"));
|
|
71
|
+
const write = deps.write ?? ((text) => process.stdout.write(text));
|
|
72
|
+
const staged = parseModeArguments(args, ["--template", "--runtime-policy", "--expected-image"]);
|
|
73
|
+
const effective = parseModeArguments(args, ["--inspect", "--image-inspect", "--expected-image"]);
|
|
74
|
+
if (!staged && !effective) {
|
|
75
|
+
write(JSON.stringify({ ok: false, error: "usage: staged --template <path> --runtime-policy <path> --expected-image <id>; effective --inspect <path> --image-inspect <path> --expected-image <id>" }) + "\n");
|
|
76
|
+
(0, runtime_1.emitNervesEvent)({
|
|
77
|
+
level: "error",
|
|
78
|
+
component: "daemon",
|
|
79
|
+
event: "daemon.container_spec_auditor_cli_error",
|
|
80
|
+
message: "container spec auditor arguments were invalid",
|
|
81
|
+
meta: { argumentCount: args.length },
|
|
82
|
+
});
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
if (effective) {
|
|
86
|
+
let containerText;
|
|
87
|
+
let imageText;
|
|
88
|
+
try {
|
|
89
|
+
containerText = readFile(effective["--inspect"]);
|
|
90
|
+
imageText = readFile(effective["--image-inspect"]);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
write(JSON.stringify({ ok: false, error: "effective audit inputs are unreadable" }) + "\n");
|
|
94
|
+
(0, runtime_1.emitNervesEvent)({
|
|
95
|
+
level: "error",
|
|
96
|
+
component: "daemon",
|
|
97
|
+
event: "daemon.container_spec_auditor_cli_error",
|
|
98
|
+
message: "container spec auditor could not read effective inputs",
|
|
99
|
+
meta: { reason: error instanceof Error ? error.message : String(error) },
|
|
100
|
+
});
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
const containerInspect = parseSingleInspect(containerText);
|
|
104
|
+
const imageInspect = parseSingleInspect(imageText);
|
|
105
|
+
const imageConfig = imageInspect?.Config;
|
|
106
|
+
const expectedEnvironment = imageConfig && typeof imageConfig === "object" && !Array.isArray(imageConfig)
|
|
107
|
+
? imageConfig.Env
|
|
108
|
+
: null;
|
|
109
|
+
if (!containerInspect || !imageInspect || !Array.isArray(expectedEnvironment) || !expectedEnvironment.every((entry) => typeof entry === "string")) {
|
|
110
|
+
write(JSON.stringify({ ok: false, error: "effective audit inputs must each contain exactly one canonical inspect record" }) + "\n");
|
|
111
|
+
(0, runtime_1.emitNervesEvent)({
|
|
112
|
+
level: "error",
|
|
113
|
+
component: "daemon",
|
|
114
|
+
event: "daemon.container_spec_auditor_cli_error",
|
|
115
|
+
message: "container spec auditor effective inputs were invalid",
|
|
116
|
+
meta: { containerValid: !!containerInspect, imageValid: !!imageInspect },
|
|
117
|
+
});
|
|
118
|
+
return 2;
|
|
119
|
+
}
|
|
120
|
+
const result = (0, container_spec_auditor_1.auditSanctuaryContainerSpec)(containerInspect, {
|
|
121
|
+
expectedImage: effective["--expected-image"],
|
|
122
|
+
expectedEnvironment,
|
|
123
|
+
});
|
|
124
|
+
if (imageInspect.Id !== effective["--expected-image"]) {
|
|
125
|
+
result.ok = false;
|
|
126
|
+
result.violations.unshift("reviewed image inspect identity does not match the expected local image ID");
|
|
127
|
+
}
|
|
128
|
+
write(JSON.stringify(result) + "\n");
|
|
129
|
+
return result.ok ? 0 : 1;
|
|
130
|
+
}
|
|
131
|
+
let templateXml;
|
|
132
|
+
let runtimePolicyText;
|
|
133
|
+
try {
|
|
134
|
+
templateXml = readFile(staged["--template"]);
|
|
135
|
+
runtimePolicyText = readFile(staged["--runtime-policy"]);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
write(JSON.stringify({ ok: false, error: "staged audit inputs are unreadable" }) + "\n");
|
|
139
|
+
(0, runtime_1.emitNervesEvent)({
|
|
140
|
+
level: "error",
|
|
141
|
+
component: "daemon",
|
|
142
|
+
event: "daemon.container_spec_auditor_cli_error",
|
|
143
|
+
message: "container spec auditor could not read staged files",
|
|
144
|
+
meta: { reason: error instanceof Error ? error.message : String(error) },
|
|
145
|
+
});
|
|
146
|
+
return 2;
|
|
147
|
+
}
|
|
148
|
+
const result = (0, container_spec_auditor_1.auditSanctuaryStagedFiles)({ templateXml, runtimePolicyText, expectedImage: staged["--expected-image"] });
|
|
149
|
+
write(JSON.stringify(result) + "\n");
|
|
150
|
+
return result.ok ? 0 : 1;
|
|
151
|
+
}
|
|
152
|
+
function runContainerSpecAuditorMain(isMain, args, runner) {
|
|
153
|
+
if (isMain)
|
|
154
|
+
process.exitCode = runner(args);
|
|
155
|
+
}
|
|
156
|
+
runContainerSpecAuditorMain(require.main === module, process.argv.slice(2), runContainerSpecAuditorCli);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditSanctuaryContainerSpec = auditSanctuaryContainerSpec;
|
|
4
|
+
exports.auditSanctuaryStagedFiles = auditSanctuaryStagedFiles;
|
|
5
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
6
|
+
const EXPECTED_BINDS = [
|
|
7
|
+
"/mnt/user/appdata/ouro-butler/runtime/.ouro-cli:/home/ouro/.ouro-cli:rw",
|
|
8
|
+
"/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro:/home/ouro/AgentBundles/sanctuary.ouro:rw",
|
|
9
|
+
];
|
|
10
|
+
const EXPECTED_MOUNTS = [
|
|
11
|
+
["/mnt/user/appdata/ouro-butler/runtime/.ouro-cli", "/home/ouro/.ouro-cli"],
|
|
12
|
+
["/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro", "/home/ouro/AgentBundles/sanctuary.ouro"],
|
|
13
|
+
];
|
|
14
|
+
const EXPECTED_EXTRA_PARAMS = "--restart=unless-stopped --user=10001:10001";
|
|
15
|
+
function record(value) {
|
|
16
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
17
|
+
? value
|
|
18
|
+
: null;
|
|
19
|
+
}
|
|
20
|
+
function stringArray(value) {
|
|
21
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : null;
|
|
22
|
+
}
|
|
23
|
+
function isEmptyRecord(value) {
|
|
24
|
+
const candidate = record(value);
|
|
25
|
+
return candidate !== null && Object.keys(candidate).length === 0;
|
|
26
|
+
}
|
|
27
|
+
function auditSanctuaryContainerSpec(value, options) {
|
|
28
|
+
const violations = [];
|
|
29
|
+
const root = record(value);
|
|
30
|
+
const config = record(root?.Config);
|
|
31
|
+
const host = record(root?.HostConfig);
|
|
32
|
+
if (!root || !config || !host) {
|
|
33
|
+
violations.push("inspect payload must contain object Config and HostConfig records");
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
if (!/^sha256:[a-f0-9]{64}$/u.test(options.expectedImage))
|
|
37
|
+
violations.push("expected image must be an exact local Docker image ID");
|
|
38
|
+
if (config.Image !== options.expectedImage)
|
|
39
|
+
violations.push("image does not match the reviewed exact local Docker image ID");
|
|
40
|
+
if (root.Path !== "node")
|
|
41
|
+
violations.push("effective container path must be node");
|
|
42
|
+
if (JSON.stringify(root.Args) !== JSON.stringify(["/opt/ouro/dist/heart/daemon/daemon-entry.js"]))
|
|
43
|
+
violations.push("effective container arguments must be the direct daemon entry");
|
|
44
|
+
if (config.User !== "10001:10001")
|
|
45
|
+
violations.push("container user must be 10001:10001");
|
|
46
|
+
if (JSON.stringify(config.Entrypoint) !== JSON.stringify(["node", "/opt/ouro/dist/heart/daemon/daemon-entry.js"]))
|
|
47
|
+
violations.push("entrypoint must be the direct daemon entry");
|
|
48
|
+
if (!(config.Cmd === null || (Array.isArray(config.Cmd) && config.Cmd.length === 0)))
|
|
49
|
+
violations.push("container command must be empty");
|
|
50
|
+
const environment = stringArray(config.Env);
|
|
51
|
+
if (!environment || JSON.stringify(environment) !== JSON.stringify(options.expectedEnvironment))
|
|
52
|
+
violations.push("container environment must exactly match the reviewed image environment");
|
|
53
|
+
if (!(config.ExposedPorts === null || config.ExposedPorts === undefined || isEmptyRecord(config.ExposedPorts)))
|
|
54
|
+
violations.push("container must expose no ports");
|
|
55
|
+
if (host.NetworkMode !== "host")
|
|
56
|
+
violations.push("network mode must be host");
|
|
57
|
+
if (host.Privileged !== false)
|
|
58
|
+
violations.push("container must not be privileged");
|
|
59
|
+
const restart = record(host.RestartPolicy);
|
|
60
|
+
if (restart?.Name !== "unless-stopped" || restart.MaximumRetryCount !== 0)
|
|
61
|
+
violations.push("restart policy must be unless-stopped");
|
|
62
|
+
if (!isEmptyRecord(host.PortBindings))
|
|
63
|
+
violations.push("container must publish no ports");
|
|
64
|
+
if (!Array.isArray(host.Devices) || host.Devices.length !== 0)
|
|
65
|
+
violations.push("container must have no devices");
|
|
66
|
+
if (!(host.CapAdd === null || (Array.isArray(host.CapAdd) && host.CapAdd.length === 0)))
|
|
67
|
+
violations.push("container must add no capabilities");
|
|
68
|
+
const binds = stringArray(host.Binds);
|
|
69
|
+
if (!binds || JSON.stringify([...binds].sort()) !== JSON.stringify([...EXPECTED_BINDS].sort()))
|
|
70
|
+
violations.push("bind set must equal the canonical two writable roots");
|
|
71
|
+
const mounts = Array.isArray(root.Mounts) ? root.Mounts : [];
|
|
72
|
+
const normalizedMounts = mounts.map((mount) => {
|
|
73
|
+
const item = record(mount);
|
|
74
|
+
return item && item.Type === "bind" && item.RW === true && typeof item.Source === "string" && typeof item.Destination === "string"
|
|
75
|
+
? [item.Source, item.Destination]
|
|
76
|
+
: null;
|
|
77
|
+
});
|
|
78
|
+
if (normalizedMounts.some((mount) => mount === null)
|
|
79
|
+
|| JSON.stringify(normalizedMounts.sort()) !== JSON.stringify(EXPECTED_MOUNTS.map((mount) => [...mount]).sort())) {
|
|
80
|
+
violations.push("effective mounts must equal the canonical two writable bind mounts");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const result = { ok: violations.length === 0, violations };
|
|
84
|
+
if (result.ok) {
|
|
85
|
+
(0, runtime_1.emitNervesEvent)({
|
|
86
|
+
component: "daemon",
|
|
87
|
+
event: "daemon.container_spec_audit_end",
|
|
88
|
+
message: "Sanctuary container spec audit passed",
|
|
89
|
+
meta: { violationCount: 0 },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
(0, runtime_1.emitNervesEvent)({
|
|
94
|
+
level: "error",
|
|
95
|
+
component: "daemon",
|
|
96
|
+
event: "daemon.container_spec_audit_error",
|
|
97
|
+
message: "Sanctuary container spec audit failed",
|
|
98
|
+
meta: { violationCount: violations.length },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
function tagValues(xml, name) {
|
|
104
|
+
return [...xml.matchAll(new RegExp(`<${name}>([^<]*)</${name}>`, "gu"))]
|
|
105
|
+
.map((match) => match[1]);
|
|
106
|
+
}
|
|
107
|
+
function singleTag(xml, name) {
|
|
108
|
+
const values = tagValues(xml, name);
|
|
109
|
+
return values.length === 1 ? values[0] : undefined;
|
|
110
|
+
}
|
|
111
|
+
function auditSanctuaryStagedFiles(input) {
|
|
112
|
+
const violations = [];
|
|
113
|
+
let runtimePolicy;
|
|
114
|
+
try {
|
|
115
|
+
runtimePolicy = JSON.parse(input.runtimePolicyText);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
runtimePolicy = null;
|
|
119
|
+
}
|
|
120
|
+
const policy = record(runtimePolicy);
|
|
121
|
+
if (policy?.scheduler !== "supercronic" || policy.updates !== "disabled" || Object.keys(policy).sort().join(",") !== "scheduler,updates") {
|
|
122
|
+
violations.push("container runtime policy must be exactly scheduler=supercronic and updates=disabled");
|
|
123
|
+
}
|
|
124
|
+
const configOpenCount = [...input.templateXml.matchAll(/<Config\b/gu)].length;
|
|
125
|
+
const configEntries = [...input.templateXml.matchAll(/<Config\b([^>]*)>([^<]*)<\/Config>/gu)];
|
|
126
|
+
const pathConfigs = configEntries
|
|
127
|
+
.map((match) => {
|
|
128
|
+
const type = match[1].match(/\bType="([^"]+)"/u)?.[1];
|
|
129
|
+
const target = match[1].match(/\bTarget="([^"]+)"/u)?.[1];
|
|
130
|
+
const mode = match[1].match(/\bMode="([^"]+)"/u)?.[1];
|
|
131
|
+
return type === "Path" && target && mode ? `${match[2]}:${target}:${mode}` : "invalid";
|
|
132
|
+
});
|
|
133
|
+
if (configOpenCount !== 2
|
|
134
|
+
|| configEntries.length !== 2
|
|
135
|
+
|| JSON.stringify([...pathConfigs].sort()) !== JSON.stringify([...EXPECTED_BINDS].sort())) {
|
|
136
|
+
violations.push("template Config entries must equal the canonical two path binds");
|
|
137
|
+
}
|
|
138
|
+
const postArgsOpenCount = [...input.templateXml.matchAll(/<PostArgs\b/gu)].length;
|
|
139
|
+
const postArgs = [...input.templateXml.matchAll(/<PostArgs(?:(?:\s*\/>)|>([^<]*)<\/PostArgs>)/gu)];
|
|
140
|
+
if (postArgsOpenCount !== 1 || postArgs.length !== 1 || (postArgs[0]?.[1] ?? "") !== "") {
|
|
141
|
+
violations.push("template PostArgs must be present exactly once and empty");
|
|
142
|
+
}
|
|
143
|
+
const extraParams = singleTag(input.templateXml, "ExtraParams");
|
|
144
|
+
if (extraParams !== EXPECTED_EXTRA_PARAMS)
|
|
145
|
+
violations.push("template ExtraParams must equal the canonical user and restart flags");
|
|
146
|
+
const repository = singleTag(input.templateXml, "Repository");
|
|
147
|
+
if (!/^sha256:[a-f0-9]{64}$/u.test(input.expectedImage))
|
|
148
|
+
violations.push("expected image must be an exact local Docker image ID");
|
|
149
|
+
if (repository !== input.expectedImage)
|
|
150
|
+
violations.push("image does not match the reviewed exact local Docker image ID");
|
|
151
|
+
if (singleTag(input.templateXml, "Network") !== "host")
|
|
152
|
+
violations.push("network mode must be host");
|
|
153
|
+
if (singleTag(input.templateXml, "Privileged") !== "false")
|
|
154
|
+
violations.push("container must not be privileged");
|
|
155
|
+
return { ok: violations.length === 0, violations };
|
|
156
|
+
}
|