@ouro.bot/cli 0.1.0-alpha.742 → 0.1.0-alpha.744
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 +32 -0
- package/deploy/unraid/Dockerfile +7 -2
- package/deploy/unraid/README.txt +704 -64
- 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/bundle-meta.json +2 -2
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +1 -1
- 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 +13 -10
- 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/core.js +37 -7
- package/dist/heart/cross-chat-delivery.js +1 -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 +126 -42
- 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/external-events/router.js +1025 -43
- 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 +47 -103
- package/dist/senses/sanctuary-health.js +53 -105
- 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 +212 -66
- package/dist/senses/telegram-effect-adapter.js +787 -0
- package/dist/senses/telegram.js +728 -48
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
|
@@ -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),
|
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) {
|
|
@@ -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)({
|
|
@@ -615,10 +615,14 @@ class OuroDaemon {
|
|
|
615
615
|
mailboxServer = null;
|
|
616
616
|
socketIdentity = null;
|
|
617
617
|
senseAutostartTimer = null;
|
|
618
|
+
externalEventReconcileTimer = null;
|
|
619
|
+
externalEventReconcileRunning = false;
|
|
618
620
|
mailboxServerFactory;
|
|
619
621
|
privateRuntimePolicyDeps;
|
|
620
622
|
onStopCommandComplete;
|
|
621
623
|
externalEventRoot;
|
|
624
|
+
privilegedEventSpoolRoot;
|
|
625
|
+
privilegedEventScanner;
|
|
622
626
|
rsvpHabitRunner;
|
|
623
627
|
nativeHabitRunner;
|
|
624
628
|
nativeHabitMatch;
|
|
@@ -640,6 +644,8 @@ class OuroDaemon {
|
|
|
640
644
|
this.privateRuntimePolicyDeps = options.privateRuntimePolicyDeps ?? {};
|
|
641
645
|
this.onStopCommandComplete = options.onStopCommandComplete ?? null;
|
|
642
646
|
this.externalEventRoot = options.externalEventRoot ?? null;
|
|
647
|
+
this.privilegedEventSpoolRoot = options.privilegedEventSpoolRoot ?? "/run/ouro-events";
|
|
648
|
+
this.privilegedEventScanner = options.privilegedEventScanner ?? router_1.scanPrivilegedEventSpool;
|
|
643
649
|
this.rsvpHabitRunner = options.rsvpHabitRunner;
|
|
644
650
|
this.nativeHabitRunner = options.nativeHabitRunner;
|
|
645
651
|
this.nativeHabitMatch = options.nativeHabitMatch;
|
|
@@ -694,6 +700,7 @@ class OuroDaemon {
|
|
|
694
700
|
agentName,
|
|
695
701
|
agentRoot: path.join(this.bundlesRoot, `${agentName}.ouro`),
|
|
696
702
|
})));
|
|
703
|
+
const externalEvents = (0, router_1.listExternalEventStatus)(this.externalEventRoot ?? (0, router_1.getExternalEventRoot)());
|
|
697
704
|
const mailboxUrl = this.mailboxServer?.origin ?? "http://127.0.0.1:0";
|
|
698
705
|
return {
|
|
699
706
|
overview: {
|
|
@@ -714,6 +721,7 @@ class OuroDaemon {
|
|
|
714
721
|
sync,
|
|
715
722
|
agents,
|
|
716
723
|
...(providers.length > 0 ? { providers } : {}),
|
|
724
|
+
...(externalEvents.length > 0 ? { externalEvents } : {}),
|
|
717
725
|
};
|
|
718
726
|
}
|
|
719
727
|
overlaySchedulerHealth(healthChecks) {
|
|
@@ -801,6 +809,11 @@ class OuroDaemon {
|
|
|
801
809
|
// (daemon manages multiple agents; agent identity must be set before loading MCP config)
|
|
802
810
|
await this.orphanStartupDrain(this.socketPath);
|
|
803
811
|
await this.openCommandSocket();
|
|
812
|
+
await this.reconcileExternalEvents();
|
|
813
|
+
this.externalEventReconcileTimer = setInterval(() => {
|
|
814
|
+
void this.reconcileExternalEvents();
|
|
815
|
+
}, 1_000);
|
|
816
|
+
this.externalEventReconcileTimer.unref?.();
|
|
804
817
|
this.triggerAutoStartAgents();
|
|
805
818
|
this.triggerAutoStartSensesWhenAgentsSettled();
|
|
806
819
|
// Write all managed PIDs to disk so the next daemon can clean up
|
|
@@ -1105,6 +1118,10 @@ class OuroDaemon {
|
|
|
1105
1118
|
clearTimeout(this.senseAutostartTimer);
|
|
1106
1119
|
this.senseAutostartTimer = null;
|
|
1107
1120
|
}
|
|
1121
|
+
if (this.externalEventReconcileTimer) {
|
|
1122
|
+
clearInterval(this.externalEventReconcileTimer);
|
|
1123
|
+
this.externalEventReconcileTimer = null;
|
|
1124
|
+
}
|
|
1108
1125
|
const workerStopTasks = [Promise.resolve().then(() => this.processManager.stopAll())];
|
|
1109
1126
|
if (this.senseManager) {
|
|
1110
1127
|
workerStopTasks.push(Promise.resolve().then(() => this.senseManager.stopAll()));
|
|
@@ -1279,14 +1296,14 @@ class OuroDaemon {
|
|
|
1279
1296
|
],
|
|
1280
1297
|
};
|
|
1281
1298
|
}
|
|
1282
|
-
buildExternalEventPrivateWakeCommand(command, receiptId) {
|
|
1299
|
+
buildExternalEventPrivateWakeCommand(command, receiptId, generation, attemptCount) {
|
|
1283
1300
|
return {
|
|
1284
1301
|
kind: "private.wake",
|
|
1285
1302
|
agent: command.agent,
|
|
1286
1303
|
reason: `external event ${command.source}/${command.eventType}`,
|
|
1287
1304
|
triggerSource: "external-event",
|
|
1288
1305
|
budgetClass: "interactive",
|
|
1289
|
-
idempotencyKey: `external-event:${command.agent}:${command.source}:${command.eventId}`,
|
|
1306
|
+
idempotencyKey: `external-event:${command.agent}:${command.source}:${command.eventId}:generation:${generation}:attempt:${attemptCount}`,
|
|
1290
1307
|
originRefs: [
|
|
1291
1308
|
{ kind: "external-event", id: command.eventId, source: command.source, eventType: command.eventType },
|
|
1292
1309
|
{ kind: "queue-receipt", id: receiptId },
|
|
@@ -1456,7 +1473,7 @@ class OuroDaemon {
|
|
|
1456
1473
|
data: result,
|
|
1457
1474
|
};
|
|
1458
1475
|
}
|
|
1459
|
-
buildPrivateRuntimeWorkerWakeMessage(command, decision) {
|
|
1476
|
+
buildPrivateRuntimeWorkerWakeMessage(command, decision, externalEvent) {
|
|
1460
1477
|
const awaitName = (0, await_private_wake_1.awaitNameFromPrivateWakeCommand)(command);
|
|
1461
1478
|
if (awaitName) {
|
|
1462
1479
|
return {
|
|
@@ -1483,13 +1500,13 @@ class OuroDaemon {
|
|
|
1483
1500
|
privateTurnDecision: decision,
|
|
1484
1501
|
};
|
|
1485
1502
|
}
|
|
1486
|
-
return { type: "message", privateTurnDecision: decision };
|
|
1503
|
+
return { type: "message", privateTurnDecision: decision, ...(externalEvent ? { externalEvent } : {}) };
|
|
1487
1504
|
}
|
|
1488
1505
|
queueExternalEventForPrivateRuntime(record) {
|
|
1489
1506
|
const pendingDir = path.join(this.bundlesRoot, `${record.agent}.ouro`, "state", "pending", pending_1.PRIVATE_RUNTIME_PENDING.friendId, pending_1.PRIVATE_RUNTIME_PENDING.channel, pending_1.PRIVATE_RUNTIME_PENDING.key);
|
|
1490
1507
|
const originKey = `${record.source}:${record.eventId}`;
|
|
1491
1508
|
const parsedReceivedAt = Date.parse(record.receivedAt);
|
|
1492
|
-
(0, pending_1.
|
|
1509
|
+
(0, pending_1.queuePendingMessageOnce)(pendingDir, {
|
|
1493
1510
|
from: "ouro-external-event",
|
|
1494
1511
|
friendId: "ouro-external-event",
|
|
1495
1512
|
channel: "external-event",
|
|
@@ -1503,6 +1520,7 @@ class OuroDaemon {
|
|
|
1503
1520
|
},
|
|
1504
1521
|
obligationStatus: "pending",
|
|
1505
1522
|
mode: "relay",
|
|
1523
|
+
packetId: `external-event:${record.agent}:${record.source}:${record.eventId}:generation:${record.generation}:attempt:${record.attemptCount}`,
|
|
1506
1524
|
});
|
|
1507
1525
|
(0, runtime_1.emitNervesEvent)({
|
|
1508
1526
|
component: "daemon",
|
|
@@ -1517,7 +1535,124 @@ class OuroDaemon {
|
|
|
1517
1535
|
},
|
|
1518
1536
|
});
|
|
1519
1537
|
}
|
|
1520
|
-
|
|
1538
|
+
externalEventRootPath() {
|
|
1539
|
+
return this.externalEventRoot ?? (0, router_1.getExternalEventRoot)();
|
|
1540
|
+
}
|
|
1541
|
+
externalEventLease(record) {
|
|
1542
|
+
/* v8 ignore next -- dispatcher calls this only with the record returned by claimExternalEvent @preserve */
|
|
1543
|
+
if (!record.claimOwner)
|
|
1544
|
+
throw new Error("External event dispatch requires a claimed receipt");
|
|
1545
|
+
return {
|
|
1546
|
+
schemaVersion: 1,
|
|
1547
|
+
recordPath: record.recordPath,
|
|
1548
|
+
agent: record.agent,
|
|
1549
|
+
source: record.source,
|
|
1550
|
+
eventId: record.eventId,
|
|
1551
|
+
generation: record.generation,
|
|
1552
|
+
observationRevision: record.observationRevision,
|
|
1553
|
+
claimOwner: record.claimOwner,
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
async dispatchExternalEvents(records) {
|
|
1557
|
+
const claimed = [];
|
|
1558
|
+
try {
|
|
1559
|
+
for (const record of records) {
|
|
1560
|
+
const owner = `external-event:${record.agent}:${record.source}:${record.eventId}:generation:${record.generation}:attempt:${record.attemptCount + 1}`;
|
|
1561
|
+
claimed.push((0, router_1.claimExternalEvent)(record.recordPath, { owner, expectedVersion: record.version, expectedGeneration: record.generation }));
|
|
1562
|
+
}
|
|
1563
|
+
const primary = claimed[0];
|
|
1564
|
+
const [primaryLease, ...relatedEvents] = claimed.map((record) => this.externalEventLease(record));
|
|
1565
|
+
const lease = { ...primaryLease, ...(relatedEvents.length > 0 ? { relatedEvents } : {}) };
|
|
1566
|
+
const receipt = {
|
|
1567
|
+
id: `external-event:${primary.agent}:${primary.source}:${primary.eventId}:generation:${primary.generation}:attempt:${primary.attemptCount}`,
|
|
1568
|
+
queuedAt: primary.updatedAt,
|
|
1569
|
+
};
|
|
1570
|
+
const wake = await this.handlePrivateRuntimeWake(this.buildExternalEventPrivateWakeCommand({
|
|
1571
|
+
kind: "external.event.submit",
|
|
1572
|
+
agent: primary.agent,
|
|
1573
|
+
source: primary.source,
|
|
1574
|
+
eventType: primary.eventType,
|
|
1575
|
+
eventId: primary.eventId,
|
|
1576
|
+
}, receipt.id, primary.generation, primary.attemptCount), () => { for (const record of claimed)
|
|
1577
|
+
this.queueExternalEventForPrivateRuntime(record); }, lease);
|
|
1578
|
+
const denied = !wake.ok || wake.data?.decision?.executable === false;
|
|
1579
|
+
if (denied)
|
|
1580
|
+
throw new Error(wake.error ?? wake.message ?? /* v8 ignore next -- daemon wake responses always carry an error or message @preserve */ "external-event private turn was denied");
|
|
1581
|
+
return { events: claimed, event: primary, receipt, wake };
|
|
1582
|
+
}
|
|
1583
|
+
catch (error) {
|
|
1584
|
+
for (const record of claimed) {
|
|
1585
|
+
const latest = (0, router_1.readExternalEventRecord)(record.recordPath);
|
|
1586
|
+
if (latest.executionState === "running" && latest.claimOwner === record.claimOwner) {
|
|
1587
|
+
(0, router_1.failExternalEventAttempt)(latest.recordPath, {
|
|
1588
|
+
owner: record.claimOwner, expectedVersion: latest.version, expectedGeneration: latest.generation,
|
|
1589
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
throw error;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
dispatchExternalEvent(record) {
|
|
1597
|
+
return this.dispatchExternalEvents([record]);
|
|
1598
|
+
}
|
|
1599
|
+
async reconcileExternalEvents() {
|
|
1600
|
+
/* v8 ignore next -- re-entrancy guard is exercised only by overlapping real timer callbacks @preserve */
|
|
1601
|
+
if (this.externalEventReconcileRunning)
|
|
1602
|
+
return;
|
|
1603
|
+
this.externalEventReconcileRunning = true;
|
|
1604
|
+
try {
|
|
1605
|
+
const now = new Date().toISOString();
|
|
1606
|
+
if (fs.existsSync(this.privilegedEventSpoolRoot)) {
|
|
1607
|
+
this.privilegedEventScanner({ spoolRoot: this.privilegedEventSpoolRoot, eventRoot: this.externalEventRootPath() });
|
|
1608
|
+
}
|
|
1609
|
+
const statuses = (0, router_1.listExternalEventStatus)(this.externalEventRootPath());
|
|
1610
|
+
const dueRecords = [];
|
|
1611
|
+
for (const status of statuses) {
|
|
1612
|
+
if (status.corrupt)
|
|
1613
|
+
continue;
|
|
1614
|
+
let record = (0, router_1.readExternalEventRecord)(status.recordPath);
|
|
1615
|
+
if (record.dispatchEnabled === false)
|
|
1616
|
+
continue;
|
|
1617
|
+
if (record.executionState === "running" && record.claimExpiresAt && Date.parse(record.claimExpiresAt) <= Date.parse(now)) {
|
|
1618
|
+
record = (0, router_1.reconcileExternalEvent)(record.recordPath);
|
|
1619
|
+
}
|
|
1620
|
+
const due = record.executionState === "received"
|
|
1621
|
+
|| record.executionState === "queued"
|
|
1622
|
+
|| (record.executionState === "retry_wait" && record.nextAttemptAt !== null && Date.parse(record.nextAttemptAt) <= Date.parse(now));
|
|
1623
|
+
if (due)
|
|
1624
|
+
dueRecords.push(record);
|
|
1625
|
+
}
|
|
1626
|
+
const batches = new Map();
|
|
1627
|
+
for (const record of dueRecords) {
|
|
1628
|
+
const key = `${record.agent}\0${record.source}`;
|
|
1629
|
+
const batch = batches.get(key) ?? [];
|
|
1630
|
+
batch.push(record);
|
|
1631
|
+
batches.set(key, batch);
|
|
1632
|
+
}
|
|
1633
|
+
for (const records of batches.values()) {
|
|
1634
|
+
for (let offset = 0; offset < records.length; offset += 32) {
|
|
1635
|
+
const batch = records.slice(offset, offset + 32);
|
|
1636
|
+
try {
|
|
1637
|
+
await this.dispatchExternalEvents(batch);
|
|
1638
|
+
}
|
|
1639
|
+
catch (error) {
|
|
1640
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1641
|
+
level: "warn",
|
|
1642
|
+
component: "daemon",
|
|
1643
|
+
event: "daemon.external_event_dispatch_error",
|
|
1644
|
+
message: "external event dispatch failed",
|
|
1645
|
+
meta: { recordPath: batch[0].recordPath, batchSize: batch.length, error: error instanceof Error ? error.message : /* v8 ignore next -- dispatch failures are Error objects @preserve */ String(error) },
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
finally {
|
|
1652
|
+
this.externalEventReconcileRunning = false;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
async handlePrivateRuntimeWake(command, beforeDispatch, externalEvent) {
|
|
1521
1656
|
if (!this.hasManagedPrivateRuntime(command.agent)) {
|
|
1522
1657
|
return {
|
|
1523
1658
|
ok: false,
|
|
@@ -1537,7 +1672,7 @@ class OuroDaemon {
|
|
|
1537
1672
|
}
|
|
1538
1673
|
await beforeDispatch?.(decision);
|
|
1539
1674
|
await this.processManager.startAgent(command.agent);
|
|
1540
|
-
this.processManager.sendToAgent?.(command.agent, this.buildPrivateRuntimeWorkerWakeMessage(command, decision));
|
|
1675
|
+
this.processManager.sendToAgent?.(command.agent, this.buildPrivateRuntimeWorkerWakeMessage(command, decision, externalEvent));
|
|
1541
1676
|
return {
|
|
1542
1677
|
ok: true,
|
|
1543
1678
|
message: `woke private runtime for ${command.agent}`,
|
|
@@ -1766,27 +1901,24 @@ class OuroDaemon {
|
|
|
1766
1901
|
evidence: command.evidence,
|
|
1767
1902
|
payloadPath: command.payloadPath,
|
|
1768
1903
|
priority: command.priority,
|
|
1904
|
+
observationRevision: command.observationRevision,
|
|
1905
|
+
transition: command.transition,
|
|
1769
1906
|
}, {
|
|
1770
1907
|
...(this.externalEventRoot ? { root: this.externalEventRoot } : {}),
|
|
1908
|
+
dispatchEnabled: command.wake !== false,
|
|
1771
1909
|
});
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
taskRef: command.taskRef ?? `${record.source}:${record.eventId}`,
|
|
1779
|
-
});
|
|
1780
|
-
let wake = null;
|
|
1781
|
-
if (command.wake !== false) {
|
|
1782
|
-
wake = await this.handlePrivateRuntimeWake(this.buildExternalEventPrivateWakeCommand(command, receipt.id), () => this.queueExternalEventForPrivateRuntime(record));
|
|
1910
|
+
if (!record.shouldWake || command.wake === false) {
|
|
1911
|
+
return {
|
|
1912
|
+
ok: true,
|
|
1913
|
+
message: `updated external event ${record.source}/${record.eventId} without wake`,
|
|
1914
|
+
data: { event: record, receipt: null, wake: null },
|
|
1915
|
+
};
|
|
1783
1916
|
}
|
|
1917
|
+
const dispatched = await this.dispatchExternalEvent(record);
|
|
1784
1918
|
return {
|
|
1785
1919
|
ok: true,
|
|
1786
|
-
message:
|
|
1787
|
-
|
|
1788
|
-
: `queued external event ${record.source}/${record.eventId} as ${receipt.id}; ${wake?.message ?? "wake skipped"}`,
|
|
1789
|
-
data: { event: record, receipt, wake },
|
|
1920
|
+
message: `queued external event ${record.source}/${record.eventId} as ${dispatched.receipt.id}; ${dispatched.wake.message ?? "wake skipped"}`,
|
|
1921
|
+
data: dispatched,
|
|
1790
1922
|
};
|
|
1791
1923
|
}
|
|
1792
1924
|
case "private.decisions": {
|