@ouro.bot/cli 0.1.0-alpha.767 → 0.1.0-alpha.769
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 +16 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +1 -1
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/arc/cares.js +91 -9
- package/dist/heart/core.js +50 -5
- package/dist/heart/daemon/daemon.js +47 -0
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +49 -7
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +4 -1
- package/dist/heart/external-events/router.js +88 -4
- package/dist/heart/private-runtime/decision-renderer.js +2 -0
- package/dist/heart/private-runtime/ledger.js +4 -1
- package/dist/heart/private-runtime/policy.js +22 -8
- package/dist/heart/start-of-turn-packet.js +7 -3
- package/dist/repertoire/tools-continuity.js +23 -28
- package/dist/senses/pipeline.js +11 -4
- package/dist/senses/sanctuary-full-visibility-contract.js +199 -9
- package/dist/senses/sanctuary-storage-optimization-contract.js +2 -3
- package/dist/senses/telegram.js +34 -11
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.769",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Keep stale Sanctuary Care evidence out of current context and require provenance-qualified same-turn Docker verification before reporting or mutation.",
|
|
8
|
+
"Recognize natural whole-Sanctuary current-status requests and require all seven current reads, including notifications, before a terminal reply.",
|
|
9
|
+
"Revive provider-lane and managed-runtime external-event dead letters exactly once per generation after typed, newer recovery evidence.",
|
|
10
|
+
"Let verified fresh Docker alerts be reported while preserving unrelated Care display and policy metadata during partial incident refreshes and constraining durable status values."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"version": "0.1.0-alpha.768",
|
|
15
|
+
"changes": [
|
|
16
|
+
"Fix Sanctuary acceptance cursor snapshots for authenticated genesis and concurrent audit writes.",
|
|
17
|
+
"Cover fail-closed cursor schema, authority, and offset validation."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
4
20
|
{
|
|
5
21
|
"version": "0.1.0-alpha.767",
|
|
6
22
|
"changes": [
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"fixed": {
|
|
103
103
|
"allowedRoot": "/evidence",
|
|
104
104
|
"evidencePath": "/evidence/cursor-snapshot.json",
|
|
105
|
-
"adapters": [{ "schema": "telegram-cursor-v1", "executable": "/opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh" }]
|
|
105
|
+
"adapters": [{ "schema": "telegram-cursor-v1", "executable": "/opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh", "allowGenesis": false }]
|
|
106
106
|
}
|
|
107
107
|
},
|
|
108
108
|
"cursor-delta": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>Mendelow Cloud Butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.769</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
package/dist/arc/cares.js
CHANGED
|
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CARE_INCIDENT_RECOVERY_REVIEW_RISK = void 0;
|
|
37
|
+
exports.isManagedDockerCareIncidentBinding = isManagedDockerCareIncidentBinding;
|
|
38
|
+
exports.projectCareEvidence = projectCareEvidence;
|
|
36
39
|
exports.createCare = createCare;
|
|
37
40
|
exports.readCares = readCares;
|
|
38
41
|
exports.readActiveCares = readActiveCares;
|
|
@@ -48,6 +51,32 @@ const session_events_1 = require("../heart/session-events");
|
|
|
48
51
|
const runtime_1 = require("../nerves/runtime");
|
|
49
52
|
const session_transaction_1 = require("../mind/session-transaction");
|
|
50
53
|
const json_store_1 = require("./json-store");
|
|
54
|
+
exports.CARE_INCIDENT_RECOVERY_REVIEW_RISK = "The verified incident recovered, but this Care still has unresolved context that needs review.";
|
|
55
|
+
function isManagedDockerCareIncidentBinding(binding) {
|
|
56
|
+
return binding.source === "sanctuary-health::Docker_critical_image_disk_utilization"
|
|
57
|
+
&& /^docker-image-disk-(?:100|[1-9]?[0-9])pct-[0-9]{8}T[0-9]{4}Z$/u.test(binding.incidentKey)
|
|
58
|
+
&& /^[a-f0-9]{64}$/u.test(binding.classifiedRevision)
|
|
59
|
+
&& binding.correlationKey === undefined
|
|
60
|
+
&& binding.resolvedAt === undefined;
|
|
61
|
+
}
|
|
62
|
+
function projectCareEvidence(care, now = Date.now()) {
|
|
63
|
+
if (care.kind !== "system" || (care.status !== "active" && care.status !== "watching") || care.nextCheckAt === null)
|
|
64
|
+
return care;
|
|
65
|
+
const staleAt = Date.parse(care.nextCheckAt);
|
|
66
|
+
if (Number.isFinite(staleAt) && staleAt > now)
|
|
67
|
+
return care;
|
|
68
|
+
return {
|
|
69
|
+
id: care.id,
|
|
70
|
+
kind: care.kind,
|
|
71
|
+
status: care.status,
|
|
72
|
+
salience: care.salience,
|
|
73
|
+
steward: care.steward,
|
|
74
|
+
evidenceStatus: "stale",
|
|
75
|
+
recheckRequired: true,
|
|
76
|
+
staleAt: care.nextCheckAt,
|
|
77
|
+
lastAssessedAt: care.updatedAt,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
51
80
|
function caresDir(agentRoot) {
|
|
52
81
|
return path.join(agentRoot, "arc", "cares");
|
|
53
82
|
}
|
|
@@ -208,11 +237,21 @@ function bindCareIncident(agentRoot, id, binding, options) {
|
|
|
208
237
|
function upsertCareForIncident(agentRoot, input) {
|
|
209
238
|
return withCareMutationLock(agentRoot, () => {
|
|
210
239
|
const incident = canonicalIncidentBinding(input.incident);
|
|
211
|
-
const existing = (0, json_store_1.readJsonDir)(caresDir(agentRoot)).find((care) => care.incidentBindings?.some((binding) => binding.source === incident.source && binding.incidentKey === incident.incidentKey));
|
|
240
|
+
const existing = (0, json_store_1.readJsonDir)(caresDir(agentRoot)).find((care) => (!input.id || care.id === input.id) && care.incidentBindings?.some((binding) => binding.source === incident.source && binding.incidentKey === incident.incidentKey));
|
|
212
241
|
if (existing) {
|
|
213
242
|
const index = existing.incidentBindings.findIndex((binding) => binding.source === incident.source && binding.incidentKey === incident.incidentKey);
|
|
214
|
-
const
|
|
215
|
-
|
|
243
|
+
const requestedRisk = input.currentRisk === undefined ? existing.currentRisk : input.currentRisk === null ? null : (0, session_events_1.capStructuredRecordString)(input.currentRisk);
|
|
244
|
+
const requestedNextCheckAt = input.nextCheckAt === undefined ? existing.nextCheckAt : input.nextCheckAt;
|
|
245
|
+
const incidentStateUnchanged = JSON.stringify(existing.incidentBindings[index]) === JSON.stringify(incident)
|
|
246
|
+
&& existing.currentRisk === requestedRisk
|
|
247
|
+
&& existing.nextCheckAt === requestedNextCheckAt;
|
|
248
|
+
const displayUnchanged = !input.id || ((input.label === undefined || existing.label === (0, session_events_1.capStructuredRecordString)(input.label))
|
|
249
|
+
&& (input.why === undefined || existing.why === (0, session_events_1.capStructuredRecordString)(input.why))
|
|
250
|
+
&& (input.kind === undefined || existing.kind === input.kind)
|
|
251
|
+
&& (input.status === undefined || existing.status === input.status)
|
|
252
|
+
&& (input.salience === undefined || existing.salience === input.salience)
|
|
253
|
+
&& (input.steward === undefined || existing.steward === input.steward));
|
|
254
|
+
const unchanged = incidentStateUnchanged && displayUnchanged;
|
|
216
255
|
if (unchanged)
|
|
217
256
|
return existing;
|
|
218
257
|
if (!input.expectedUpdatedAt || input.expectedUpdatedAt !== existing.updatedAt)
|
|
@@ -221,15 +260,29 @@ function upsertCareForIncident(agentRoot, input) {
|
|
|
221
260
|
incidentBindings[index] = incident;
|
|
222
261
|
const updated = {
|
|
223
262
|
...existing,
|
|
224
|
-
|
|
225
|
-
|
|
263
|
+
...(input.id && input.label !== undefined ? { label: (0, session_events_1.capStructuredRecordString)(input.label) } : {}),
|
|
264
|
+
...(input.id && input.why !== undefined ? { why: (0, session_events_1.capStructuredRecordString)(input.why) } : {}),
|
|
265
|
+
...(input.id && input.kind !== undefined ? { kind: input.kind } : {}),
|
|
266
|
+
...(input.id && input.status !== undefined ? { status: input.status } : {}),
|
|
267
|
+
...(input.id && input.salience !== undefined ? { salience: input.salience } : {}),
|
|
268
|
+
...(input.id && input.steward !== undefined ? { steward: input.steward } : {}),
|
|
269
|
+
currentRisk: requestedRisk,
|
|
270
|
+
nextCheckAt: requestedNextCheckAt,
|
|
226
271
|
incidentBindings,
|
|
227
272
|
updatedAt: nextUpdatedAt(existing.updatedAt),
|
|
228
273
|
};
|
|
229
274
|
writeCareFile(agentRoot, updated);
|
|
230
275
|
return updated;
|
|
231
276
|
}
|
|
232
|
-
|
|
277
|
+
if (input.id)
|
|
278
|
+
throw new Error("Care incident upsert target not found");
|
|
279
|
+
const { id: _id, incident: _incident, expectedUpdatedAt: _expectedUpdatedAt, ...careInput } = input;
|
|
280
|
+
return createCareUnlocked(agentRoot, {
|
|
281
|
+
label: careInput.label ?? "untitled", why: careInput.why ?? "", kind: careInput.kind ?? "system", status: careInput.status ?? "active",
|
|
282
|
+
salience: careInput.salience ?? "medium", steward: careInput.steward ?? "mine", currentRisk: careInput.currentRisk ?? null,
|
|
283
|
+
nextCheckAt: careInput.nextCheckAt ?? null, relatedFriendIds: careInput.relatedFriendIds, relatedAgentIds: careInput.relatedAgentIds,
|
|
284
|
+
relatedObligationIds: careInput.relatedObligationIds, relatedEpisodeIds: careInput.relatedEpisodeIds, incidentBindings: [incident],
|
|
285
|
+
});
|
|
233
286
|
});
|
|
234
287
|
}
|
|
235
288
|
function resolveCareIncident(agentRoot, id, input) {
|
|
@@ -241,11 +294,40 @@ function resolveCareIncident(agentRoot, id, input) {
|
|
|
241
294
|
const index = bindings.findIndex((binding) => binding.source === input.source && binding.incidentKey === input.incidentKey);
|
|
242
295
|
if (index < 0)
|
|
243
296
|
throw new Error("Care incident binding not found");
|
|
244
|
-
|
|
297
|
+
const display = input.display;
|
|
298
|
+
const requestedDisplay = display ? {
|
|
299
|
+
label: display.label === undefined ? care.label : (0, session_events_1.capStructuredRecordString)(display.label),
|
|
300
|
+
why: display.why === undefined ? care.why : (0, session_events_1.capStructuredRecordString)(display.why),
|
|
301
|
+
currentRisk: display.currentRisk === undefined ? care.currentRisk : display.currentRisk === null ? null : (0, session_events_1.capStructuredRecordString)(display.currentRisk),
|
|
302
|
+
nextCheckAt: display.nextCheckAt === undefined ? care.nextCheckAt : display.nextCheckAt,
|
|
303
|
+
} : undefined;
|
|
304
|
+
const displayUnchanged = !requestedDisplay || JSON.stringify({
|
|
305
|
+
label: care.label,
|
|
306
|
+
why: care.why,
|
|
307
|
+
currentRisk: care.currentRisk,
|
|
308
|
+
nextCheckAt: care.nextCheckAt,
|
|
309
|
+
}) === JSON.stringify(requestedDisplay);
|
|
310
|
+
if (bindings[index].resolvedAt && displayUnchanged)
|
|
245
311
|
return care;
|
|
246
312
|
const now = nextUpdatedAt(care.updatedAt);
|
|
247
|
-
|
|
248
|
-
|
|
313
|
+
if (!bindings[index].resolvedAt)
|
|
314
|
+
bindings[index] = { ...bindings[index], resolvedAt: now };
|
|
315
|
+
const noUnresolvedBindings = bindings.every((binding) => binding.resolvedAt);
|
|
316
|
+
const unresolvedBindingsBeforeResolution = care.incidentBindings.filter((binding) => !binding.resolvedAt);
|
|
317
|
+
const soleBindingOwnsRisk = care.kind === "system" && unresolvedBindingsBeforeResolution.length === 1 && isManagedDockerCareIncidentBinding(unresolvedBindingsBeforeResolution[0]);
|
|
318
|
+
const unresolvedContextRemains = !noUnresolvedBindings || (care.currentRisk !== null && !soleBindingOwnsRisk);
|
|
319
|
+
const canonicalDisplay = requestedDisplay && requestedDisplay.currentRisk === null && unresolvedContextRemains
|
|
320
|
+
? { ...requestedDisplay, currentRisk: exports.CARE_INCIDENT_RECOVERY_REVIEW_RISK, nextCheckAt: requestedDisplay.nextCheckAt ?? new Date(Date.parse(now) + 15 * 60_000).toISOString() }
|
|
321
|
+
: requestedDisplay;
|
|
322
|
+
const resultingRisk = canonicalDisplay ? canonicalDisplay.currentRisk : care.currentRisk;
|
|
323
|
+
const shouldResolveCare = noUnresolvedBindings && (care.currentRisk === null || soleBindingOwnsRisk) && resultingRisk === null;
|
|
324
|
+
const updated = {
|
|
325
|
+
...care,
|
|
326
|
+
...canonicalDisplay,
|
|
327
|
+
incidentBindings: bindings,
|
|
328
|
+
...(shouldResolveCare ? { status: "resolved", resolvedAt: care.resolvedAt ?? now } : {}),
|
|
329
|
+
updatedAt: now,
|
|
330
|
+
};
|
|
249
331
|
writeCareFile(agentRoot, updated);
|
|
250
332
|
(0, runtime_1.emitNervesEvent)({
|
|
251
333
|
component: "heart",
|
package/dist/heart/core.js
CHANGED
|
@@ -569,10 +569,10 @@ function toolResultIndicatesFailure(content) {
|
|
|
569
569
|
|| normalized.startsWith("blocked:")
|
|
570
570
|
|| normalized.startsWith("rejected:");
|
|
571
571
|
}
|
|
572
|
-
function requiredToolResultSucceeded(name, content, validate) {
|
|
572
|
+
function requiredToolResultSucceeded(name, content, args, validate) {
|
|
573
573
|
if (toolResultIndicatesFailure(content))
|
|
574
574
|
return false;
|
|
575
|
-
return validate?.(name, content) ?? true;
|
|
575
|
+
return validate?.(name, content, args) ?? true;
|
|
576
576
|
}
|
|
577
577
|
function effectFingerprint(name, rawArguments) {
|
|
578
578
|
let args;
|
|
@@ -1252,6 +1252,11 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1252
1252
|
})()
|
|
1253
1253
|
: ordinaryActiveTools;
|
|
1254
1254
|
const candidateToolNames = new Set(candidateActiveTools.map((tool) => tool.function.name));
|
|
1255
|
+
const unadvertisedRequiredTool = requiredToolCallNames.find((name) => !candidateToolNames.has(name));
|
|
1256
|
+
if (unadvertisedRequiredTool) {
|
|
1257
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "engine", event: "engine.required_tool_unadvertised", message: "required tool is not advertised for the active channel", meta: { toolName: unadvertisedRequiredTool, channel: String(channel) } });
|
|
1258
|
+
throw new Error(`required tool is not advertised for this channel: ${unadvertisedRequiredTool}`);
|
|
1259
|
+
}
|
|
1255
1260
|
const forcedHistoricalToolNames = new Set(unresolvedHistoricalEffects
|
|
1256
1261
|
.map((effect) => effect.name)
|
|
1257
1262
|
.filter((name) => candidateToolNames.has(name)));
|
|
@@ -1942,7 +1947,14 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1942
1947
|
done = true;
|
|
1943
1948
|
continue;
|
|
1944
1949
|
}
|
|
1945
|
-
const
|
|
1950
|
+
const requiredDispatchRejections = new Map();
|
|
1951
|
+
for (const entry of validCalls) {
|
|
1952
|
+
const requiredArgs = entry.validated.arguments;
|
|
1953
|
+
const rejection = options?.requiredToolCalls?.validateToolCallBeforeDispatch?.(entry.call.name, requiredArgs);
|
|
1954
|
+
if (rejection)
|
|
1955
|
+
requiredDispatchRejections.set(entry.call.id, { name: entry.call.name, args: requiredArgs, message: rejection });
|
|
1956
|
+
}
|
|
1957
|
+
const approvalCalls = await Promise.all(validCalls.filter((entry) => !requiredDispatchRejections.has(entry.call.id)).map(async (entry) => {
|
|
1946
1958
|
const classification = await (0, tools_1.classifyApprovalForInvocation)(entry.call.name, entry.validated.arguments, augmentedToolContext);
|
|
1947
1959
|
return {
|
|
1948
1960
|
...entry,
|
|
@@ -2039,6 +2051,28 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
2039
2051
|
for (const tc of result.toolCalls) {
|
|
2040
2052
|
if (signal?.aborted)
|
|
2041
2053
|
break;
|
|
2054
|
+
const requiredDispatchRejection = requiredDispatchRejections.get(tc.id);
|
|
2055
|
+
if (requiredDispatchRejection) {
|
|
2056
|
+
callbacks.onToolStart(tc.name, requiredDispatchRejection.args);
|
|
2057
|
+
callbacks.onToolEnd(tc.name, (0, tools_1.summarizeArgs)(tc.name, requiredDispatchRejection.args), false);
|
|
2058
|
+
pushGenerated({ role: "tool", tool_call_id: tc.id, content: requiredDispatchRejection.message });
|
|
2059
|
+
providerRuntime.appendToolOutput(tc.id, requiredDispatchRejection.message);
|
|
2060
|
+
options?.toolBoundaryObserver?.({
|
|
2061
|
+
name: tc.name,
|
|
2062
|
+
reason: "dependency_rejected",
|
|
2063
|
+
globallyResolvable: typeof (0, tools_1.resolveToolDefinition)(tc.name)?.handler === "function",
|
|
2064
|
+
invoked: false,
|
|
2065
|
+
sideEffect: false,
|
|
2066
|
+
});
|
|
2067
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2068
|
+
level: "warn",
|
|
2069
|
+
component: "engine",
|
|
2070
|
+
event: "engine.required_tool_dispatch_rejected",
|
|
2071
|
+
message: "required tool dependency rejected before approval and handler dispatch",
|
|
2072
|
+
meta: { toolName: tc.name },
|
|
2073
|
+
});
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2042
2076
|
// Reject sole-call tools when mixed with other tool calls
|
|
2043
2077
|
const terminalProjection = (0, tools_1.resolveToolDefinition)(tc.name)?.terminalProjection;
|
|
2044
2078
|
const soleCallRejection = SOLE_CALL_REJECTION[tc.name]
|
|
@@ -2321,9 +2355,20 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
2321
2355
|
success = false;
|
|
2322
2356
|
augmentedToolContext?.habitSession?.recordError?.(toolResult);
|
|
2323
2357
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2358
|
+
const validatedRequiredResult = success && requiredToolCallNames.includes(tc.name) && options?.requiredToolCalls?.requireSuccessfulResults
|
|
2359
|
+
? requiredToolResultSucceeded(tc.name, toolResult, args, options.requiredToolCalls.validateRequiredToolResult)
|
|
2360
|
+
: false;
|
|
2361
|
+
if (validatedRequiredResult) {
|
|
2326
2362
|
dispatchedRequiredToolCalls.add(tc.name);
|
|
2363
|
+
for (const requiredName of options?.requiredToolCalls?.requiredToolCallsAfterResult?.(tc.name, args, toolResult) ?? []) {
|
|
2364
|
+
if (!candidateToolNames.has(requiredName)) {
|
|
2365
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "engine", event: "engine.required_tool_unadvertised", message: "dependent required tool is not advertised for the active channel", meta: { toolName: requiredName, channel: String(channel) } });
|
|
2366
|
+
throw new Error(`dependent required tool is not advertised for this channel: ${requiredName}`);
|
|
2367
|
+
}
|
|
2368
|
+
if (!requiredToolCallNames.includes(requiredName))
|
|
2369
|
+
requiredToolCallNames.push(requiredName);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2327
2372
|
if (success && currentEffectFingerprint && !toolResultIndicatesFailure(toolResult)) {
|
|
2328
2373
|
unresolvedHistoricalEffects = unresolvedHistoricalEffects.filter((effect) => effect.fingerprint !== currentEffectFingerprint);
|
|
2329
2374
|
}
|
|
@@ -70,6 +70,7 @@ const mailbox_types_1 = require("../mailbox/mailbox-types");
|
|
|
70
70
|
const mailbox_read_1 = require("../mailbox/mailbox-read");
|
|
71
71
|
const mailbox_view_1 = require("../mailbox/mailbox-view");
|
|
72
72
|
const provider_visibility_1 = require("../provider-visibility");
|
|
73
|
+
const provider_binding_resolver_1 = require("../provider-binding-resolver");
|
|
73
74
|
const private_runtime_1 = require("../private-runtime");
|
|
74
75
|
const socket_client_1 = require("./socket-client");
|
|
75
76
|
const flight_recorder_1 = require("../../arc/flight-recorder");
|
|
@@ -1555,6 +1556,7 @@ class OuroDaemon {
|
|
|
1555
1556
|
}
|
|
1556
1557
|
async dispatchExternalEvents(records) {
|
|
1557
1558
|
const claimed = [];
|
|
1559
|
+
let failureClass;
|
|
1558
1560
|
try {
|
|
1559
1561
|
for (const record of records) {
|
|
1560
1562
|
const owner = `external-event:${record.agent}:${record.source}:${record.eventId}:generation:${record.generation}:attempt:${record.attemptCount + 1}`;
|
|
@@ -1575,6 +1577,11 @@ class OuroDaemon {
|
|
|
1575
1577
|
eventId: primary.eventId,
|
|
1576
1578
|
}, receipt.id, primary.generation, primary.attemptCount), () => { for (const record of claimed)
|
|
1577
1579
|
this.queueExternalEventForPrivateRuntime(record); }, lease);
|
|
1580
|
+
failureClass = wake.denialCode === "managed_runtime_unavailable"
|
|
1581
|
+
? "managed_runtime_unavailable"
|
|
1582
|
+
: wake.data?.decision?.denialCode === "provider_lane_unavailable"
|
|
1583
|
+
? "provider_lane_unavailable"
|
|
1584
|
+
: undefined;
|
|
1578
1585
|
const denied = !wake.ok || wake.data?.decision?.executable === false;
|
|
1579
1586
|
if (denied)
|
|
1580
1587
|
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");
|
|
@@ -1587,6 +1594,7 @@ class OuroDaemon {
|
|
|
1587
1594
|
(0, router_1.failExternalEventAttempt)(latest.recordPath, {
|
|
1588
1595
|
owner: record.claimOwner, expectedVersion: latest.version, expectedGeneration: latest.generation,
|
|
1589
1596
|
error: error instanceof Error ? error.message : String(error),
|
|
1597
|
+
...(failureClass ? { failureClass } : {}),
|
|
1590
1598
|
});
|
|
1591
1599
|
}
|
|
1592
1600
|
}
|
|
@@ -1614,6 +1622,43 @@ class OuroDaemon {
|
|
|
1614
1622
|
let record = (0, router_1.readExternalEventRecord)(status.recordPath);
|
|
1615
1623
|
if (record.dispatchEnabled === false)
|
|
1616
1624
|
continue;
|
|
1625
|
+
if (record.executionState === "dead_letter") {
|
|
1626
|
+
const failure = (0, router_1.externalEventRecoveryFailure)(record);
|
|
1627
|
+
if (!failure || record.recoveryGrant?.generation === record.generation)
|
|
1628
|
+
continue;
|
|
1629
|
+
let evidence = null;
|
|
1630
|
+
try {
|
|
1631
|
+
if (failure.class === "provider_lane_unavailable") {
|
|
1632
|
+
const binding = (0, provider_binding_resolver_1.resolveEffectiveProviderBinding)({
|
|
1633
|
+
agentName: record.agent,
|
|
1634
|
+
agentRoot: path.join(this.bundlesRoot, `${record.agent}.ouro`),
|
|
1635
|
+
lane: "inner",
|
|
1636
|
+
});
|
|
1637
|
+
if (binding.ok && binding.binding.credential.status === "present"
|
|
1638
|
+
&& binding.binding.readiness.status === "ready" && binding.binding.readiness.checkedAt
|
|
1639
|
+
&& Date.parse(binding.binding.readiness.checkedAt) > Date.parse(failure.failedAt)) {
|
|
1640
|
+
evidence = { class: failure.class, observedAt: binding.binding.readiness.checkedAt };
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
else if (this.hasManagedPrivateRuntime(record.agent) && Date.parse(now) > Date.parse(failure.failedAt)) {
|
|
1644
|
+
evidence = { class: failure.class, observedAt: now };
|
|
1645
|
+
}
|
|
1646
|
+
if (!evidence)
|
|
1647
|
+
continue;
|
|
1648
|
+
const revival = (0, router_1.reviveExternalEventAfterRecovery)(record.recordPath, { expectedVersion: record.version, expectedGeneration: record.generation, evidence, now: () => now });
|
|
1649
|
+
record = revival.record;
|
|
1650
|
+
}
|
|
1651
|
+
catch (error) {
|
|
1652
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1653
|
+
level: "warn",
|
|
1654
|
+
component: "daemon",
|
|
1655
|
+
event: "daemon.external_event_recovery_error",
|
|
1656
|
+
message: "external event dead-letter recovery check failed",
|
|
1657
|
+
meta: { agent: record.agent, source: record.source, eventId: record.eventId, generation: record.generation, error: error instanceof Error ? error.message : String(error) },
|
|
1658
|
+
});
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1617
1662
|
if (record.executionState === "running" && record.claimExpiresAt && Date.parse(record.claimExpiresAt) <= Date.parse(now)) {
|
|
1618
1663
|
record = (0, router_1.reconcileExternalEvent)(record.recordPath);
|
|
1619
1664
|
}
|
|
@@ -1657,6 +1702,7 @@ class OuroDaemon {
|
|
|
1657
1702
|
return {
|
|
1658
1703
|
ok: false,
|
|
1659
1704
|
error: `No managed agent '${command.agent}' is registered with daemon-managed private runtime.`,
|
|
1705
|
+
denialCode: "managed_runtime_unavailable",
|
|
1660
1706
|
};
|
|
1661
1707
|
}
|
|
1662
1708
|
const decision = await (0, private_runtime_1.requestPrivateTurnDecision)(this.buildPrivateRuntimeWakeRequest(command), {
|
|
@@ -1667,6 +1713,7 @@ class OuroDaemon {
|
|
|
1667
1713
|
return {
|
|
1668
1714
|
ok: true,
|
|
1669
1715
|
message: `private-runtime wake denied for ${command.agent}: ${decision.deniedReason}`,
|
|
1716
|
+
...(decision.denialCode ? { denialCode: decision.denialCode } : {}),
|
|
1670
1717
|
data: { decision },
|
|
1671
1718
|
};
|
|
1672
1719
|
}
|
|
@@ -99,6 +99,7 @@ const NETWORK_TIMEOUT_MS = 10_000;
|
|
|
99
99
|
const KEY_DIRECTORY = "/boot/config/plugins/dynamix.my.servers/keys";
|
|
100
100
|
const SELECTED_KEY_RECORD = "/run/ouro-acceptance/unraid-key.json";
|
|
101
101
|
const TELEGRAM_OFFSET = "/home/ouro/AgentBundles/sanctuary.ouro/state/senses/telegram/offset.json";
|
|
102
|
+
const TELEGRAM_IDENTITY_KEY = "/home/ouro/AgentBundles/sanctuary.ouro/state/senses/telegram/identity.key";
|
|
102
103
|
const TELEGRAM_AUDIT = `/home/ouro/AgentBundles/sanctuary.ouro/${telegram_audit_ledger_1.TELEGRAM_ACCEPTANCE_AUDIT_RELATIVE_PATH}`;
|
|
103
104
|
const TELEGRAM_AUDIT_HEAD = `/home/ouro/AgentBundles/sanctuary.ouro/${telegram_audit_ledger_1.TELEGRAM_ACCEPTANCE_AUDIT_HEAD_RELATIVE_PATH}`;
|
|
104
105
|
const IMAGE_DIGEST_FILE = "/run/ouro-acceptance/image-digest";
|
|
@@ -1848,16 +1849,52 @@ async function storeTelegramBootstrap(payload, deps) {
|
|
|
1848
1849
|
}
|
|
1849
1850
|
return { stored: true };
|
|
1850
1851
|
}
|
|
1851
|
-
function cursorSnapshot(deps) {
|
|
1852
|
+
function cursorSnapshot(payload, deps) {
|
|
1853
|
+
exactKeys(payload, ["allowGenesis", "operation", "schema"], "Telegram cursor snapshot request");
|
|
1854
|
+
if (payload.schema !== "telegram-cursor-v1" || typeof payload.allowGenesis !== "boolean")
|
|
1855
|
+
throw new Error("Telegram cursor snapshot request is invalid");
|
|
1852
1856
|
const offsetRaw = fixedFile(deps, TELEGRAM_OFFSET);
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1857
|
+
const identityKey = fixedFile(deps, TELEGRAM_IDENTITY_KEY).trim();
|
|
1858
|
+
if (!/^[A-Za-z0-9_-]{43}$/u.test(identityKey) || Buffer.from(identityKey, "base64url").length !== 32) {
|
|
1859
|
+
throw new Error("Telegram identity key is invalid");
|
|
1860
|
+
}
|
|
1855
1861
|
const offset = object(JSON.parse(offsetRaw), "Telegram offset");
|
|
1856
1862
|
if (!Number.isSafeInteger(offset.nextUpdateId) || offset.nextUpdateId < 0)
|
|
1857
1863
|
throw new Error("Telegram offset is invalid");
|
|
1864
|
+
let auditCursorDigest;
|
|
1865
|
+
let verificationFailure;
|
|
1866
|
+
let observedAuditState = false;
|
|
1867
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1868
|
+
const headBefore = optionalFixedFile(deps, TELEGRAM_AUDIT_HEAD);
|
|
1869
|
+
const auditRaw = optionalFixedFile(deps, TELEGRAM_AUDIT);
|
|
1870
|
+
const headAfter = optionalFixedFile(deps, TELEGRAM_AUDIT_HEAD);
|
|
1871
|
+
if (headBefore === null && auditRaw === null && headAfter === null) {
|
|
1872
|
+
if (payload.allowGenesis !== true)
|
|
1873
|
+
throw new Error("Telegram audit genesis is not authorized for this snapshot");
|
|
1874
|
+
if (!observedAuditState) {
|
|
1875
|
+
auditCursorDigest = sha256("ouroboros.telegram.acceptance.cursor.v1\0genesis");
|
|
1876
|
+
break;
|
|
1877
|
+
}
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
observedAuditState = true;
|
|
1881
|
+
if (headBefore !== null && auditRaw !== null && headAfter !== null && headBefore === headAfter) {
|
|
1882
|
+
try {
|
|
1883
|
+
(0, telegram_audit_ledger_1.verifyTelegramAuditLedger)({ ledgerRaw: auditRaw, headRaw: headAfter, identityKey });
|
|
1884
|
+
auditCursorDigest = sha256(`ouroboros.telegram.acceptance.cursor.v1\0present\0${auditRaw}\0${headAfter}`);
|
|
1885
|
+
break;
|
|
1886
|
+
}
|
|
1887
|
+
catch (error) {
|
|
1888
|
+
verificationFailure = error;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
if (auditCursorDigest === undefined) {
|
|
1893
|
+
throw new Error(`Telegram acceptance audit state could not be captured consistently${verificationFailure ? `: ${verificationFailure.message}` : ""}`, verificationFailure ? { cause: verificationFailure } : undefined);
|
|
1894
|
+
}
|
|
1858
1895
|
return {
|
|
1859
1896
|
offsetDigest: sha256(JSON.stringify({ nextUpdateId: offset.nextUpdateId })),
|
|
1860
|
-
auditCursorDigest
|
|
1897
|
+
auditCursorDigest,
|
|
1861
1898
|
};
|
|
1862
1899
|
}
|
|
1863
1900
|
function telegramPollerQuiescence(payload, deps) {
|
|
@@ -2065,7 +2102,7 @@ function provenance(payload, deps) {
|
|
|
2065
2102
|
const containerDigest = fixedFile(deps, CONTAINER_DIGEST_FILE).trim();
|
|
2066
2103
|
if (!SHA256.test(imageDigest) || !SHA256.test(containerDigest))
|
|
2067
2104
|
throw new Error("live provenance digest is invalid");
|
|
2068
|
-
const cursor = cursorSnapshot(deps);
|
|
2105
|
+
const cursor = cursorSnapshot({ operation: "snapshot", schema: "telegram-cursor-v1", allowGenesis: false }, deps);
|
|
2069
2106
|
return { imageDigest, containerDigest, cursorDigest: sha256(`${cursor.offsetDigest}\0${cursor.auditCursorDigest}`) };
|
|
2070
2107
|
}
|
|
2071
2108
|
function evidenceSnapshot(payload, deps) {
|
|
@@ -2078,7 +2115,11 @@ function evidenceSnapshot(payload, deps) {
|
|
|
2078
2115
|
const imageDigest = fixedFile(deps, IMAGE_DIGEST_FILE).trim();
|
|
2079
2116
|
if (!SHA256.test(imageDigest))
|
|
2080
2117
|
throw new Error("container image digest is invalid");
|
|
2081
|
-
return {
|
|
2118
|
+
return {
|
|
2119
|
+
healthy: health.healthy,
|
|
2120
|
+
containerImageDigest: imageDigest,
|
|
2121
|
+
telegramOffsetDigest: cursorSnapshot({ operation: "snapshot", schema: "telegram-cursor-v1", allowGenesis: false }, deps).offsetDigest,
|
|
2122
|
+
};
|
|
2082
2123
|
}
|
|
2083
2124
|
function bootId(deps) {
|
|
2084
2125
|
const value = fixedFile(deps, BOOT_ID_FILE).trim();
|
|
@@ -2147,6 +2188,7 @@ function materializeConfig(payload, deps) {
|
|
|
2147
2188
|
if (phase !== "before" && phase !== "after")
|
|
2148
2189
|
throw new Error("cursor snapshot phase is invalid");
|
|
2149
2190
|
config.evidencePath = `/evidence/cursor-${phase}.json`;
|
|
2191
|
+
config.adapters = config.adapters.map((adapter) => ({ ...adapter, allowGenesis: phase === "before" }));
|
|
2150
2192
|
}
|
|
2151
2193
|
else if (command === "unraid-key-rotate") {
|
|
2152
2194
|
const closed = object(JSON.parse(fixedFile(deps, CLOSED_INVENTORY_FILE)), "closed Unraid inventory");
|
|
@@ -2188,7 +2230,7 @@ async function executeSanctuaryAcceptanceAdapter(rawPayload, deps = createSanctu
|
|
|
2188
2230
|
result = telegramPollerQuiescence(payload, deps);
|
|
2189
2231
|
break;
|
|
2190
2232
|
case "snapshot":
|
|
2191
|
-
result = cursorSnapshot(deps);
|
|
2233
|
+
result = cursorSnapshot(payload, deps);
|
|
2192
2234
|
break;
|
|
2193
2235
|
case "inject_callbacks_concurrently":
|
|
2194
2236
|
result = await concurrentCallbackProbe(payload, deps);
|
|
@@ -1011,7 +1011,10 @@ async function cursorSnapshot(config, deps) {
|
|
|
1011
1011
|
throw new Error("snapshot adapter schemas must be unique");
|
|
1012
1012
|
schemas.add(schema);
|
|
1013
1013
|
const executable = adapter(spec.executable, "snapshot adapter executable");
|
|
1014
|
-
const
|
|
1014
|
+
const allowGenesis = spec.allowGenesis === undefined ? false : boolean(spec.allowGenesis, "snapshot adapter allowGenesis");
|
|
1015
|
+
if (allowGenesis && evidencePath !== path.join(root, "cursor-before.json"))
|
|
1016
|
+
throw new Error("snapshot adapter genesis authority is only valid for cursor-before evidence");
|
|
1017
|
+
const payload = await deps.runAdapter(executable, { operation: "snapshot", schema, allowGenesis });
|
|
1015
1018
|
const selected = fixedEvidenceValues(schema, payload);
|
|
1016
1019
|
for (const [name, value] of Object.entries(selected))
|
|
1017
1020
|
values[`${schema}.${name}`] = value;
|