@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.
Files changed (81) hide show
  1. package/changelog.json +42 -0
  2. package/deploy/unraid/Dockerfile +7 -2
  3. package/deploy/unraid/README.txt +717 -106
  4. package/deploy/unraid/migrate-sanctuary-bundle.mjs +21 -0
  5. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  6. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  7. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  8. package/deploy/unraid/ouro-events/install-usenet-guard.sh +434 -0
  9. package/deploy/unraid/ouro-events/usenet-health.sh +178 -0
  10. package/deploy/unraid/sanctuary-acceptance-contract.json +4 -3
  11. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +5 -1
  12. package/deploy/unraid/sanctuary.ouro/agent.json +2 -2
  13. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +2 -2
  14. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +1 -1
  15. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +3 -10
  16. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -1
  17. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -1
  18. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +4 -0
  19. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +6 -2
  20. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +4 -2
  21. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  22. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +19 -17
  23. package/deploy/unraid/sanctuary.xml +12 -9
  24. package/dist/arc/cares.js +171 -37
  25. package/dist/arc/obligations.js +38 -0
  26. package/dist/heart/awaiting/await-alert.js +4 -2
  27. package/dist/heart/awaiting/await-expiry.js +39 -4
  28. package/dist/heart/awaiting/await-parser.js +8 -0
  29. package/dist/heart/context-loss-gauntlet.js +31 -2
  30. package/dist/heart/context-loss-sentinel.js +2 -0
  31. package/dist/heart/core.js +37 -7
  32. package/dist/heart/cross-chat-delivery.js +1 -0
  33. package/dist/heart/daemon/container-healthcheck.js +3 -1
  34. package/dist/heart/daemon/container-runtime.js +13 -0
  35. package/dist/heart/daemon/container-spec-auditor-main.js +11 -7
  36. package/dist/heart/daemon/container-spec-auditor.js +38 -11
  37. package/dist/heart/daemon/daemon-entry.js +19 -5
  38. package/dist/heart/daemon/daemon.js +154 -22
  39. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +151 -87
  40. package/dist/heart/daemon/sanctuary-acceptance-harness.js +26 -29
  41. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +42 -51
  42. package/dist/heart/daemon/sanctuary-bundle-migration.js +398 -0
  43. package/dist/heart/daemon/sanctuary-scheduler-origin.js +11 -5
  44. package/dist/heart/daemon/supercronic-supervisor.js +16 -1
  45. package/dist/heart/external-events/router.js +1025 -43
  46. package/dist/heart/provider-models.js +10 -3
  47. package/dist/heart/session-events.js +23 -5
  48. package/dist/heart/steward-policy.js +302 -0
  49. package/dist/heart/tool-approval.js +3 -1
  50. package/dist/heart/turn-context.js +1 -0
  51. package/dist/mind/context.js +8 -3
  52. package/dist/mind/pending.js +25 -0
  53. package/dist/mind/prompt.js +49 -1
  54. package/dist/repertoire/guardrails.js +2 -1
  55. package/dist/repertoire/relationship-authorization.js +194 -0
  56. package/dist/repertoire/tools-awaiting.js +229 -26
  57. package/dist/repertoire/tools-base.js +15 -0
  58. package/dist/repertoire/tools-continuity.js +259 -4
  59. package/dist/repertoire/tools-notes.js +29 -3
  60. package/dist/repertoire/tools-session.js +219 -4
  61. package/dist/repertoire/tools-steward-policy.js +88 -0
  62. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  63. package/dist/repertoire/tools-unraid.js +113 -6
  64. package/dist/repertoire/tools.js +104 -9
  65. package/dist/repertoire/unraid-restart.js +86 -4
  66. package/dist/senses/private-runtime-worker.js +36 -3
  67. package/dist/senses/private-runtime.js +310 -63
  68. package/dist/senses/sanctuary-health-acceptance-probe.js +8 -6
  69. package/dist/senses/sanctuary-health-runner.js +51 -96
  70. package/dist/senses/sanctuary-health.js +54 -93
  71. package/dist/senses/sanctuary-runtime.js +36 -2
  72. package/dist/senses/sanctuary-sab.js +80 -0
  73. package/dist/senses/shared-turn.js +64 -9
  74. package/dist/senses/telegram-admission.js +684 -0
  75. package/dist/senses/telegram-approval-runtime.js +46 -6
  76. package/dist/senses/telegram-client.js +236 -72
  77. package/dist/senses/telegram-effect-adapter.js +787 -0
  78. package/dist/senses/telegram-entry.js +4 -1
  79. package/dist/senses/telegram.js +728 -48
  80. package/npm-shrinkwrap.json +6 -6
  81. 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
- const alertResult = await (0, await_alert_1.deliverAwaitAlert)({
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.attempted };
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 nextSafeActionCheck(card) {
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",
@@ -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 baseTools = bindCurrentIngressEvidenceLocator(unboundBaseTools, options?.toolContext?.currentIngressEvidence);
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 ordinaryActiveTools = [
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 = options?.approvalCoordinator ? validCalls.map((entry) => ({
1744
- ...entry,
1745
- policy: (0, tools_1.approvalPolicyForToolName)(entry.call.name, entry.validated.arguments),
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
- toolResult = await execToolFn(tc.name, args, augmentedToolContext);
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) {
@@ -10,6 +10,7 @@ function buildPendingEnvelope(request, agentName, now) {
10
10
  channel: request.channel,
11
11
  key: request.key,
12
12
  content: request.content,
13
+ ...(request.requestId ? { requestId: request.requestId } : {}),
13
14
  timestamp: now,
14
15
  };
15
16
  }
@@ -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 || !fs.existsSync(`/proc/${managed.pid}`))
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
- 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");
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 (effective) {
88
+ if (selectedEffective) {
86
89
  let containerText;
87
90
  let imageText;
88
91
  try {
89
- containerText = readFile(effective["--inspect"]);
90
- imageText = readFile(effective["--image-inspect"]);
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: effective["--expected-image"],
124
+ expectedImage: selectedEffective["--expected-image"],
122
125
  expectedEnvironment,
126
+ mountContract: legacyEffective ? "legacy-alpha742" : "canonical",
123
127
  });
124
- if (imageInspect.Id !== effective["--expected-image"]) {
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([...EXPECTED_BINDS].sort()))
70
- violations.push("bind set must equal the canonical two writable roots");
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 === true && typeof item.Source === "string" && typeof item.Destination === "string"
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(EXPECTED_MOUNTS.map((mount) => [...mount]).sort())) {
80
- violations.push("effective mounts must equal the canonical two writable bind 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 !== 2
134
- || configEntries.length !== 2
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 two path binds");
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, schedulerScenario ? {
416
- acceptanceMetrics: {
417
- onPrivateTurnStart: () => { privateTurnCount += 1; },
418
- onProviderInvocation: () => { providerInvocationCount += 1; },
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)({