@ouro.bot/cli 0.1.0-alpha.741 → 0.1.0-alpha.743
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 +42 -0
- package/deploy/unraid/Dockerfile +7 -2
- package/deploy/unraid/README.txt +717 -106
- package/deploy/unraid/migrate-sanctuary-bundle.mjs +21 -0
- package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
- package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
- package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
- package/deploy/unraid/ouro-events/install-usenet-guard.sh +434 -0
- package/deploy/unraid/ouro-events/usenet-health.sh +178 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +4 -3
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +5 -1
- package/deploy/unraid/sanctuary.ouro/agent.json +2 -2
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +2 -2
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +1 -1
- package/deploy/unraid/sanctuary.ouro/provider-readiness.json +3 -10
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -1
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -1
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +4 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +6 -2
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +4 -2
- package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +19 -17
- package/deploy/unraid/sanctuary.xml +12 -9
- package/dist/arc/cares.js +171 -37
- package/dist/arc/obligations.js +38 -0
- package/dist/heart/awaiting/await-alert.js +4 -2
- package/dist/heart/awaiting/await-expiry.js +39 -4
- package/dist/heart/awaiting/await-parser.js +8 -0
- package/dist/heart/context-loss-gauntlet.js +31 -2
- package/dist/heart/context-loss-sentinel.js +2 -0
- package/dist/heart/core.js +37 -7
- package/dist/heart/cross-chat-delivery.js +1 -0
- package/dist/heart/daemon/container-healthcheck.js +3 -1
- package/dist/heart/daemon/container-runtime.js +13 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +11 -7
- package/dist/heart/daemon/container-spec-auditor.js +38 -11
- package/dist/heart/daemon/daemon-entry.js +19 -5
- package/dist/heart/daemon/daemon.js +154 -22
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +151 -87
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +26 -29
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +42 -51
- package/dist/heart/daemon/sanctuary-bundle-migration.js +398 -0
- package/dist/heart/daemon/sanctuary-scheduler-origin.js +11 -5
- package/dist/heart/daemon/supercronic-supervisor.js +16 -1
- package/dist/heart/external-events/router.js +1025 -43
- package/dist/heart/provider-models.js +10 -3
- package/dist/heart/session-events.js +23 -5
- package/dist/heart/steward-policy.js +302 -0
- package/dist/heart/tool-approval.js +3 -1
- package/dist/heart/turn-context.js +1 -0
- package/dist/mind/context.js +8 -3
- package/dist/mind/pending.js +25 -0
- package/dist/mind/prompt.js +49 -1
- package/dist/repertoire/guardrails.js +2 -1
- package/dist/repertoire/relationship-authorization.js +194 -0
- package/dist/repertoire/tools-awaiting.js +229 -26
- package/dist/repertoire/tools-base.js +15 -0
- package/dist/repertoire/tools-continuity.js +259 -4
- package/dist/repertoire/tools-notes.js +29 -3
- package/dist/repertoire/tools-session.js +219 -4
- package/dist/repertoire/tools-steward-policy.js +88 -0
- package/dist/repertoire/tools-telegram-contacts.js +37 -0
- package/dist/repertoire/tools-unraid.js +113 -6
- package/dist/repertoire/tools.js +104 -9
- package/dist/repertoire/unraid-restart.js +86 -4
- package/dist/senses/private-runtime-worker.js +36 -3
- package/dist/senses/private-runtime.js +310 -63
- package/dist/senses/sanctuary-health-acceptance-probe.js +8 -6
- package/dist/senses/sanctuary-health-runner.js +51 -96
- package/dist/senses/sanctuary-health.js +54 -93
- package/dist/senses/sanctuary-runtime.js +36 -2
- package/dist/senses/sanctuary-sab.js +80 -0
- package/dist/senses/shared-turn.js +64 -9
- package/dist/senses/telegram-admission.js +684 -0
- package/dist/senses/telegram-approval-runtime.js +46 -6
- package/dist/senses/telegram-client.js +236 -72
- package/dist/senses/telegram-effect-adapter.js +787 -0
- package/dist/senses/telegram-entry.js +4 -1
- package/dist/senses/telegram.js +728 -48
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
|
@@ -40,6 +40,7 @@ const runtime_1 = require("../../nerves/runtime");
|
|
|
40
40
|
const await_parser_1 = require("./await-parser");
|
|
41
41
|
const await_runtime_state_1 = require("./await-runtime-state");
|
|
42
42
|
const await_alert_1 = require("./await-alert");
|
|
43
|
+
const obligations_1 = require("../../arc/obligations");
|
|
43
44
|
/**
|
|
44
45
|
* Daemon-side max_age expiry helper. Reads the pending await, moves it to
|
|
45
46
|
* `awaiting/.done/` with `status: expired`, writes `expired_at` and
|
|
@@ -69,8 +70,8 @@ async function archiveAndAlertExpiredAwait(options) {
|
|
|
69
70
|
}
|
|
70
71
|
const parsed = (0, await_parser_1.parseAwaitFile)(content, source);
|
|
71
72
|
const runtime = (0, await_runtime_state_1.readAwaitRuntimeState)(options.agentRoot, options.awaitName);
|
|
72
|
-
const lastObservation = runtime?.last_observation ?? null;
|
|
73
|
-
const expiredAt = now().toISOString();
|
|
73
|
+
const lastObservation = parsed.last_observation_at_expiry ?? runtime?.last_observation ?? null;
|
|
74
|
+
const expiredAt = parsed.expired_at ?? now().toISOString();
|
|
74
75
|
const merged = {
|
|
75
76
|
condition: parsed.condition,
|
|
76
77
|
cadence: parsed.cadence,
|
|
@@ -81,9 +82,41 @@ async function archiveAndAlertExpiredAwait(options) {
|
|
|
81
82
|
created_at: parsed.created_at,
|
|
82
83
|
filed_from: parsed.filed_from,
|
|
83
84
|
filed_for_friend_id: parsed.filed_for_friend_id,
|
|
85
|
+
filed_from_key: parsed.filed_from_key,
|
|
86
|
+
request_id: parsed.request_id,
|
|
87
|
+
obligation_id: parsed.obligation_id,
|
|
84
88
|
expired_at: expiredAt,
|
|
85
89
|
last_observation_at_expiry: lastObservation,
|
|
86
90
|
};
|
|
91
|
+
// A request-bound return must remain live through effect authorization and
|
|
92
|
+
// acceptance. Freeze its expiry payload in the still-pending await so a
|
|
93
|
+
// crash after Telegram acceptance retries the exact same effect.
|
|
94
|
+
if (parsed.request_id) {
|
|
95
|
+
const staged = { ...merged, status: "pending" };
|
|
96
|
+
const temporary = `${source}.${process.pid}.expiry.tmp`;
|
|
97
|
+
try {
|
|
98
|
+
fs.writeFileSync(temporary, (0, await_parser_1.renderAwaitFile)(staged, parsed.body), { encoding: "utf-8", mode: 0o600, flag: "wx" });
|
|
99
|
+
fs.renameSync(temporary, source);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
try {
|
|
103
|
+
fs.unlinkSync(temporary);
|
|
104
|
+
}
|
|
105
|
+
catch { /* no staged residue to remove */ }
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
const alertResult = await (0, await_alert_1.deliverAwaitAlert)({
|
|
109
|
+
awaitFile: (0, await_parser_1.parseAwaitFile)(fs.readFileSync(source, "utf-8"), source),
|
|
110
|
+
reason: "expired",
|
|
111
|
+
observation: lastObservation,
|
|
112
|
+
agentRoot: options.agentRoot,
|
|
113
|
+
agentName: options.agentName,
|
|
114
|
+
deliveryDeps: options.deliveryDeps,
|
|
115
|
+
});
|
|
116
|
+
if (alertResult.delivery?.status !== "delivered_now") {
|
|
117
|
+
return { archived: false, alerted: alertResult.attempted, reason: alertResult.delivery?.status ?? alertResult.skipped ?? "delivery not accepted" };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
87
120
|
fs.mkdirSync(doneDir, { recursive: true });
|
|
88
121
|
fs.writeFileSync(target, (0, await_parser_1.renderAwaitFile)(merged, parsed.body), "utf-8");
|
|
89
122
|
fs.unlinkSync(source);
|
|
@@ -96,7 +129,9 @@ async function archiveAndAlertExpiredAwait(options) {
|
|
|
96
129
|
// Re-parse archived file to feed the alert with merged fields
|
|
97
130
|
const archivedContent = fs.readFileSync(target, "utf-8");
|
|
98
131
|
const archived = (0, await_parser_1.parseAwaitFile)(archivedContent, target);
|
|
99
|
-
|
|
132
|
+
if (archived.obligation_id)
|
|
133
|
+
(0, obligations_1.fulfillObligation)(options.agentRoot, archived.obligation_id);
|
|
134
|
+
const alertResult = parsed.request_id ? null : await (0, await_alert_1.deliverAwaitAlert)({
|
|
100
135
|
awaitFile: archived,
|
|
101
136
|
reason: "expired",
|
|
102
137
|
observation: lastObservation,
|
|
@@ -104,5 +139,5 @@ async function archiveAndAlertExpiredAwait(options) {
|
|
|
104
139
|
agentName: options.agentName,
|
|
105
140
|
deliveryDeps: options.deliveryDeps,
|
|
106
141
|
});
|
|
107
|
-
return { archived: true, alerted: alertResult
|
|
142
|
+
return { archived: true, alerted: parsed.request_id ? true : Boolean(alertResult?.attempted) };
|
|
108
143
|
}
|
|
@@ -66,10 +66,14 @@ function emptyAwait(stem, body) {
|
|
|
66
66
|
alert: null,
|
|
67
67
|
mode: "full",
|
|
68
68
|
max_age: null,
|
|
69
|
+
wake_at: null,
|
|
69
70
|
status: "pending",
|
|
70
71
|
created_at: null,
|
|
71
72
|
filed_from: null,
|
|
72
73
|
filed_for_friend_id: null,
|
|
74
|
+
filed_from_key: null,
|
|
75
|
+
request_id: null,
|
|
76
|
+
obligation_id: null,
|
|
73
77
|
body,
|
|
74
78
|
resolved_at: null,
|
|
75
79
|
resolution_observation: null,
|
|
@@ -102,10 +106,14 @@ function parseAwaitFile(content, filePath) {
|
|
|
102
106
|
alert: nonEmptyString(frontmatter.alert),
|
|
103
107
|
mode,
|
|
104
108
|
max_age: nonEmptyString(frontmatter.max_age),
|
|
109
|
+
wake_at: nonEmptyString(frontmatter.wake_at),
|
|
105
110
|
status,
|
|
106
111
|
created_at: nonEmptyString(frontmatter.created_at),
|
|
107
112
|
filed_from: nonEmptyString(frontmatter.filed_from),
|
|
108
113
|
filed_for_friend_id: nonEmptyString(frontmatter.filed_for_friend_id),
|
|
114
|
+
filed_from_key: nonEmptyString(frontmatter.filed_from_key),
|
|
115
|
+
request_id: nonEmptyString(frontmatter.request_id),
|
|
116
|
+
obligation_id: nonEmptyString(frontmatter.obligation_id),
|
|
109
117
|
body,
|
|
110
118
|
resolved_at: nonEmptyString(frontmatter.resolved_at),
|
|
111
119
|
resolution_observation: nonEmptyString(frontmatter.resolution_observation),
|
|
@@ -81,8 +81,26 @@ function currentAskCheck(card) {
|
|
|
81
81
|
evidence,
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
|
-
function
|
|
84
|
+
function isSettledTurnWait(resume) {
|
|
85
|
+
return resume.hasCompleteState
|
|
86
|
+
&& resume.recorderHealth.status === "ok"
|
|
87
|
+
&& resume.blockedBecause.length === 1
|
|
88
|
+
&& resume.blockedBecause[0] === "turn outcome settled; wait for new input before acting"
|
|
89
|
+
&& resume.nextSafeAction.value === "inspect the latest session and wait for new input before acting";
|
|
90
|
+
}
|
|
91
|
+
function nextSafeActionCheck(card, resume) {
|
|
85
92
|
const evidence = card.nextAction.source ? [card.nextAction.source] : [];
|
|
93
|
+
if (isSettledTurnWait(resume)) {
|
|
94
|
+
return makeCheck({
|
|
95
|
+
id: "next_safe_action",
|
|
96
|
+
label: "Next safe action",
|
|
97
|
+
status: "pass",
|
|
98
|
+
score: 20,
|
|
99
|
+
maxScore: 20,
|
|
100
|
+
detail: "idle: turn is settled and waiting for new input.",
|
|
101
|
+
evidence: flightRecorderEvidence(card),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
86
104
|
if (card.nextAction.actor === "unknown") {
|
|
87
105
|
return makeCheck({
|
|
88
106
|
id: "next_safe_action",
|
|
@@ -117,6 +135,17 @@ function staleGuardCheck(resume, card) {
|
|
|
117
135
|
evidence,
|
|
118
136
|
});
|
|
119
137
|
}
|
|
138
|
+
if (isSettledTurnWait(resume)) {
|
|
139
|
+
return makeCheck({
|
|
140
|
+
id: "stale_guard",
|
|
141
|
+
label: "Stale-state guard",
|
|
142
|
+
status: "pass",
|
|
143
|
+
score: 15,
|
|
144
|
+
maxScore: 15,
|
|
145
|
+
detail: "Flight recorder has complete settled-turn state and is safely waiting for new input.",
|
|
146
|
+
evidence,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
120
149
|
const reasons = [
|
|
121
150
|
"canContinue is false",
|
|
122
151
|
...(!resume.hasCompleteState ? ["incomplete resume state"] : []),
|
|
@@ -287,7 +316,7 @@ function runContextLossGauntlet(agentName, agentRoot, options = {}) {
|
|
|
287
316
|
const generatedAt = (options.now ?? (() => new Date()))().toISOString();
|
|
288
317
|
const checks = [
|
|
289
318
|
currentAskCheck(card),
|
|
290
|
-
nextSafeActionCheck(card),
|
|
319
|
+
nextSafeActionCheck(card, flightRecorderResume),
|
|
291
320
|
staleGuardCheck(flightRecorderResume, card),
|
|
292
321
|
obligationsCheck(card),
|
|
293
322
|
returnRoutesCheck(card),
|
|
@@ -521,6 +521,8 @@ function healthSignals(results) {
|
|
|
521
521
|
}
|
|
522
522
|
function defaultGitStatus(agentRoot) {
|
|
523
523
|
try {
|
|
524
|
+
if (!fs.existsSync(path.join(agentRoot, ".git")))
|
|
525
|
+
return { ok: true, porcelain: "" };
|
|
524
526
|
const porcelain = (0, child_process_1.execFileSync)("git", ["status", "--porcelain=v1", "-uall"], {
|
|
525
527
|
cwd: agentRoot,
|
|
526
528
|
encoding: "utf-8",
|
package/dist/heart/core.js
CHANGED
|
@@ -962,6 +962,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
962
962
|
try {
|
|
963
963
|
const buildSystemOptions = {
|
|
964
964
|
...options,
|
|
965
|
+
relationshipContextScopes: options?.toolContext?.relationshipAuthorization?.authorizedContextScopes,
|
|
966
|
+
relationshipProfileId: options?.toolContext?.relationshipAuthorization?.profileId,
|
|
967
|
+
relationshipToolNames: options?.toolContext?.relationshipAuthorization?.advertisedToolNames,
|
|
965
968
|
orientationFrame: turnOrientationFrame,
|
|
966
969
|
resumePriorWork: options?.resumePriorWork === true,
|
|
967
970
|
providerCapabilities: providerRuntime.capabilities,
|
|
@@ -1083,7 +1086,11 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1083
1086
|
catch { /* unsupported */ }
|
|
1084
1087
|
const toolPreferences = currentContext?.friend?.toolPreferences;
|
|
1085
1088
|
const unboundBaseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, friends_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined, currentContext, providerRuntime.capabilities, options?.mcpManager, providerRuntime.model);
|
|
1086
|
-
const
|
|
1089
|
+
const relationshipToolNames = options?.toolContext?.relationshipAuthorization?.advertisedToolNames;
|
|
1090
|
+
const relationshipScopedTools = relationshipToolNames
|
|
1091
|
+
? unboundBaseTools.filter((tool) => relationshipToolNames.includes(tool.function.name))
|
|
1092
|
+
: unboundBaseTools;
|
|
1093
|
+
const baseTools = bindCurrentIngressEvidenceLocator(relationshipScopedTools, options?.toolContext?.currentIngressEvidence);
|
|
1087
1094
|
// Augment tool context with reasoning effort controls from provider
|
|
1088
1095
|
const baseToolContext = options?.toolContext
|
|
1089
1096
|
?? (turnOrientationFrame ? { signin: async () => undefined, orientationFrame: turnOrientationFrame } : undefined);
|
|
@@ -1127,7 +1134,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1127
1134
|
const filteredBaseTools = isPrivateRuntimeChannel
|
|
1128
1135
|
? baseTools.filter((t) => privateRuntimeHabitCanSendMessage || t.function.name !== "send_message")
|
|
1129
1136
|
: baseTools;
|
|
1130
|
-
const
|
|
1137
|
+
const unscopedOrdinaryActiveTools = [
|
|
1131
1138
|
...filteredBaseTools,
|
|
1132
1139
|
...(augmentedToolContext?.noSend === true ? [] : [tools_1.ponderTool]),
|
|
1133
1140
|
...(isPrivateRuntimeChannel && privateRuntimeHabitCanSurface ? [tools_2.surfaceToolDef] : []),
|
|
@@ -1136,6 +1143,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1136
1143
|
...(!isPrivateRuntimeChannel ? [tools_1.settleTool] : []),
|
|
1137
1144
|
...(isChatStyleChannel(channel ?? "") ? [tools_1.speakTool] : []),
|
|
1138
1145
|
];
|
|
1146
|
+
const ordinaryActiveTools = relationshipToolNames
|
|
1147
|
+
? unscopedOrdinaryActiveTools.filter((tool) => relationshipToolNames.includes(tool.function.name))
|
|
1148
|
+
: unscopedOrdinaryActiveTools;
|
|
1139
1149
|
const activeTools = options?.toolProfile === "sanctuary-health-private"
|
|
1140
1150
|
? (() => {
|
|
1141
1151
|
const sendTools = baseTools.filter((tool) => tool.function.name === "send_message");
|
|
@@ -1740,10 +1750,13 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1740
1750
|
done = true;
|
|
1741
1751
|
continue;
|
|
1742
1752
|
}
|
|
1743
|
-
const approvalCalls =
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1753
|
+
const approvalCalls = await Promise.all(validCalls.map(async (entry) => {
|
|
1754
|
+
const classification = await (0, tools_1.classifyApprovalForInvocation)(entry.call.name, entry.validated.arguments, augmentedToolContext);
|
|
1755
|
+
return {
|
|
1756
|
+
...entry,
|
|
1757
|
+
...classification,
|
|
1758
|
+
};
|
|
1759
|
+
}));
|
|
1747
1760
|
const protectedCall = approvalCalls.find((entry) => entry.policy.kind === "required");
|
|
1748
1761
|
if (protectedCall && result.toolCalls.length !== 1) {
|
|
1749
1762
|
streamCallbackBuffer?.discard();
|
|
@@ -1763,6 +1776,21 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1763
1776
|
continue;
|
|
1764
1777
|
}
|
|
1765
1778
|
if (protectedCall && protectedCall.policy.kind === "required") {
|
|
1779
|
+
if (!options?.approvalCoordinator) {
|
|
1780
|
+
streamCallbackBuffer?.discard();
|
|
1781
|
+
pushGenerated(msg);
|
|
1782
|
+
const rejection = "rejected: this protected tool requires approval, but the approval coordinator is unavailable; the handler was not invoked.";
|
|
1783
|
+
pushGenerated({ role: "tool", tool_call_id: protectedCall.call.id, content: rejection });
|
|
1784
|
+
providerRuntime.appendToolOutput(protectedCall.call.id, rejection);
|
|
1785
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1786
|
+
level: "warn",
|
|
1787
|
+
component: "engine",
|
|
1788
|
+
event: "engine.approval_coordinator_unavailable",
|
|
1789
|
+
message: "protected tool failed closed before handler execution",
|
|
1790
|
+
meta: { toolName: protectedCall.call.name, toolCallId: protectedCall.call.id },
|
|
1791
|
+
});
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1766
1794
|
streamCallbackBuffer?.discard();
|
|
1767
1795
|
pushGenerated(msg);
|
|
1768
1796
|
const toolDigest = (0, tool_arguments_1.digestJson)({
|
|
@@ -2078,7 +2106,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
2078
2106
|
let success;
|
|
2079
2107
|
try {
|
|
2080
2108
|
const execToolFn = options?.execTool ?? tools_1.execTool;
|
|
2081
|
-
|
|
2109
|
+
const routineActionSelection = approvalCalls.find((entry) => entry.call.id === tc.id)?.routineActionSelection;
|
|
2110
|
+
const executionToolContext = routineActionSelection && augmentedToolContext ? { ...augmentedToolContext, routineActionSelection } : augmentedToolContext;
|
|
2111
|
+
toolResult = await execToolFn(tc.name, args, executionToolContext);
|
|
2082
2112
|
success = true;
|
|
2083
2113
|
}
|
|
2084
2114
|
catch (e) {
|
|
@@ -72,7 +72,7 @@ function runContainerHealthcheck(options = {}) {
|
|
|
72
72
|
fail("daemon status is not ready");
|
|
73
73
|
const agents = health.agents;
|
|
74
74
|
const managed = agents?.[agent];
|
|
75
|
-
if (!managed || managed.status !== "running" || typeof managed.pid !== "number" || managed.pid <= 0
|
|
75
|
+
if (!managed || managed.status !== "running" || typeof managed.pid !== "number" || managed.pid <= 0)
|
|
76
76
|
fail("managed agent is not running");
|
|
77
77
|
let processArguments = "";
|
|
78
78
|
try {
|
|
@@ -81,6 +81,8 @@ function runContainerHealthcheck(options = {}) {
|
|
|
81
81
|
catch {
|
|
82
82
|
fail("managed process inventory is unavailable");
|
|
83
83
|
}
|
|
84
|
+
if (!(0, container_runtime_1.hasManagedAgentProcess)(processArguments, agent))
|
|
85
|
+
fail("managed agent is not running exactly once");
|
|
84
86
|
if (!(0, container_runtime_1.hasManagedTelegramProcess)(processArguments, agent))
|
|
85
87
|
fail("managed Telegram sense is not running exactly once");
|
|
86
88
|
if (!(0, container_runtime_1.hasManagedSupercronicProcess)(processArguments, agent))
|
|
@@ -33,11 +33,24 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.hasManagedAgentProcess = hasManagedAgentProcess;
|
|
36
37
|
exports.hasManagedTelegramProcess = hasManagedTelegramProcess;
|
|
37
38
|
exports.hasManagedSupercronicProcess = hasManagedSupercronicProcess;
|
|
38
39
|
exports.readContainerRuntimePolicy = readContainerRuntimePolicy;
|
|
39
40
|
const fs = __importStar(require("node:fs"));
|
|
40
41
|
const runtime_1 = require("../../nerves/runtime");
|
|
42
|
+
function hasManagedAgentProcess(processArguments, agent) {
|
|
43
|
+
const wanted = agent.trim();
|
|
44
|
+
if (!wanted)
|
|
45
|
+
return false;
|
|
46
|
+
const matches = processArguments.split("\n").filter((line) => {
|
|
47
|
+
const fields = line.trim().split(/\s+/u);
|
|
48
|
+
const entryIndex = fields.findIndex((field) => field === "/opt/ouro/dist/heart/agent-entry.js");
|
|
49
|
+
const agentIndex = fields.findIndex((field) => field === "--agent");
|
|
50
|
+
return entryIndex >= 0 && agentIndex >= 0 && fields[agentIndex + 1] === wanted;
|
|
51
|
+
});
|
|
52
|
+
return matches.length === 1;
|
|
53
|
+
}
|
|
41
54
|
function hasManagedTelegramProcess(processArguments, agent) {
|
|
42
55
|
const wanted = agent.trim();
|
|
43
56
|
if (!wanted)
|
|
@@ -71,8 +71,11 @@ function runContainerSpecAuditorCli(args, deps = {}) {
|
|
|
71
71
|
const write = deps.write ?? ((text) => process.stdout.write(text));
|
|
72
72
|
const staged = parseModeArguments(args, ["--template", "--runtime-policy", "--expected-image"]);
|
|
73
73
|
const effective = parseModeArguments(args, ["--inspect", "--image-inspect", "--expected-image"]);
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const legacyCandidate = parseModeArguments(args, ["--inspect", "--image-inspect", "--expected-image", "--mount-contract"]);
|
|
75
|
+
const legacyEffective = legacyCandidate?.["--mount-contract"] === "legacy-alpha742" ? legacyCandidate : null;
|
|
76
|
+
const selectedEffective = effective ?? legacyEffective;
|
|
77
|
+
if (!staged && !selectedEffective) {
|
|
78
|
+
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> [--mount-contract legacy-alpha742]" }) + "\n");
|
|
76
79
|
(0, runtime_1.emitNervesEvent)({
|
|
77
80
|
level: "error",
|
|
78
81
|
component: "daemon",
|
|
@@ -82,12 +85,12 @@ function runContainerSpecAuditorCli(args, deps = {}) {
|
|
|
82
85
|
});
|
|
83
86
|
return 2;
|
|
84
87
|
}
|
|
85
|
-
if (
|
|
88
|
+
if (selectedEffective) {
|
|
86
89
|
let containerText;
|
|
87
90
|
let imageText;
|
|
88
91
|
try {
|
|
89
|
-
containerText = readFile(
|
|
90
|
-
imageText = readFile(
|
|
92
|
+
containerText = readFile(selectedEffective["--inspect"]);
|
|
93
|
+
imageText = readFile(selectedEffective["--image-inspect"]);
|
|
91
94
|
}
|
|
92
95
|
catch (error) {
|
|
93
96
|
write(JSON.stringify({ ok: false, error: "effective audit inputs are unreadable" }) + "\n");
|
|
@@ -118,10 +121,11 @@ function runContainerSpecAuditorCli(args, deps = {}) {
|
|
|
118
121
|
return 2;
|
|
119
122
|
}
|
|
120
123
|
const result = (0, container_spec_auditor_1.auditSanctuaryContainerSpec)(containerInspect, {
|
|
121
|
-
expectedImage:
|
|
124
|
+
expectedImage: selectedEffective["--expected-image"],
|
|
122
125
|
expectedEnvironment,
|
|
126
|
+
mountContract: legacyEffective ? "legacy-alpha742" : "canonical",
|
|
123
127
|
});
|
|
124
|
-
if (imageInspect.Id !==
|
|
128
|
+
if (imageInspect.Id !== selectedEffective["--expected-image"]) {
|
|
125
129
|
result.ok = false;
|
|
126
130
|
result.violations.unshift("reviewed image inspect identity does not match the expected local image ID");
|
|
127
131
|
}
|
|
@@ -6,11 +6,18 @@ const runtime_1 = require("../../nerves/runtime");
|
|
|
6
6
|
const EXPECTED_BINDS = [
|
|
7
7
|
"/mnt/user/appdata/ouro-butler/runtime/.ouro-cli:/home/ouro/.ouro-cli:rw",
|
|
8
8
|
"/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro:/home/ouro/AgentBundles/sanctuary.ouro:rw",
|
|
9
|
+
"/boot/config/custom/ouro-events/spool:/run/ouro-events:ro",
|
|
10
|
+
"/mnt/user/appdata/sabnzbd/sabnzbd.ini:/run/sanctuary/sabnzbd.ini:ro",
|
|
9
11
|
];
|
|
10
12
|
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
|
+
["/mnt/user/appdata/ouro-butler/runtime/.ouro-cli", "/home/ouro/.ouro-cli", true],
|
|
14
|
+
["/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro", "/home/ouro/AgentBundles/sanctuary.ouro", true],
|
|
15
|
+
["/boot/config/custom/ouro-events/spool", "/run/ouro-events", false],
|
|
16
|
+
["/mnt/user/appdata/sabnzbd/sabnzbd.ini", "/run/sanctuary/sabnzbd.ini", false],
|
|
13
17
|
];
|
|
18
|
+
const LEGACY_ALPHA742_IMAGE = "sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d";
|
|
19
|
+
const LEGACY_ALPHA742_BINDS = EXPECTED_BINDS.slice(0, 2);
|
|
20
|
+
const LEGACY_ALPHA742_MOUNTS = EXPECTED_MOUNTS.slice(0, 2);
|
|
14
21
|
const EXPECTED_EXTRA_PARAMS = "--restart=unless-stopped --user=10001:10001";
|
|
15
22
|
function record(value) {
|
|
16
23
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -29,12 +36,18 @@ function auditSanctuaryContainerSpec(value, options) {
|
|
|
29
36
|
const root = record(value);
|
|
30
37
|
const config = record(root?.Config);
|
|
31
38
|
const host = record(root?.HostConfig);
|
|
39
|
+
const network = record(root?.NetworkSettings);
|
|
32
40
|
if (!root || !config || !host) {
|
|
33
41
|
violations.push("inspect payload must contain object Config and HostConfig records");
|
|
34
42
|
}
|
|
35
43
|
else {
|
|
36
44
|
if (!/^sha256:[a-f0-9]{64}$/u.test(options.expectedImage))
|
|
37
45
|
violations.push("expected image must be an exact local Docker image ID");
|
|
46
|
+
const mountContract = options.mountContract ?? "canonical";
|
|
47
|
+
if (mountContract === "legacy-alpha742" && options.expectedImage !== LEGACY_ALPHA742_IMAGE)
|
|
48
|
+
violations.push("legacy mount exception requires the pinned alpha.742 image ID");
|
|
49
|
+
const expectedBinds = mountContract === "legacy-alpha742" ? LEGACY_ALPHA742_BINDS : EXPECTED_BINDS;
|
|
50
|
+
const expectedMounts = mountContract === "legacy-alpha742" ? LEGACY_ALPHA742_MOUNTS : EXPECTED_MOUNTS;
|
|
38
51
|
if (config.Image !== options.expectedImage)
|
|
39
52
|
violations.push("image does not match the reviewed exact local Docker image ID");
|
|
40
53
|
if (root.Path !== "node")
|
|
@@ -54,8 +67,16 @@ function auditSanctuaryContainerSpec(value, options) {
|
|
|
54
67
|
violations.push("container must expose no ports");
|
|
55
68
|
if (host.NetworkMode !== "host")
|
|
56
69
|
violations.push("network mode must be host");
|
|
70
|
+
if (host.PidMode !== "")
|
|
71
|
+
violations.push("PID namespace must be private");
|
|
72
|
+
if (host.IpcMode !== "private")
|
|
73
|
+
violations.push("IPC namespace must be private");
|
|
57
74
|
if (host.Privileged !== false)
|
|
58
75
|
violations.push("container must not be privileged");
|
|
76
|
+
if (host.ReadonlyRootfs !== false)
|
|
77
|
+
violations.push("root filesystem mode must match the reviewed runtime");
|
|
78
|
+
if (!(host.SecurityOpt === null || (Array.isArray(host.SecurityOpt) && host.SecurityOpt.length === 0)))
|
|
79
|
+
violations.push("container must set no security options");
|
|
59
80
|
const restart = record(host.RestartPolicy);
|
|
60
81
|
if (restart?.Name !== "unless-stopped" || restart.MaximumRetryCount !== 0)
|
|
61
82
|
violations.push("restart policy must be unless-stopped");
|
|
@@ -65,19 +86,25 @@ function auditSanctuaryContainerSpec(value, options) {
|
|
|
65
86
|
violations.push("container must have no devices");
|
|
66
87
|
if (!(host.CapAdd === null || (Array.isArray(host.CapAdd) && host.CapAdd.length === 0)))
|
|
67
88
|
violations.push("container must add no capabilities");
|
|
89
|
+
if (!(host.CapDrop === null || (Array.isArray(host.CapDrop) && host.CapDrop.length === 0)))
|
|
90
|
+
violations.push("container must drop no capabilities");
|
|
91
|
+
if (host.PublishAllPorts !== false)
|
|
92
|
+
violations.push("container must not publish all exposed ports");
|
|
93
|
+
if (!isEmptyRecord(network?.Ports))
|
|
94
|
+
violations.push("effective network ports must be empty");
|
|
68
95
|
const binds = stringArray(host.Binds);
|
|
69
|
-
if (!binds || JSON.stringify([...binds].sort()) !== JSON.stringify([...
|
|
70
|
-
violations.push("bind set
|
|
96
|
+
if (!binds || JSON.stringify([...binds].sort()) !== JSON.stringify([...expectedBinds].sort()))
|
|
97
|
+
violations.push("bind set does not match the selected Sanctuary mount contract");
|
|
71
98
|
const mounts = Array.isArray(root.Mounts) ? root.Mounts : [];
|
|
72
99
|
const normalizedMounts = mounts.map((mount) => {
|
|
73
100
|
const item = record(mount);
|
|
74
|
-
return item && item.Type === "bind" && item.RW ===
|
|
75
|
-
? [item.Source, item.Destination]
|
|
101
|
+
return item && item.Type === "bind" && typeof item.RW === "boolean" && typeof item.Source === "string" && typeof item.Destination === "string"
|
|
102
|
+
? [item.Source, item.Destination, item.RW]
|
|
76
103
|
: null;
|
|
77
104
|
});
|
|
78
105
|
if (normalizedMounts.some((mount) => mount === null)
|
|
79
|
-
|| JSON.stringify(normalizedMounts.sort()) !== JSON.stringify(
|
|
80
|
-
violations.push("effective mounts
|
|
106
|
+
|| JSON.stringify(normalizedMounts.sort()) !== JSON.stringify(expectedMounts.map((mount) => [...mount]).sort())) {
|
|
107
|
+
violations.push("effective mounts do not match the selected Sanctuary mount contract");
|
|
81
108
|
}
|
|
82
109
|
}
|
|
83
110
|
const result = { ok: violations.length === 0, violations };
|
|
@@ -130,10 +157,10 @@ function auditSanctuaryStagedFiles(input) {
|
|
|
130
157
|
const mode = match[1].match(/\bMode="([^"]+)"/u)?.[1];
|
|
131
158
|
return type === "Path" && target && mode ? `${match[2]}:${target}:${mode}` : "invalid";
|
|
132
159
|
});
|
|
133
|
-
if (configOpenCount !==
|
|
134
|
-
|| configEntries.length !==
|
|
160
|
+
if (configOpenCount !== 4
|
|
161
|
+
|| configEntries.length !== 4
|
|
135
162
|
|| JSON.stringify([...pathConfigs].sort()) !== JSON.stringify([...EXPECTED_BINDS].sort())) {
|
|
136
|
-
violations.push("template Config entries must equal the canonical
|
|
163
|
+
violations.push("template Config entries must equal the canonical four path binds");
|
|
137
164
|
}
|
|
138
165
|
const postArgsOpenCount = [...input.templateXml.matchAll(/<PostArgs\b/gu)].length;
|
|
139
166
|
const postArgs = [...input.templateXml.matchAll(/<PostArgs(?:(?:\s*\/>)|>([^<]*)<\/PostArgs>)/gu)];
|
|
@@ -412,12 +412,20 @@ const daemon = new daemon_1.OuroDaemon({
|
|
|
412
412
|
const before = schedulerScenario ? (0, sanctuary_scheduler_liveness_1.readSanctuaryHealthCursor)(agentRoot) : null;
|
|
413
413
|
let providerInvocationCount = 0;
|
|
414
414
|
let privateTurnCount = 0;
|
|
415
|
-
const result = await (0, sanctuary_health_runner_1.runSanctuaryHealthHabit)(agent,
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
415
|
+
const result = await (0, sanctuary_health_runner_1.runSanctuaryHealthHabit)(agent, {
|
|
416
|
+
submitEvidence: async (input) => {
|
|
417
|
+
const response = await daemon.handleCommand({ kind: "external.event.submit", ...input });
|
|
418
|
+
if (!response.ok)
|
|
419
|
+
throw new Error(response.error ?? "Sanctuary health evidence submission failed");
|
|
420
|
+
return response.data.event;
|
|
419
421
|
},
|
|
420
|
-
|
|
422
|
+
...(schedulerScenario ? {
|
|
423
|
+
acceptanceMetrics: {
|
|
424
|
+
onPrivateTurnStart: () => { privateTurnCount += 1; },
|
|
425
|
+
onProviderInvocation: () => { providerInvocationCount += 1; },
|
|
426
|
+
},
|
|
427
|
+
} : {}),
|
|
428
|
+
});
|
|
421
429
|
if (schedulerScenario) {
|
|
422
430
|
if (!supercronicSupervisor || !before || !occurrenceId || !schedulerOrigin)
|
|
423
431
|
throw new Error("Sanctuary scheduler liveness supervisor provenance is unavailable");
|
|
@@ -834,6 +842,12 @@ void (0, daemon_bootstrap_startup_1.startDaemonAfterContainerCredentialBootstrap
|
|
|
834
842
|
awaitName,
|
|
835
843
|
deliveryDeps: {
|
|
836
844
|
agentName: agent,
|
|
845
|
+
deliverers: {
|
|
846
|
+
telegram: async (request) => {
|
|
847
|
+
const { sendTelegramAwaitFollowUp } = await Promise.resolve().then(() => __importStar(require("../../senses/telegram")));
|
|
848
|
+
return sendTelegramAwaitFollowUp(agent, request);
|
|
849
|
+
},
|
|
850
|
+
},
|
|
837
851
|
queuePending: () => {
|
|
838
852
|
// Best-effort: queue private-runtime wake so the agent processes the alert path.
|
|
839
853
|
(0, socket_client_1.sendDaemonCommand)(socketPath, (0, await_private_wake_1.buildAwaitPrivateWakeCommand)({
|