@ouro.bot/cli 0.1.0-alpha.771 → 0.1.0-alpha.773
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 +17 -0
- package/deploy/unraid/README.txt +37 -9
- package/deploy/unraid/sanctuary-acceptance-contract.json +1 -1
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +242 -31
- package/deploy/unraid/sanctuary-unit16-run.sh +19 -2
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/daemon/cli-defaults.js +4 -0
- package/dist/heart/daemon/cli-exec.js +39 -1
- package/dist/heart/daemon/daemon-bootstrap-startup.js +40 -4
- package/dist/heart/daemon/daemon-entry.js +34 -71
- package/dist/heart/daemon/daemon.js +71 -25
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +111 -1
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +245 -49
- package/dist/heart/daemon/startup-tui.js +1 -0
- package/dist/heart/external-events/router.js +24 -14
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PUBLIC_DAEMON_STARTUP_FAILURE_REASON = void 0;
|
|
4
|
+
exports.createProviderReadinessPreparationFailure = createProviderReadinessPreparationFailure;
|
|
3
5
|
exports.failFastContainerCredentialBootstrapStartup = failFastContainerCredentialBootstrapStartup;
|
|
4
6
|
exports.startDaemonAfterContainerCredentialBootstrap = startDaemonAfterContainerCredentialBootstrap;
|
|
5
7
|
const runtime_1 = require("../../nerves/runtime");
|
|
6
8
|
const daemon_tombstone_1 = require("./daemon-tombstone");
|
|
7
9
|
const REDACTED_BOOTSTRAP_STARTUP_ERROR = "container credential bootstrap rejected; recoverable claim retained for reconciliation";
|
|
10
|
+
const REDACTED_DAEMON_PREPARATION_ERROR = "provider runtime preparation failed before startup; run `ouro doctor` for diagnosis";
|
|
11
|
+
exports.PUBLIC_DAEMON_STARTUP_FAILURE_REASON = "startupFailurePublic";
|
|
12
|
+
class DaemonPreparationFailure extends Error {
|
|
13
|
+
name = "DaemonPreparationFailure";
|
|
14
|
+
}
|
|
15
|
+
function createProviderReadinessPreparationFailure(issues) {
|
|
16
|
+
const lines = ["Provider checks need attention"];
|
|
17
|
+
for (const issue of issues) {
|
|
18
|
+
lines.push(issue.summary);
|
|
19
|
+
lines.push(...issue.actions.map((action) => ` ${action.actor}: ${action.command}`));
|
|
20
|
+
}
|
|
21
|
+
return new DaemonPreparationFailure(lines.join("\n"));
|
|
22
|
+
}
|
|
8
23
|
function failFastContainerCredentialBootstrapStartup(input) {
|
|
9
|
-
|
|
24
|
+
failFastDaemonStartup({
|
|
25
|
+
exit: input.exit,
|
|
26
|
+
errorMessage: REDACTED_BOOTSTRAP_STARTUP_ERROR,
|
|
27
|
+
eventMessage: "daemon entrypoint failed before server startup",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function failFastDaemonStartup(input) {
|
|
31
|
+
const errorMessage = input.errorMessage;
|
|
32
|
+
const error = new Error(errorMessage);
|
|
10
33
|
try {
|
|
11
|
-
(0, daemon_tombstone_1.writeDaemonTombstone)(
|
|
34
|
+
(0, daemon_tombstone_1.writeDaemonTombstone)(exports.PUBLIC_DAEMON_STARTUP_FAILURE_REASON, error);
|
|
12
35
|
}
|
|
13
36
|
catch {
|
|
14
37
|
// Exit remains mandatory even if best-effort tombstone reporting fails.
|
|
@@ -18,8 +41,8 @@ function failFastContainerCredentialBootstrapStartup(input) {
|
|
|
18
41
|
level: "error",
|
|
19
42
|
component: "daemon",
|
|
20
43
|
event: "daemon.entry_error",
|
|
21
|
-
message:
|
|
22
|
-
meta: { error:
|
|
44
|
+
message: input.eventMessage,
|
|
45
|
+
meta: { error: errorMessage },
|
|
23
46
|
});
|
|
24
47
|
}
|
|
25
48
|
catch {
|
|
@@ -36,6 +59,19 @@ async function startDaemonAfterContainerCredentialBootstrap(input) {
|
|
|
36
59
|
failFastContainerCredentialBootstrapStartup({ exit: input.exit });
|
|
37
60
|
return false;
|
|
38
61
|
}
|
|
62
|
+
try {
|
|
63
|
+
await input.prepareDaemon?.();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
input.markStartupFailure();
|
|
67
|
+
const controlledMessage = error instanceof DaemonPreparationFailure ? error.message : null;
|
|
68
|
+
failFastDaemonStartup({
|
|
69
|
+
exit: input.exit,
|
|
70
|
+
errorMessage: controlledMessage ?? REDACTED_DAEMON_PREPARATION_ERROR,
|
|
71
|
+
eventMessage: controlledMessage ?? REDACTED_DAEMON_PREPARATION_ERROR,
|
|
72
|
+
});
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
39
75
|
await input.startDaemon();
|
|
40
76
|
return true;
|
|
41
77
|
}
|
|
@@ -74,7 +74,6 @@ const runtime_credentials_1 = require("../runtime-credentials");
|
|
|
74
74
|
const machine_identity_1 = require("../machine-identity");
|
|
75
75
|
const container_credential_bootstrap_1 = require("./container-credential-bootstrap");
|
|
76
76
|
const daemon_bootstrap_startup_1 = require("./daemon-bootstrap-startup");
|
|
77
|
-
const auth_flow_1 = require("../auth/auth-flow");
|
|
78
77
|
const sanctuary_health_runner_1 = require("../../senses/sanctuary-health-runner");
|
|
79
78
|
const sanctuary_acceptance_marker_1 = require("./sanctuary-acceptance-marker");
|
|
80
79
|
const sanctuary_scheduler_liveness_1 = require("./sanctuary-scheduler-liveness");
|
|
@@ -588,77 +587,40 @@ function writeStopCommandHealthState() {
|
|
|
588
587
|
// Health writes are best-effort during shutdown.
|
|
589
588
|
}
|
|
590
589
|
}
|
|
591
|
-
function
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
agent,
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (result.ok) {
|
|
620
|
-
const missingProviders = providerPreloadMissingTargets(result, providers);
|
|
621
|
-
if (missingProviders.length > 0) {
|
|
622
|
-
(0, runtime_1.emitNervesEvent)({
|
|
623
|
-
level: "warn",
|
|
624
|
-
component: "daemon",
|
|
625
|
-
event: "daemon.provider_preload_unavailable",
|
|
626
|
-
message: "provider credential preload returned an incomplete selected provider cache",
|
|
627
|
-
meta: {
|
|
628
|
-
agent,
|
|
629
|
-
reason: "missing",
|
|
630
|
-
error: `missing selected providers: ${missingProviders.join(", ")}`,
|
|
631
|
-
},
|
|
632
|
-
});
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
const records = Object.values(result.pool.providers).filter((record) => !!record);
|
|
636
|
-
(0, provider_credentials_1.cacheProviderCredentialRecords)(agent, records, new Date(result.pool.updatedAt));
|
|
637
|
-
return;
|
|
590
|
+
async function prepareProviderRuntime() {
|
|
591
|
+
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
592
|
+
const readiness = await Promise.all(managedAgents.map(async (agent) => {
|
|
593
|
+
try {
|
|
594
|
+
const result = await (0, agent_config_check_1.checkAgentConfigWithProviderHealth)(agent, bundlesRoot);
|
|
595
|
+
if (result.ok)
|
|
596
|
+
return null;
|
|
597
|
+
(0, runtime_1.emitNervesEvent)({
|
|
598
|
+
level: "warn",
|
|
599
|
+
component: "daemon",
|
|
600
|
+
event: "daemon.provider_readiness_unavailable",
|
|
601
|
+
message: "fresh provider readiness was unavailable before daemon startup",
|
|
602
|
+
meta: { agent },
|
|
603
|
+
});
|
|
604
|
+
return result.issue ?? {
|
|
605
|
+
summary: `${agent}: provider runtime unavailable`,
|
|
606
|
+
actions: [{ actor: "agent-runnable", command: "ouro doctor" }],
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
(0, runtime_1.emitNervesEvent)({
|
|
611
|
+
level: "warn",
|
|
612
|
+
component: "daemon",
|
|
613
|
+
event: "daemon.provider_readiness_unavailable",
|
|
614
|
+
message: "fresh provider readiness check failed before daemon startup",
|
|
615
|
+
meta: { agent },
|
|
616
|
+
});
|
|
617
|
+
throw new Error("provider runtime preparation failed");
|
|
638
618
|
}
|
|
639
|
-
(0, runtime_1.emitNervesEvent)({
|
|
640
|
-
level: "warn",
|
|
641
|
-
component: "daemon",
|
|
642
|
-
event: "daemon.provider_preload_unavailable",
|
|
643
|
-
message: "provider credential preload could not refresh selected provider cache",
|
|
644
|
-
meta: {
|
|
645
|
-
agent,
|
|
646
|
-
reason: result.reason,
|
|
647
|
-
error: result.error,
|
|
648
|
-
},
|
|
649
|
-
});
|
|
650
619
|
}));
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
level: "error",
|
|
656
|
-
component: "daemon",
|
|
657
|
-
event: "daemon.provider_preload_error",
|
|
658
|
-
message: "provider credential preload failed after daemon startup",
|
|
659
|
-
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive non-Error provider preload failures @preserve */ String(error) },
|
|
660
|
-
});
|
|
661
|
-
});
|
|
620
|
+
const failures = readiness.filter((entry) => entry !== null);
|
|
621
|
+
if (failures.length > 0) {
|
|
622
|
+
throw (0, daemon_bootstrap_startup_1.createProviderReadinessPreparationFailure)(failures);
|
|
623
|
+
}
|
|
662
624
|
}
|
|
663
625
|
function scheduleStartupSentinelAfterProviderPreload(agent, preload) {
|
|
664
626
|
void preload.then(async () => {
|
|
@@ -693,6 +655,7 @@ function scheduleStartupSentinelAfterProviderPreload(agent, preload) {
|
|
|
693
655
|
/* v8 ignore start -- habit wiring: lambdas delegate to processManager/fs; tested via HabitScheduler unit tests @preserve */
|
|
694
656
|
void (0, daemon_bootstrap_startup_1.startDaemonAfterContainerCredentialBootstrap)({
|
|
695
657
|
loadBootstrap: () => (0, container_credential_bootstrap_1.loadContainerCredentialBootstrap)(managedAgents),
|
|
658
|
+
prepareDaemon: prepareProviderRuntime,
|
|
696
659
|
startDaemon: () => daemon.start(),
|
|
697
660
|
markStartupFailure: () => { _tombstoneWritten = true; },
|
|
698
661
|
exit: (code) => process.exit(code),
|
|
@@ -700,7 +663,7 @@ void (0, daemon_bootstrap_startup_1.startDaemonAfterContainerCredentialBootstrap
|
|
|
700
663
|
if (!started)
|
|
701
664
|
return;
|
|
702
665
|
supercronicSupervisor?.start();
|
|
703
|
-
const providerPreload =
|
|
666
|
+
const providerPreload = Promise.resolve();
|
|
704
667
|
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
705
668
|
const ouroPath = supercronicSupervisor
|
|
706
669
|
? "/usr/local/bin/node /opt/ouro/dist/heart/daemon/ouro-entry.js"
|
|
@@ -618,6 +618,7 @@ class OuroDaemon {
|
|
|
618
618
|
senseAutostartTimer = null;
|
|
619
619
|
externalEventReconcileTimer = null;
|
|
620
620
|
externalEventReconcileRunning = false;
|
|
621
|
+
constructedAtMs = Date.now();
|
|
621
622
|
mailboxServerFactory;
|
|
622
623
|
privateRuntimePolicyDeps;
|
|
623
624
|
onStopCommandComplete;
|
|
@@ -1503,7 +1504,7 @@ class OuroDaemon {
|
|
|
1503
1504
|
}
|
|
1504
1505
|
return { type: "message", privateTurnDecision: decision, ...(externalEvent ? { externalEvent } : {}) };
|
|
1505
1506
|
}
|
|
1506
|
-
queueExternalEventForPrivateRuntime(record) {
|
|
1507
|
+
queueExternalEventForPrivateRuntime(record, quiet = false) {
|
|
1507
1508
|
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);
|
|
1508
1509
|
const originKey = `${record.source}:${record.eventId}`;
|
|
1509
1510
|
const parsedReceivedAt = Date.parse(record.receivedAt);
|
|
@@ -1523,18 +1524,20 @@ class OuroDaemon {
|
|
|
1523
1524
|
mode: "relay",
|
|
1524
1525
|
packetId: `external-event:${record.agent}:${record.source}:${record.eventId}:generation:${record.generation}:attempt:${record.attemptCount}`,
|
|
1525
1526
|
});
|
|
1526
|
-
(
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1527
|
+
if (!quiet) {
|
|
1528
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1529
|
+
component: "daemon",
|
|
1530
|
+
event: "daemon.external_event_private_runtime_queued",
|
|
1531
|
+
message: "queued external event for private-runtime attention",
|
|
1532
|
+
meta: {
|
|
1533
|
+
agent: record.agent,
|
|
1534
|
+
source: record.source,
|
|
1535
|
+
eventType: record.eventType,
|
|
1536
|
+
eventId: record.eventId,
|
|
1537
|
+
pendingDir,
|
|
1538
|
+
},
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1538
1541
|
}
|
|
1539
1542
|
externalEventRootPath() {
|
|
1540
1543
|
return this.externalEventRoot ?? (0, router_1.getExternalEventRoot)();
|
|
@@ -1575,8 +1578,19 @@ class OuroDaemon {
|
|
|
1575
1578
|
source: primary.source,
|
|
1576
1579
|
eventType: primary.eventType,
|
|
1577
1580
|
eventId: primary.eventId,
|
|
1578
|
-
}, receipt.id, primary.generation, primary.attemptCount), () => {
|
|
1579
|
-
|
|
1581
|
+
}, receipt.id, primary.generation, primary.attemptCount), () => {
|
|
1582
|
+
const batched = claimed.length > 1;
|
|
1583
|
+
for (const record of claimed)
|
|
1584
|
+
this.queueExternalEventForPrivateRuntime(record, batched);
|
|
1585
|
+
if (batched) {
|
|
1586
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1587
|
+
component: "daemon",
|
|
1588
|
+
event: "daemon.external_event_private_runtime_batch_queued",
|
|
1589
|
+
message: "queued an external-event batch for private-runtime attention",
|
|
1590
|
+
meta: { agent: primary.agent, source: primary.source, eventCount: claimed.length },
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
}, lease);
|
|
1580
1594
|
failureClass = wake.denialCode === "managed_runtime_unavailable"
|
|
1581
1595
|
? "managed_runtime_unavailable"
|
|
1582
1596
|
: wake.data?.decision?.denialCode === "provider_lane_unavailable"
|
|
@@ -1616,6 +1630,9 @@ class OuroDaemon {
|
|
|
1616
1630
|
}
|
|
1617
1631
|
const statuses = (0, router_1.listExternalEventStatus)(this.externalEventRootPath());
|
|
1618
1632
|
const dueRecords = [];
|
|
1633
|
+
const providerEvidenceByAgent = new Map();
|
|
1634
|
+
const runtimeEvidenceByAgent = new Map();
|
|
1635
|
+
const recoveredByClass = new Map();
|
|
1619
1636
|
for (const status of statuses) {
|
|
1620
1637
|
if (status.corrupt)
|
|
1621
1638
|
continue;
|
|
@@ -1629,15 +1646,35 @@ class OuroDaemon {
|
|
|
1629
1646
|
let evidence = null;
|
|
1630
1647
|
try {
|
|
1631
1648
|
if (failure.class === "provider_lane_unavailable") {
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1649
|
+
if (!providerEvidenceByAgent.has(record.agent)) {
|
|
1650
|
+
providerEvidenceByAgent.set(record.agent, null);
|
|
1651
|
+
const binding = (0, provider_binding_resolver_1.resolveEffectiveProviderBinding)({
|
|
1652
|
+
agentName: record.agent,
|
|
1653
|
+
agentRoot: path.join(this.bundlesRoot, `${record.agent}.ouro`),
|
|
1654
|
+
lane: "inner",
|
|
1655
|
+
});
|
|
1656
|
+
providerEvidenceByAgent.set(record.agent, binding.ok && binding.binding.credential.status === "present"
|
|
1657
|
+
&& binding.binding.readiness.status === "ready" && binding.binding.readiness.checkedAt
|
|
1658
|
+
? { observedAt: binding.binding.readiness.checkedAt }
|
|
1659
|
+
: null);
|
|
1660
|
+
}
|
|
1661
|
+
const current = providerEvidenceByAgent.get(record.agent);
|
|
1662
|
+
const readinessIsCurrentBoot = current && Date.parse(current.observedAt) >= this.constructedAtMs;
|
|
1663
|
+
if (current && ((0, router_1.isExactLegacyProviderRecoveryFailure)(record)
|
|
1664
|
+
? readinessIsCurrentBoot
|
|
1665
|
+
: Date.parse(current.observedAt) > Date.parse(failure.failedAt))) {
|
|
1666
|
+
evidence = { class: failure.class, observedAt: current.observedAt };
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
else if (failure.class === "execution_lease_expired") {
|
|
1670
|
+
if (!runtimeEvidenceByAgent.has(record.agent)) {
|
|
1671
|
+
const snapshot = this.processManager.listAgentSnapshots().find((candidate) => candidate.name === record.agent && candidate.channel === "private-runtime"
|
|
1672
|
+
&& candidate.status === "running" && candidate.pid !== null && candidate.startedAt !== null);
|
|
1673
|
+
runtimeEvidenceByAgent.set(record.agent, snapshot?.startedAt ? { observedAt: snapshot.startedAt } : null);
|
|
1674
|
+
}
|
|
1675
|
+
const current = runtimeEvidenceByAgent.get(record.agent);
|
|
1676
|
+
if (current && Date.parse(current.observedAt) > Date.parse(failure.failedAt)) {
|
|
1677
|
+
evidence = { class: failure.class, observedAt: current.observedAt };
|
|
1641
1678
|
}
|
|
1642
1679
|
}
|
|
1643
1680
|
else if (this.hasManagedPrivateRuntime(record.agent) && Date.parse(now) > Date.parse(failure.failedAt)) {
|
|
@@ -1645,8 +1682,9 @@ class OuroDaemon {
|
|
|
1645
1682
|
}
|
|
1646
1683
|
if (!evidence)
|
|
1647
1684
|
continue;
|
|
1648
|
-
const revival = (0, router_1.reviveExternalEventAfterRecovery)(record.recordPath, { expectedVersion: record.version, expectedGeneration: record.generation, evidence, now: () => now });
|
|
1685
|
+
const revival = (0, router_1.reviveExternalEventAfterRecovery)(record.recordPath, { expectedVersion: record.version, expectedGeneration: record.generation, evidence, now: () => now, quiet: true });
|
|
1649
1686
|
record = revival.record;
|
|
1687
|
+
recoveredByClass.set(failure.class, (recoveredByClass.get(failure.class) ?? 0) + 1);
|
|
1650
1688
|
}
|
|
1651
1689
|
catch (error) {
|
|
1652
1690
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -1668,6 +1706,14 @@ class OuroDaemon {
|
|
|
1668
1706
|
if (due)
|
|
1669
1707
|
dueRecords.push(record);
|
|
1670
1708
|
}
|
|
1709
|
+
if (recoveredByClass.size > 0) {
|
|
1710
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1711
|
+
component: "daemon",
|
|
1712
|
+
event: "daemon.external_event_recovery_batch",
|
|
1713
|
+
message: "requeued external events after cached infrastructure recovery evidence",
|
|
1714
|
+
meta: { recovered: Object.fromEntries(recoveredByClass) },
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1671
1717
|
const batches = new Map();
|
|
1672
1718
|
for (const record of dueRecords) {
|
|
1673
1719
|
const key = `${record.agent}\0${record.source}`;
|
|
@@ -32,6 +32,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.proveAttemptedRecoveryWithoutRetry = void 0;
|
|
37
40
|
exports.evaluateSanctuaryProviderReadinessContract = evaluateSanctuaryProviderReadinessContract;
|
|
@@ -45,6 +48,8 @@ exports.canonicalDockerIdFromUnraidPrefixedId = canonicalDockerIdFromUnraidPrefi
|
|
|
45
48
|
exports.runSanctuaryProductionBoundaryProbe = runSanctuaryProductionBoundaryProbe;
|
|
46
49
|
exports.createSanctuaryReadOnlyDenialScenarioDriver = createSanctuaryReadOnlyDenialScenarioDriver;
|
|
47
50
|
exports.readDefaultSanctuaryScenarioFacts = readDefaultSanctuaryScenarioFacts;
|
|
51
|
+
exports.callbackPlaybackSnapshot = callbackPlaybackSnapshot;
|
|
52
|
+
exports.recordCallbackPlayback = recordCallbackPlayback;
|
|
48
53
|
exports.executeSanctuaryAcceptanceAdapter = executeSanctuaryAcceptanceAdapter;
|
|
49
54
|
exports.executeSanctuaryAcceptanceVaultProbe = executeSanctuaryAcceptanceVaultProbe;
|
|
50
55
|
exports.executeSanctuaryAcceptanceRevokedProbe = executeSanctuaryAcceptanceRevokedProbe;
|
|
@@ -55,6 +60,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
55
60
|
const node_fs_1 = require("node:fs");
|
|
56
61
|
const node_net_1 = require("node:net");
|
|
57
62
|
const path = __importStar(require("node:path"));
|
|
63
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
58
64
|
const friends_1 = require("@ouro.bot/friends");
|
|
59
65
|
const runtime_1 = require("../../nerves/runtime");
|
|
60
66
|
const telegram_approval_runtime_1 = require("../../senses/telegram-approval-runtime");
|
|
@@ -99,6 +105,7 @@ const NETWORK_TIMEOUT_MS = 10_000;
|
|
|
99
105
|
const KEY_DIRECTORY = "/boot/config/plugins/dynamix.my.servers/keys";
|
|
100
106
|
const SELECTED_KEY_RECORD = "/run/ouro-acceptance/unraid-key.json";
|
|
101
107
|
const TELEGRAM_OFFSET = "/home/ouro/AgentBundles/sanctuary.ouro/state/senses/telegram/offset.json";
|
|
108
|
+
const CALLBACK_PLAYBACK_JOURNAL = "state/approvals/sanctuary-callback-playback.sqlite";
|
|
102
109
|
const TELEGRAM_IDENTITY_KEY = "/home/ouro/AgentBundles/sanctuary.ouro/state/senses/telegram/identity.key";
|
|
103
110
|
const TELEGRAM_AUDIT = `/home/ouro/AgentBundles/sanctuary.ouro/${telegram_audit_ledger_1.TELEGRAM_ACCEPTANCE_AUDIT_RELATIVE_PATH}`;
|
|
104
111
|
const TELEGRAM_AUDIT_HEAD = `/home/ouro/AgentBundles/sanctuary.ouro/${telegram_audit_ledger_1.TELEGRAM_ACCEPTANCE_AUDIT_HEAD_RELATIVE_PATH}`;
|
|
@@ -244,6 +251,7 @@ function createSanctuaryAcceptanceAdapterDependencies(secretFd = 3, options = {}
|
|
|
244
251
|
refreshMachine: runtime_credentials_1.refreshMachineRuntimeCredentialConfig,
|
|
245
252
|
mergeMachine: runtime_credentials_1.mergeMachineRuntimeCredentialConfig,
|
|
246
253
|
callbackProbe: executeSanctuaryAcceptanceCallbackProbe,
|
|
254
|
+
callbackPlaybackSnapshot: (coordinateDigest) => callbackPlaybackSnapshot((0, identity_1.getAgentRoot)(TARGET_ID), coordinateDigest),
|
|
247
255
|
interactiveRuntime: executeSanctuaryInteractiveRuntimeOperation,
|
|
248
256
|
hostRequest: options.hostRequest ?? ((payload) => defaultHostRequest(payload, hostBrokerSocket, adapterTimeoutMs)),
|
|
249
257
|
telegramCredentials: () => (0, telegram_1.loadTelegramSenseCredentials)(TARGET_ID),
|
|
@@ -1912,6 +1920,94 @@ function callbackUpdate(value) {
|
|
|
1912
1920
|
object(update.callback_query, "callback update callback_query");
|
|
1913
1921
|
return update;
|
|
1914
1922
|
}
|
|
1923
|
+
function withCallbackPlaybackJournal(agentRoot, operation) {
|
|
1924
|
+
const databasePath = path.join(agentRoot, CALLBACK_PLAYBACK_JOURNAL);
|
|
1925
|
+
(0, node_fs_1.mkdirSync)(path.dirname(databasePath), { recursive: true, mode: 0o700 });
|
|
1926
|
+
const database = new better_sqlite3_1.default(databasePath);
|
|
1927
|
+
try {
|
|
1928
|
+
(0, node_fs_1.chmodSync)(databasePath, 0o600);
|
|
1929
|
+
database.pragma("journal_mode = DELETE");
|
|
1930
|
+
database.pragma("synchronous = FULL");
|
|
1931
|
+
database.exec(`
|
|
1932
|
+
CREATE TABLE IF NOT EXISTS callback_playback (
|
|
1933
|
+
coordinate_digest TEXT PRIMARY KEY NOT NULL CHECK(length(coordinate_digest) = 64),
|
|
1934
|
+
playback_count INTEGER NOT NULL CHECK(playback_count > 0)
|
|
1935
|
+
)
|
|
1936
|
+
`);
|
|
1937
|
+
return operation(database);
|
|
1938
|
+
}
|
|
1939
|
+
finally {
|
|
1940
|
+
database.close();
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
function callbackPlaybackSnapshot(agentRoot, coordinateDigest) {
|
|
1944
|
+
if (!SHA256.test(coordinateDigest))
|
|
1945
|
+
throw new Error("callback coordinate digest is invalid");
|
|
1946
|
+
return withCallbackPlaybackJournal(agentRoot, (database) => {
|
|
1947
|
+
const rows = database.prepare("SELECT coordinate_digest, playback_count FROM callback_playback ORDER BY coordinate_digest")
|
|
1948
|
+
.all();
|
|
1949
|
+
if (rows.some((row) => !SHA256.test(row.coordinate_digest) || !Number.isSafeInteger(row.playback_count) || row.playback_count <= 0)) {
|
|
1950
|
+
throw new Error("callback playback journal is invalid");
|
|
1951
|
+
}
|
|
1952
|
+
const playbackCount = rows.find((row) => row.coordinate_digest === coordinateDigest)?.playback_count ?? 0;
|
|
1953
|
+
return { playbackCount, coordinateDigest, journalDigest: sha256(JSON.stringify(rows)) };
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
function recordCallbackPlayback(agentRoot, coordinateDigest) {
|
|
1957
|
+
if (!SHA256.test(coordinateDigest))
|
|
1958
|
+
throw new Error("callback coordinate digest is invalid");
|
|
1959
|
+
withCallbackPlaybackJournal(agentRoot, (database) => {
|
|
1960
|
+
database.prepare(`
|
|
1961
|
+
INSERT INTO callback_playback (coordinate_digest, playback_count) VALUES (?, 1)
|
|
1962
|
+
ON CONFLICT(coordinate_digest) DO UPDATE SET playback_count = playback_count + 1
|
|
1963
|
+
`).run(coordinateDigest);
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
function callbackCoordinate(update) {
|
|
1967
|
+
const updateId = update.update_id;
|
|
1968
|
+
if (!Number.isSafeInteger(updateId) || Number(updateId) < 0)
|
|
1969
|
+
throw new Error("callback update_id is invalid");
|
|
1970
|
+
const callback = object(update.callback_query, "callback update callback_query");
|
|
1971
|
+
const from = object(callback.from, "callback update sender");
|
|
1972
|
+
const message = object(callback.message, "callback update message");
|
|
1973
|
+
const chat = object(message.chat, "callback update chat");
|
|
1974
|
+
const callbackId = (value, label) => {
|
|
1975
|
+
if (!Number.isSafeInteger(value) || Number(value) <= 0)
|
|
1976
|
+
throw new Error(`${label} is invalid`);
|
|
1977
|
+
return String(value);
|
|
1978
|
+
};
|
|
1979
|
+
const coordinate = {
|
|
1980
|
+
updateId: Number(updateId),
|
|
1981
|
+
queryId: text(callback.id, "callback query id"),
|
|
1982
|
+
callbackData: text(callback.data, "callback data"),
|
|
1983
|
+
userId: callbackId(from.id, "callback user id"),
|
|
1984
|
+
chatId: callbackId(chat.id, "callback chat id"),
|
|
1985
|
+
messageId: callbackId(message.message_id, "callback message id"),
|
|
1986
|
+
};
|
|
1987
|
+
return {
|
|
1988
|
+
updateId: coordinate.updateId,
|
|
1989
|
+
digest: sha256(`ouroboros.sanctuary.callback-coordinate.v1\0${JSON.stringify(coordinate)}`),
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
function callbackPlaybackPreflight(payload, deps) {
|
|
1993
|
+
exactKeys(payload, ["operation", "update"], "callback playback preflight request");
|
|
1994
|
+
const coordinate = callbackCoordinate(callbackUpdate(payload.update));
|
|
1995
|
+
const offset = object(JSON.parse(fixedFile(deps, TELEGRAM_OFFSET)), "Telegram offset");
|
|
1996
|
+
exactKeys(offset, ["nextUpdateId"], "Telegram offset");
|
|
1997
|
+
if (!Number.isSafeInteger(offset.nextUpdateId) || Number(offset.nextUpdateId) < 0)
|
|
1998
|
+
throw new Error("Telegram offset is invalid");
|
|
1999
|
+
const snapshot = object(dependency(deps.callbackPlaybackSnapshot, "callback playback journal")(coordinate.digest), "callback playback journal snapshot");
|
|
2000
|
+
exactKeys(snapshot, ["coordinateDigest", "journalDigest", "playbackCount"], "callback playback journal snapshot");
|
|
2001
|
+
if (snapshot.coordinateDigest !== coordinate.digest || typeof snapshot.journalDigest !== "string" || !SHA256.test(snapshot.journalDigest)
|
|
2002
|
+
|| !Number.isSafeInteger(snapshot.playbackCount) || Number(snapshot.playbackCount) < 0) {
|
|
2003
|
+
throw new Error("callback playback journal snapshot is invalid");
|
|
2004
|
+
}
|
|
2005
|
+
return {
|
|
2006
|
+
playbackCount: Math.max(Number(snapshot.playbackCount), Number(offset.nextUpdateId) > coordinate.updateId ? 1 : 0),
|
|
2007
|
+
coordinateDigest: coordinate.digest,
|
|
2008
|
+
journalDigest: snapshot.journalDigest,
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
1915
2011
|
async function concurrentCallbackProbe(payload, deps) {
|
|
1916
2012
|
const update = callbackUpdate(payload.update);
|
|
1917
2013
|
if (!Number.isSafeInteger(payload.concurrency) || payload.concurrency < 2 || payload.concurrency > 16) {
|
|
@@ -2026,6 +2122,12 @@ async function storeKey(payload, deps) {
|
|
|
2026
2122
|
if (text(handles[id], "vault-backed Unraid credential") !== text(stored[field], "vault-backed Unraid credential")) {
|
|
2027
2123
|
throw new Error("Unraid key handle does not bind to the active vault field");
|
|
2028
2124
|
}
|
|
2125
|
+
const acknowledged = object(await dependency(deps.hostRequest, "Sanctuary host broker")({
|
|
2126
|
+
operation: "acknowledge_key_storage", targetServerId: TARGET_SERVER_ID, keyId: id,
|
|
2127
|
+
}), "Unraid key storage acknowledgement");
|
|
2128
|
+
if (acknowledged.acknowledged !== true || acknowledged.id !== id) {
|
|
2129
|
+
throw new Error("Unraid key storage acknowledgement failed");
|
|
2130
|
+
}
|
|
2029
2131
|
return { stored: true, keyId: id };
|
|
2030
2132
|
}
|
|
2031
2133
|
async function probeKey(payload, deps) {
|
|
@@ -2232,6 +2334,9 @@ async function executeSanctuaryAcceptanceAdapter(rawPayload, deps = createSanctu
|
|
|
2232
2334
|
case "snapshot":
|
|
2233
2335
|
result = cursorSnapshot(payload, deps);
|
|
2234
2336
|
break;
|
|
2337
|
+
case "callback_playback_preflight":
|
|
2338
|
+
result = callbackPlaybackPreflight(payload, deps);
|
|
2339
|
+
break;
|
|
2235
2340
|
case "inject_callbacks_concurrently":
|
|
2236
2341
|
result = await concurrentCallbackProbe(payload, deps);
|
|
2237
2342
|
break;
|
|
@@ -2418,7 +2523,7 @@ async function executeSanctuaryInteractiveRuntimeOperation(rawPayload, supplied)
|
|
|
2418
2523
|
};
|
|
2419
2524
|
return (0, sanctuary_interactive_control_1.executeSanctuaryInteractiveEngine)(rawPayload, deps);
|
|
2420
2525
|
}
|
|
2421
|
-
async function executeSanctuaryAcceptanceCallbackProbe(rawUpdate,
|
|
2526
|
+
async function executeSanctuaryAcceptanceCallbackProbe(rawUpdate, replay, deps = {
|
|
2422
2527
|
refresh: runtime_credentials_1.refreshRuntimeCredentialConfig,
|
|
2423
2528
|
credentials: telegram_1.loadTelegramSenseCredentials,
|
|
2424
2529
|
identityKey: telegram_1.readOrCreateTelegramIdentityKey,
|
|
@@ -2426,6 +2531,7 @@ async function executeSanctuaryAcceptanceCallbackProbe(rawUpdate, _replay, deps
|
|
|
2426
2531
|
createRuntime: telegram_approval_runtime_1.createTelegramApprovalRuntime,
|
|
2427
2532
|
toolContext: sanctuary_runtime_1.createSanctuaryToolContext,
|
|
2428
2533
|
effects: createAcceptanceProbeEffects,
|
|
2534
|
+
recordCallbackPlayback: (coordinateDigest) => recordCallbackPlayback((0, identity_1.getAgentRoot)(TARGET_ID), coordinateDigest),
|
|
2429
2535
|
}) {
|
|
2430
2536
|
const update = callbackUpdate(rawUpdate);
|
|
2431
2537
|
const refreshed = await deps.refresh(TARGET_ID);
|
|
@@ -2453,7 +2559,11 @@ async function executeSanctuaryAcceptanceCallbackProbe(rawUpdate, _replay, deps
|
|
|
2453
2559
|
effects: effectBoundary?.port ?? unavailableEffects,
|
|
2454
2560
|
});
|
|
2455
2561
|
try {
|
|
2562
|
+
deps.recordCallbackPlayback(callbackCoordinate(update).digest);
|
|
2456
2563
|
const result = await runtime.transport.handleUpdate(update);
|
|
2564
|
+
if (replay && (result.handled !== true || result.accepted !== false || result.reason !== "stale_callback")) {
|
|
2565
|
+
throw new Error("Telegram callback replay did not settle as stale");
|
|
2566
|
+
}
|
|
2457
2567
|
return {
|
|
2458
2568
|
settled: result.handled,
|
|
2459
2569
|
claimed: result.reason === "accepted" || result.reason === "decision_refused",
|