@ouro.bot/cli 0.1.0-alpha.766 → 0.1.0-alpha.768
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 +15 -0
- package/deploy/unraid/README.txt +96 -5
- package/deploy/unraid/sanctuary-acceptance-contract.json +1 -1
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +0 -2
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -2
- package/dist/heart/daemon/container-spec-auditor.js +3 -5
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +49 -7
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +4 -1
- package/dist/senses/sanctuary-full-visibility-contract.js +62 -16
- package/dist/senses/sanctuary-runtime.js +16 -2
- package/dist/senses/sanctuary-sab.js +24 -19
- package/dist/senses/shared-turn.js +3 -2
- package/dist/senses/telegram.js +28 -8
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
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.768",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Fix Sanctuary acceptance cursor snapshots for authenticated genesis and concurrent audit writes.",
|
|
8
|
+
"Cover fail-closed cursor schema, authority, and offset validation."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.767",
|
|
13
|
+
"changes": [
|
|
14
|
+
"Make Sanctuary status turns preserve completed live checks and report bounded download-queue unavailability without retries, stale guesses, or empty replies.",
|
|
15
|
+
"Move SAB queue access into the machine vault with guarded one-shot provisioning, live readiness verification, and no persistent host INI mount.",
|
|
16
|
+
"Share the owner-status completion fallback between Telegram's admitted-message replay and normal message paths, and cover its inactive, incomplete, and complete states."
|
|
17
|
+
]
|
|
18
|
+
},
|
|
4
19
|
{
|
|
5
20
|
"version": "0.1.0-alpha.766",
|
|
6
21
|
"changes": [
|
package/deploy/unraid/README.txt
CHANGED
|
@@ -793,6 +793,8 @@ Effective-spec audit helper:
|
|
|
793
793
|
bootstrap_sanctuary_vault "$IMAGE_ID" \
|
|
794
794
|
/mnt/user/appdata/ouro-butler/runtime/container-credentials.json \
|
|
795
795
|
sanctuary-unraid sanctuary || return $?
|
|
796
|
+
provision_sanctuary_sab_credential "$IMAGE_ID" || return $?
|
|
797
|
+
verify_sanctuary_sab_readiness "$IMAGE_ID" || return $?
|
|
796
798
|
validate_sanctuary_legacy_staging "$PREPARED_LEGACY_CONTAINER_ID" "$PREPARED_LEGACY_IMAGE_ID" || return $?
|
|
797
799
|
LEGACY_STAGING_CONTAINER_ID=$PREPARED_LEGACY_CONTAINER_ID
|
|
798
800
|
LEGACY_STAGING_IMAGE_ID=$PREPARED_LEGACY_IMAGE_ID
|
|
@@ -938,7 +940,6 @@ Effective-spec audit helper:
|
|
|
938
940
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
939
941
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
940
942
|
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
941
|
-
--mount "type=bind,src=/mnt/user/appdata/sabnzbd/sabnzbd.ini,dst=/run/sanctuary/sabnzbd.ini,readonly" \
|
|
942
943
|
"$IMAGE_ID" \
|
|
943
944
|
&& audit_effective ouro-butler-staging "$IMAGE_ID" "$IMAGE_ID" \
|
|
944
945
|
&& docker start ouro-butler-staging \
|
|
@@ -952,7 +953,6 @@ Effective-spec audit helper:
|
|
|
952
953
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
953
954
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
954
955
|
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
955
|
-
--mount "type=bind,src=/mnt/user/appdata/sabnzbd/sabnzbd.ini,dst=/run/sanctuary/sabnzbd.ini,readonly" \
|
|
956
956
|
"$IMAGE_ID" \
|
|
957
957
|
&& audit_effective ouro-butler "$IMAGE_ID" "$IMAGE_ID" \
|
|
958
958
|
&& docker start ouro-butler \
|
|
@@ -1187,6 +1187,75 @@ local unlock: available
|
|
|
1187
1187
|
fi
|
|
1188
1188
|
validate_sanctuary_roots "$BOOTSTRAP_RUNTIME_ROOT" "$BOOTSTRAP_AGENT_ROOT" || return $?
|
|
1189
1189
|
}
|
|
1190
|
+
provision_sanctuary_sab_credential() {
|
|
1191
|
+
SAB_BOOTSTRAP_IMAGE_ID=$1
|
|
1192
|
+
validate_exact_image_id "$SAB_BOOTSTRAP_IMAGE_ID" || return $?
|
|
1193
|
+
SAB_BOOTSTRAP_SOURCE=/mnt/user/appdata/sabnzbd/sabnzbd.ini
|
|
1194
|
+
SAB_BOOTSTRAP_RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
|
|
1195
|
+
SAB_BOOTSTRAP_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
|
|
1196
|
+
SAB_BOOTSTRAP_ENVELOPE="$SAB_BOOTSTRAP_RUNTIME_ROOT/container-credentials.json"
|
|
1197
|
+
SAB_BOOTSTRAP_CLAIM="$SAB_BOOTSTRAP_ENVELOPE.consuming"
|
|
1198
|
+
test -f "$SAB_BOOTSTRAP_SOURCE" || return $?
|
|
1199
|
+
test ! -L "$SAB_BOOTSTRAP_SOURCE" || return 1
|
|
1200
|
+
test ! -e "$SAB_BOOTSTRAP_ENVELOPE" || return 1
|
|
1201
|
+
test ! -e "$SAB_BOOTSTRAP_CLAIM" || return 1
|
|
1202
|
+
! docker container inspect ouro-butler-sab-credential-bootstrap >/dev/null 2>&1 || return 1
|
|
1203
|
+
SAB_BOOTSTRAP_TMP=$(mktemp "$SAB_BOOTSTRAP_RUNTIME_ROOT/container-credentials.json.tmp.XXXXXX") || return $?
|
|
1204
|
+
trap 'rm -f -- "$SAB_BOOTSTRAP_TMP"' EXIT || return $?
|
|
1205
|
+
/usr/local/bin/node -e '
|
|
1206
|
+
const fs = require("node:fs");
|
|
1207
|
+
const [source, destination] = process.argv.slice(1);
|
|
1208
|
+
const match = fs.readFileSync(source, "utf8").match(/^\s*api_key\s*=\s*(\S+)\s*$/mu);
|
|
1209
|
+
if (!match || !match[1]) process.exit(1);
|
|
1210
|
+
const credential = { type: "ouro.runtimeCredentialBootstrap", agentName: "sanctuary", machineId: "sanctuary", machineRuntimeConfig: { sabnzbdApiKey: match[1] } };
|
|
1211
|
+
const envelope = { schemaVersion: 1, credentials: [credential] };
|
|
1212
|
+
fs.writeFileSync(destination, `${JSON.stringify(envelope)}\n`, { mode: 0o600, flag: "w" });
|
|
1213
|
+
' "$SAB_BOOTSTRAP_SOURCE" "$SAB_BOOTSTRAP_TMP" || return $?
|
|
1214
|
+
chown 10001:10001 "$SAB_BOOTSTRAP_TMP" || return $?
|
|
1215
|
+
chmod 0600 "$SAB_BOOTSTRAP_TMP" || return $?
|
|
1216
|
+
sync -f "$SAB_BOOTSTRAP_TMP" || return $?
|
|
1217
|
+
mv -f -- "$SAB_BOOTSTRAP_TMP" "$SAB_BOOTSTRAP_ENVELOPE" || return $?
|
|
1218
|
+
sync -f "$SAB_BOOTSTRAP_RUNTIME_ROOT" || return $?
|
|
1219
|
+
trap - EXIT || return $?
|
|
1220
|
+
docker run --rm --pull=never --network host --name ouro-butler-sab-credential-bootstrap --user 10001:10001 \
|
|
1221
|
+
--mount "type=bind,src=$SAB_BOOTSTRAP_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
|
|
1222
|
+
--mount "type=bind,src=$SAB_BOOTSTRAP_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1223
|
+
--entrypoint node "$SAB_BOOTSTRAP_IMAGE_ID" -e '
|
|
1224
|
+
const { loadContainerCredentialBootstrap } = require("/opt/ouro/dist/heart/daemon/container-credential-bootstrap.js");
|
|
1225
|
+
loadContainerCredentialBootstrap(["sanctuary"]).then(
|
|
1226
|
+
() => process.exit(0),
|
|
1227
|
+
() => { process.stderr.write("SAB credential bootstrap failed\n"); process.exit(1); },
|
|
1228
|
+
);
|
|
1229
|
+
' || return $?
|
|
1230
|
+
! docker container inspect ouro-butler-sab-credential-bootstrap >/dev/null 2>&1 || return 1
|
|
1231
|
+
test ! -e "$SAB_BOOTSTRAP_ENVELOPE" || return 1
|
|
1232
|
+
test ! -e "$SAB_BOOTSTRAP_CLAIM" || return 1
|
|
1233
|
+
}
|
|
1234
|
+
verify_sanctuary_sab_readiness() {
|
|
1235
|
+
SAB_READINESS_IMAGE_ID=$1
|
|
1236
|
+
validate_exact_image_id "$SAB_READINESS_IMAGE_ID" || return $?
|
|
1237
|
+
! docker container inspect ouro-butler-sab-readiness >/dev/null 2>&1 || return 1
|
|
1238
|
+
docker run --rm --pull=never --network host --name ouro-butler-sab-readiness --user 10001:10001 \
|
|
1239
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
1240
|
+
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1241
|
+
--entrypoint node "$SAB_READINESS_IMAGE_ID" -e '
|
|
1242
|
+
(async () => {
|
|
1243
|
+
try {
|
|
1244
|
+
const { refreshMachineRuntimeCredentialConfig } = require("/opt/ouro/dist/heart/runtime-credentials.js");
|
|
1245
|
+
const { createSanctuarySabClient } = require("/opt/ouro/dist/senses/sanctuary-sab.js");
|
|
1246
|
+
const current = await refreshMachineRuntimeCredentialConfig("sanctuary", "sanctuary");
|
|
1247
|
+
if (!current.ok || typeof current.config.sabnzbdApiKey !== "string" || !current.config.sabnzbdApiKey.trim()) throw new Error();
|
|
1248
|
+
const snapshot = await createSanctuarySabClient({ loadApiKey: async () => current.config.sabnzbdApiKey }).readQueue();
|
|
1249
|
+
if (typeof snapshot.paused !== "boolean" || !Number.isSafeInteger(snapshot.queuedJobs) || !snapshot.observedAt) throw new Error();
|
|
1250
|
+
process.stdout.write("Sanctuary SAB queue readiness verified.\n");
|
|
1251
|
+
} catch {
|
|
1252
|
+
process.stderr.write("Sanctuary SAB queue readiness verification failed.\n");
|
|
1253
|
+
process.exitCode = 1;
|
|
1254
|
+
}
|
|
1255
|
+
})();
|
|
1256
|
+
' || return $?
|
|
1257
|
+
! docker container inspect ouro-butler-sab-readiness >/dev/null 2>&1 || return 1
|
|
1258
|
+
}
|
|
1190
1259
|
authenticate_sanctuary_provider() {
|
|
1191
1260
|
AUTH_IMAGE_ID=$1
|
|
1192
1261
|
AUTH_PROVIDER=${2-}
|
|
@@ -1606,6 +1675,13 @@ Update:
|
|
|
1606
1675
|
"$IMAGE_ID" --template /audit/sanctuary.xml --runtime-policy /audit/container-runtime.json --expected-image "$IMAGE_ID"
|
|
1607
1676
|
cleanup_event_asset_stage
|
|
1608
1677
|
trap - EXIT
|
|
1678
|
+
if provision_sanctuary_sab_credential "$IMAGE_ID" \
|
|
1679
|
+
&& verify_sanctuary_sab_readiness "$IMAGE_ID"; then
|
|
1680
|
+
:
|
|
1681
|
+
else
|
|
1682
|
+
SAB_READINESS_STATUS=$?
|
|
1683
|
+
(exit "$SAB_READINESS_STATUS")
|
|
1684
|
+
fi
|
|
1609
1685
|
Guard the atomic autostart disable separately. If it fails, production has not
|
|
1610
1686
|
been touched and the captured status is propagated:
|
|
1611
1687
|
if disable_butler_autostart; then
|
|
@@ -1712,7 +1788,6 @@ ouro-butler-rollback
|
|
|
1712
1788
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
1713
1789
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1714
1790
|
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
1715
|
-
--mount "type=bind,src=/mnt/user/appdata/sabnzbd/sabnzbd.ini,dst=/run/sanctuary/sabnzbd.ini,readonly" \
|
|
1716
1791
|
"$IMAGE_ID" \
|
|
1717
1792
|
&& audit_effective ouro-butler-staging "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" \
|
|
1718
1793
|
&& docker start ouro-butler-staging \
|
|
@@ -1761,7 +1836,6 @@ ouro-butler-rollback
|
|
|
1761
1836
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
1762
1837
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1763
1838
|
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
1764
|
-
--mount "type=bind,src=/mnt/user/appdata/sabnzbd/sabnzbd.ini,dst=/run/sanctuary/sabnzbd.ini,readonly" \
|
|
1765
1839
|
"$IMAGE_ID" \
|
|
1766
1840
|
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" \
|
|
1767
1841
|
&& docker start ouro-butler \
|
|
@@ -2065,7 +2139,6 @@ Restore:
|
|
|
2065
2139
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
2066
2140
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
2067
2141
|
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
2068
|
-
--mount "type=bind,src=/mnt/user/appdata/sabnzbd/sabnzbd.ini,dst=/run/sanctuary/sabnzbd.ini,readonly" \
|
|
2069
2142
|
"$IMAGE_ID" \
|
|
2070
2143
|
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" \
|
|
2071
2144
|
&& docker start ouro-butler \
|
|
@@ -2131,6 +2204,24 @@ Credential recovery:
|
|
|
2131
2204
|
Never print either envelope's contents or place them in logs or command arguments.
|
|
2132
2205
|
After successful import, both files
|
|
2133
2206
|
are absent and the vault is the only credential source of truth.
|
|
2207
|
+
`provision_sanctuary_sab_credential "$IMAGE_ID"` is the one-shot migration for
|
|
2208
|
+
the existing SAB key. Root reads the fixed host INI, writes only a mode-0600
|
|
2209
|
+
`ouro.runtimeCredentialBootstrap` envelope owned by 10001:10001, and a
|
|
2210
|
+
same-image one-shot container merges `sabnzbdApiKey` into
|
|
2211
|
+
`runtime/machines/sanctuary/config`; the key never appears in argv, the
|
|
2212
|
+
environment, logs, the Unraid template, or the Butler mounts. The bootstrap
|
|
2213
|
+
reconciler is idempotent for the same value and fails closed on a conflict.
|
|
2214
|
+
`verify_sanctuary_sab_readiness "$IMAGE_ID"` then reloads that machine item
|
|
2215
|
+
from the vault and performs a real bounded queue read before any poller
|
|
2216
|
+
cutover. The runtime and agent roots are persistent binds and package-managed
|
|
2217
|
+
updates do not replace the machine vault item, so the credential survives
|
|
2218
|
+
template and image updates.
|
|
2219
|
+
Bootstrap is not the rotation path. After SAB rotates its key, use the
|
|
2220
|
+
existing hidden-prompt merge command
|
|
2221
|
+
`ouro vault config set --agent sanctuary --key sabnzbdApiKey --scope machine`,
|
|
2222
|
+
restart the reviewed Butler container so its machine-config cache refreshes,
|
|
2223
|
+
and rerun `verify_sanctuary_sab_readiness "$IMAGE_ID"`; a conflicting
|
|
2224
|
+
bootstrap is expected to stop rather than overwrite canonical vault state.
|
|
2134
2225
|
Never print or place credential values in logs, templates, command arguments,
|
|
2135
2226
|
backups, or this runbook.
|
|
2136
2227
|
|
|
@@ -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": {
|
|
@@ -18,7 +18,6 @@ const RUNTIME_POLICY_FILE = "/opt/ouro/container-runtime.json"
|
|
|
18
18
|
const PRODUCTION_RUNTIME_SOURCE = "/mnt/user/appdata/ouro-butler/runtime/.ouro-cli"
|
|
19
19
|
const PRODUCTION_BUNDLE_SOURCE = "/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro"
|
|
20
20
|
const PRODUCTION_EVENT_SPOOL_SOURCE = "/boot/config/custom/ouro-events/spool"
|
|
21
|
-
const PRODUCTION_SAB_CONFIG_SOURCE = "/mnt/user/appdata/sabnzbd/sabnzbd.ini"
|
|
22
21
|
const GRAPHQL_ENDPOINT = "http://127.0.0.1/graphql"
|
|
23
22
|
const BOOT_ID = "/proc/sys/kernel/random/boot_id"
|
|
24
23
|
const MDCMD = "/usr/local/sbin/mdcmd"
|
|
@@ -392,7 +391,6 @@ async function containerSnapshot(expectedImage) {
|
|
|
392
391
|
{ destination: "/home/ouro/.ouro-cli", source: PRODUCTION_RUNTIME_SOURCE, mode: "rw", propagation: "rprivate", rw: true, type: "bind" },
|
|
393
392
|
{ destination: "/home/ouro/AgentBundles/sanctuary.ouro", source: PRODUCTION_BUNDLE_SOURCE, mode: "rw", propagation: "rprivate", rw: true, type: "bind" },
|
|
394
393
|
{ destination: "/run/ouro-events", source: PRODUCTION_EVENT_SPOOL_SOURCE, mode: "ro", propagation: "rprivate", rw: false, type: "bind" },
|
|
395
|
-
{ destination: "/run/sanctuary/sabnzbd.ini", source: PRODUCTION_SAB_CONFIG_SOURCE, mode: "ro", propagation: "rprivate", rw: false, type: "bind" },
|
|
396
394
|
]
|
|
397
395
|
const mountsExact = mounts.length === expectedMounts.length && expectedMounts.every((expected) => mounts.some((mount) => mount.destination === expected.destination && mount.source === expected.source && mount.mode === expected.mode && mount.propagation === expected.propagation && mount.rw === expected.rw && mount.type === expected.type))
|
|
398
396
|
const securityExact = value.privileged === false && (value.capAdd === null || (Array.isArray(value.capAdd) && value.capAdd.length === 0))
|
|
@@ -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.768</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -24,5 +24,4 @@
|
|
|
24
24
|
<Config Name="Runtime state" Target="/home/ouro/.ouro-cli" Default="/mnt/user/appdata/ouro-butler/runtime/.ouro-cli" Mode="rw" Description="Machine-scoped runtime" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ouro-butler/runtime/.ouro-cli</Config>
|
|
25
25
|
<Config Name="Agent bundle" Target="/home/ouro/AgentBundles/sanctuary.ouro" Default="/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro" Mode="rw" Description="Sanctuary bundle" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro</Config>
|
|
26
26
|
<Config Name="Privileged event spool" Target="/run/ouro-events" Default="/boot/config/custom/ouro-events/spool" Mode="ro" Description="Root-owned detector envelopes, read-only inside the Butler" Type="Path" Display="always" Required="true" Mask="false">/boot/config/custom/ouro-events/spool</Config>
|
|
27
|
-
<Config Name="SAB queue verification" Target="/run/sanctuary/sabnzbd.ini" Default="/mnt/user/appdata/sabnzbd/sabnzbd.ini" Mode="ro" Description="Existing SAB configuration used only for an independent read after a verified protective pause" Type="Path" Display="advanced" Required="true" Mask="false">/mnt/user/appdata/sabnzbd/sabnzbd.ini</Config>
|
|
28
27
|
</Container>
|
|
@@ -7,13 +7,11 @@ 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
9
|
"/boot/config/custom/ouro-events/spool:/run/ouro-events:ro",
|
|
10
|
-
"/mnt/user/appdata/sabnzbd/sabnzbd.ini:/run/sanctuary/sabnzbd.ini:ro",
|
|
11
10
|
];
|
|
12
11
|
const EXPECTED_MOUNTS = [
|
|
13
12
|
["/mnt/user/appdata/ouro-butler/runtime/.ouro-cli", "/home/ouro/.ouro-cli", true, "rprivate"],
|
|
14
13
|
["/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro", "/home/ouro/AgentBundles/sanctuary.ouro", true, "rprivate"],
|
|
15
14
|
["/boot/config/custom/ouro-events/spool", "/run/ouro-events", false, "rprivate"],
|
|
16
|
-
["/mnt/user/appdata/sabnzbd/sabnzbd.ini", "/run/sanctuary/sabnzbd.ini", false, "rprivate"],
|
|
17
15
|
];
|
|
18
16
|
const LEGACY_ALPHA742_IMAGE = "sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d";
|
|
19
17
|
const LEGACY_ALPHA742_MOUNTS = EXPECTED_MOUNTS.slice(0, 2);
|
|
@@ -152,10 +150,10 @@ function auditSanctuaryStagedFiles(input) {
|
|
|
152
150
|
const mode = match[1].match(/\bMode="([^"]+)"/u)?.[1];
|
|
153
151
|
return type === "Path" && target && mode ? `${match[2]}:${target}:${mode}` : "invalid";
|
|
154
152
|
});
|
|
155
|
-
if (configOpenCount !==
|
|
156
|
-
|| configEntries.length !==
|
|
153
|
+
if (configOpenCount !== EXPECTED_BINDS.length
|
|
154
|
+
|| configEntries.length !== EXPECTED_BINDS.length
|
|
157
155
|
|| JSON.stringify([...pathConfigs].sort()) !== JSON.stringify([...EXPECTED_BINDS].sort())) {
|
|
158
|
-
violations.push("template Config entries must equal the canonical
|
|
156
|
+
violations.push("template Config entries must equal the canonical path binds");
|
|
159
157
|
}
|
|
160
158
|
const postArgsOpenCount = [...input.templateXml.matchAll(/<PostArgs\b/gu)].length;
|
|
161
159
|
const postArgs = [...input.templateXml.matchAll(/<PostArgs(?:(?:\s*\/>)|>([^<]*)<\/PostArgs>)/gu)];
|
|
@@ -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;
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanctuaryFullVisibilityEmptyResponse = sanctuaryFullVisibilityEmptyResponse;
|
|
3
4
|
exports.sanctuaryFullVisibilityRequiredToolCalls = sanctuaryFullVisibilityRequiredToolCalls;
|
|
4
5
|
const runtime_1 = require("../nerves/runtime");
|
|
5
6
|
const REQUIRED_TOOL_NAMES = ["query_active_work", "query_cares", "unraid_get_system", "unraid_list_containers", "unraid_get_storage", "sanctuary_get_download_queue"];
|
|
6
7
|
const WHOLE_STATUS_REQUESTS = new Set(["what are you working on", "what's going on with sanctuary"]);
|
|
7
|
-
function unsupportedCurrentClaim(answer) {
|
|
8
|
+
function unsupportedCurrentClaim(answer, queueUnavailable) {
|
|
9
|
+
if (queueUnavailable && answer.trim().length === 0)
|
|
10
|
+
return "Give Ari the current results that did complete and say plainly that the download queue is currently unavailable; do not return an empty answer.";
|
|
11
|
+
if (queueUnavailable) {
|
|
12
|
+
const unsupportedQueueClaim = answer.split(/[,;!?\n]|(?<!\d)\.(?!\d)/u).some((clause) => {
|
|
13
|
+
if (!/\b(?:download(?:s| queue)?|queue|SABnzbd)\b/iu.test(clause))
|
|
14
|
+
return false;
|
|
15
|
+
const explicitlyUnverified = /\b(?:(?:cannot|can't|could not|couldn't|unable to) (?:currently )?(?:be )?(?:verif(?:y|ied)|read|check(?:ed)?|confirm(?:ed)?)|(?:do not|don't) know(?: whether)?|unknown|unavailable|unverified|stale|historical|previous|prior|failed)\b|\bneeds? (?:a )?(?:fresh |authoritative )*(?:check|read|verification)\b/iu.test(clause);
|
|
16
|
+
return !explicitlyUnverified;
|
|
17
|
+
});
|
|
18
|
+
if (unsupportedQueueClaim)
|
|
19
|
+
return "The current download-queue read is unavailable. Preserve the other current results, but do not claim a queue state; say plainly that downloads could not be verified.";
|
|
20
|
+
}
|
|
8
21
|
const unsupported = answer.replace(/docker\.img/giu, "docker image").split(/[,;!?\n]|(?<!\d)\.(?!\d)/u).some((sentence) => {
|
|
9
22
|
if (!/docker image(?: disk)?/iu.test(sentence))
|
|
10
23
|
return false;
|
|
@@ -24,43 +37,76 @@ function unsupportedCurrentClaim(answer) {
|
|
|
24
37
|
});
|
|
25
38
|
return unsupportedProviderClaim ? "The current queue read does not prove provider credit or authentication status. Do not report Astraweb, block-credit, or authentication failure as current; say that the provider needs a fresh authoritative check." : undefined;
|
|
26
39
|
}
|
|
40
|
+
function exactQueueUnavailableResult(value) {
|
|
41
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
42
|
+
return false;
|
|
43
|
+
const result = value;
|
|
44
|
+
if (JSON.stringify(Object.keys(result).sort()) !== JSON.stringify(["error", "observedAt", "ok"]))
|
|
45
|
+
return false;
|
|
46
|
+
if (result.ok !== false || typeof result.observedAt !== "string" || !Number.isFinite(Date.parse(result.observedAt)) || new Date(result.observedAt).toISOString() !== result.observedAt)
|
|
47
|
+
return false;
|
|
48
|
+
const error = result.error;
|
|
49
|
+
return !!error && typeof error === "object" && !Array.isArray(error)
|
|
50
|
+
&& JSON.stringify(Object.keys(error).sort()) === JSON.stringify(["code"])
|
|
51
|
+
&& ["credential_unavailable", "request_unavailable", "malformed_response"].includes(String(error.code));
|
|
52
|
+
}
|
|
27
53
|
function successfulCurrentResult(name, result) {
|
|
28
54
|
if (Buffer.byteLength(result, "utf8") > 1_000_000)
|
|
29
|
-
return false;
|
|
55
|
+
return { valid: false, queueUnavailable: false };
|
|
30
56
|
if (name === "query_active_work")
|
|
31
|
-
return result.trimStart().startsWith("this is my current top-level live world-state.");
|
|
57
|
+
return { valid: result.trimStart().startsWith("this is my current top-level live world-state."), queueUnavailable: false };
|
|
32
58
|
try {
|
|
33
59
|
const parsed = JSON.parse(result);
|
|
34
60
|
if (name === "query_cares")
|
|
35
|
-
return Array.isArray(parsed);
|
|
61
|
+
return { valid: Array.isArray(parsed), queueUnavailable: false };
|
|
36
62
|
if (name === "sanctuary_get_download_queue") {
|
|
63
|
+
if (exactQueueUnavailableResult(parsed))
|
|
64
|
+
return { valid: true, queueUnavailable: true };
|
|
37
65
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
38
|
-
return false;
|
|
66
|
+
return { valid: false, queueUnavailable: false };
|
|
39
67
|
const queue = parsed;
|
|
40
68
|
const exactKeys = ["observedAt", "paused", "queuedJobs", "stateDigest", "status"];
|
|
41
|
-
return JSON.stringify(Object.keys(queue).sort()) === JSON.stringify(exactKeys)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
69
|
+
return { valid: JSON.stringify(Object.keys(queue).sort()) === JSON.stringify(exactKeys)
|
|
70
|
+
&& typeof queue.paused === "boolean"
|
|
71
|
+
&& typeof queue.status === "string" && Buffer.byteLength(queue.status, "utf8") <= 64
|
|
72
|
+
&& Number.isSafeInteger(queue.queuedJobs) && Number(queue.queuedJobs) >= 0 && Number(queue.queuedJobs) <= 1_000_000
|
|
73
|
+
&& typeof queue.observedAt === "string" && Number.isFinite(Date.parse(queue.observedAt)) && new Date(queue.observedAt).toISOString() === queue.observedAt
|
|
74
|
+
&& typeof queue.stateDigest === "string" && /^[a-f0-9]{64}$/u.test(queue.stateDigest), queueUnavailable: false };
|
|
47
75
|
}
|
|
48
|
-
return !!parsed && typeof parsed === "object" && !Array.isArray(parsed) && parsed.ok === true;
|
|
76
|
+
return { valid: !!parsed && typeof parsed === "object" && !Array.isArray(parsed) && parsed.ok === true, queueUnavailable: false };
|
|
49
77
|
}
|
|
50
78
|
catch {
|
|
51
|
-
return false;
|
|
79
|
+
return { valid: false, queueUnavailable: false };
|
|
52
80
|
}
|
|
53
81
|
}
|
|
82
|
+
function normalizedRequest(request) {
|
|
83
|
+
return request.normalize("NFKC").trim().toLocaleLowerCase("en-US").replaceAll("’", "'").replace(/[?!.\s]+$/gu, "");
|
|
84
|
+
}
|
|
85
|
+
function sanctuaryFullVisibilityEmptyResponse(request) {
|
|
86
|
+
return WHOLE_STATUS_REQUESTS.has(normalizedRequest(request))
|
|
87
|
+
? "I couldn't finish a trustworthy Sanctuary status check because a current check was unavailable. I won't guess or reuse old alerts; please try again shortly."
|
|
88
|
+
: undefined;
|
|
89
|
+
}
|
|
54
90
|
function sanctuaryFullVisibilityRequiredToolCalls(request, advertisedToolNames) {
|
|
55
|
-
const normalized = request
|
|
91
|
+
const normalized = normalizedRequest(request);
|
|
56
92
|
if (!WHOLE_STATUS_REQUESTS.has(normalized) || !REQUIRED_TOOL_NAMES.every((name) => advertisedToolNames.includes(name)))
|
|
57
93
|
return undefined;
|
|
58
94
|
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_full_visibility_reads_required", message: "required current Sanctuary visibility reads", meta: { toolCount: REQUIRED_TOOL_NAMES.length } });
|
|
95
|
+
let queueUnavailable = false;
|
|
96
|
+
const completed = new Set();
|
|
59
97
|
return {
|
|
60
98
|
names: REQUIRED_TOOL_NAMES,
|
|
61
99
|
retryMessage: "Before answering, read current active work, cares, system health, service state, storage, and the download queue. Current tool facts outrank care history; a stale care is a recheck item, not a present-tense fact. Then give Ari one compact household summary; do not ask him to choose a status slice.",
|
|
62
100
|
requireSuccessfulResults: true,
|
|
63
|
-
validateRequiredToolResult:
|
|
64
|
-
|
|
101
|
+
validateRequiredToolResult: (name, result) => {
|
|
102
|
+
const validation = successfulCurrentResult(name, result);
|
|
103
|
+
if (name === "sanctuary_get_download_queue" && validation.valid)
|
|
104
|
+
queueUnavailable = validation.queueUnavailable;
|
|
105
|
+
if (validation.valid)
|
|
106
|
+
completed.add(name);
|
|
107
|
+
return validation.valid;
|
|
108
|
+
},
|
|
109
|
+
validateTerminalAnswer: (answer) => unsupportedCurrentClaim(answer, queueUnavailable),
|
|
110
|
+
emptyResponseFallback: () => REQUIRED_TOOL_NAMES.every((name) => completed.has(name)) ? sanctuaryFullVisibilityEmptyResponse(request) : undefined,
|
|
65
111
|
};
|
|
66
112
|
}
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.runWithSanctuaryToolReceiptCollection = runWithSanctuaryToolReceiptCollection;
|
|
37
|
+
exports.loadSanctuarySabApiKey = loadSanctuarySabApiKey;
|
|
37
38
|
exports.createSanctuaryToolContext = createSanctuaryToolContext;
|
|
38
39
|
const fs = __importStar(require("node:fs"));
|
|
39
40
|
const path = __importStar(require("node:path"));
|
|
@@ -92,6 +93,9 @@ function required(config, field) {
|
|
|
92
93
|
throw new Error(`Sanctuary ${field} is missing`);
|
|
93
94
|
return value.trim();
|
|
94
95
|
}
|
|
96
|
+
async function loadSanctuarySabApiKey(agentName) {
|
|
97
|
+
return required(machineConfig(agentName), "sabnzbdApiKey");
|
|
98
|
+
}
|
|
95
99
|
function requiredObject(config, field) {
|
|
96
100
|
const value = config[field];
|
|
97
101
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -192,7 +196,7 @@ function createSanctuaryToolContext(agentName) {
|
|
|
192
196
|
const endpoint = required(initial, "unraidGraphqlUrl");
|
|
193
197
|
const readClient = new unraid_client_1.UnraidClient({ endpoint, apiKey: required(initial, "unraidReadApiKey") });
|
|
194
198
|
const reads = (0, tools_unraid_1.createUnraidReadTools)(readClient);
|
|
195
|
-
const sab = (0, sanctuary_sab_1.createSanctuarySabClient)();
|
|
199
|
+
const sab = (0, sanctuary_sab_1.createSanctuarySabClient)({ loadApiKey: () => loadSanctuarySabApiKey(agentName) });
|
|
196
200
|
const mediaOptimization = (0, sanctuary_media_optimization_1.createSanctuaryMediaOptimizationClient)(optionalJellyfin(initial));
|
|
197
201
|
const acceptanceRead = (toolName, read) => async (...args) => {
|
|
198
202
|
try {
|
|
@@ -238,7 +242,17 @@ function createSanctuaryToolContext(agentName) {
|
|
|
238
242
|
const services = await Promise.all(sanctuary_health_1.SANCTUARY_PUBLIC_ENDPOINTS.map(async (url) => ({ name: new URL(url).hostname.split(".")[0], ...await (0, sanctuary_health_1.probeSanctuaryEndpoint)(url) })));
|
|
239
243
|
return { ok: true, data: { observedAt: new Date().toISOString(), services, degraded: services.some((service) => !service.ok) } };
|
|
240
244
|
}),
|
|
241
|
-
getDownloadQueue: acceptanceRead("sanctuary_get_download_queue",
|
|
245
|
+
getDownloadQueue: acceptanceRead("sanctuary_get_download_queue", async () => {
|
|
246
|
+
try {
|
|
247
|
+
return await sab.readQueue();
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
const code = (0, sanctuary_sab_1.sanctuarySabReadUnavailableCode)(error);
|
|
251
|
+
if (code)
|
|
252
|
+
return { ok: false, error: { code }, observedAt: new Date().toISOString() };
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}),
|
|
242
256
|
getMediaOptimization: acceptanceRead("sanctuary_get_media_optimization", mediaOptimization.read),
|
|
243
257
|
resumeDownloadQueue: async () => {
|
|
244
258
|
try {
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SANCTUARY_SAB_CREDENTIAL_UNAVAILABLE = void 0;
|
|
4
|
+
exports.sanctuarySabReadUnavailableCode = sanctuarySabReadUnavailableCode;
|
|
3
5
|
exports.createSanctuarySabClient = createSanctuarySabClient;
|
|
4
6
|
const node_crypto_1 = require("node:crypto");
|
|
5
|
-
const node_fs_1 = require("node:fs");
|
|
6
7
|
const runtime_1 = require("../nerves/runtime");
|
|
7
|
-
const DEFAULT_INI_PATH = "/run/sanctuary/sabnzbd.ini";
|
|
8
8
|
const BASE_URL = "http://127.0.0.1:8090/api";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return value;
|
|
9
|
+
exports.SANCTUARY_SAB_CREDENTIAL_UNAVAILABLE = "SAB queue verification credential is unavailable";
|
|
10
|
+
function sanctuarySabReadUnavailableCode(error) {
|
|
11
|
+
if (!(error instanceof Error))
|
|
12
|
+
return undefined;
|
|
13
|
+
if (error.message === exports.SANCTUARY_SAB_CREDENTIAL_UNAVAILABLE)
|
|
14
|
+
return "credential_unavailable";
|
|
15
|
+
if (error.message === "SAB queue request failed")
|
|
16
|
+
return "request_unavailable";
|
|
17
|
+
if (error.message === "SAB queue response is malformed")
|
|
18
|
+
return "malformed_response";
|
|
19
|
+
return undefined;
|
|
21
20
|
}
|
|
22
21
|
function queuedJobs(value) {
|
|
23
22
|
const parsed = typeof value === "string" && /^\d{1,7}$/u.test(value) ? Number(value) : value;
|
|
@@ -25,15 +24,21 @@ function queuedJobs(value) {
|
|
|
25
24
|
throw new Error("SAB queue response is malformed");
|
|
26
25
|
return Number(parsed);
|
|
27
26
|
}
|
|
28
|
-
function createSanctuarySabClient(options
|
|
29
|
-
const iniPath = options.iniPath ?? DEFAULT_INI_PATH;
|
|
27
|
+
function createSanctuarySabClient(options) {
|
|
30
28
|
const fetchImpl = options.fetch ?? fetch;
|
|
31
|
-
let cachedApiKey = null;
|
|
32
29
|
const request = async (mode) => {
|
|
33
|
-
|
|
30
|
+
let apiKey;
|
|
31
|
+
try {
|
|
32
|
+
apiKey = (await options.loadApiKey()).trim();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error(exports.SANCTUARY_SAB_CREDENTIAL_UNAVAILABLE);
|
|
36
|
+
}
|
|
37
|
+
if (!apiKey)
|
|
38
|
+
throw new Error(exports.SANCTUARY_SAB_CREDENTIAL_UNAVAILABLE);
|
|
34
39
|
let response;
|
|
35
40
|
try {
|
|
36
|
-
response = await fetchImpl(`${BASE_URL}?mode=${mode}${mode === "queue" ? "&output=json" : ""}&apikey=${encodeURIComponent(
|
|
41
|
+
response = await fetchImpl(`${BASE_URL}?mode=${mode}${mode === "queue" ? "&output=json" : ""}&apikey=${encodeURIComponent(apiKey)}`, { signal: AbortSignal.timeout(15_000) });
|
|
37
42
|
}
|
|
38
43
|
catch {
|
|
39
44
|
throw new Error("SAB queue request failed");
|
|
@@ -430,14 +430,15 @@ async function runSenseTurn(options) {
|
|
|
430
430
|
if (persistPromise)
|
|
431
431
|
await persistPromise;
|
|
432
432
|
const postTurnSession = (0, context_1.loadSession)(sessPath);
|
|
433
|
+
const emptyFallback = options.emptyResponseFallback?.();
|
|
433
434
|
if (postTurnSession?.messages) {
|
|
434
435
|
const recovered = extractOutwardSenseDeliveryText(postTurnSession.messages);
|
|
435
|
-
finalResponse = recovered ?? "(agent responded but response was empty)";
|
|
436
|
+
finalResponse = recovered ?? emptyFallback ?? "(agent responded but response was empty)";
|
|
436
437
|
if (recovered)
|
|
437
438
|
responseCausalSessionEventId = newOutwardCoordinates(persistedEvents, existingEventIds).at(-1)?.eventId;
|
|
438
439
|
}
|
|
439
440
|
else {
|
|
440
|
-
finalResponse = "(agent responded but response was empty)";
|
|
441
|
+
finalResponse = emptyFallback ?? "(agent responded but response was empty)";
|
|
441
442
|
}
|
|
442
443
|
}
|
|
443
444
|
else {
|
package/dist/senses/telegram.js
CHANGED
|
@@ -63,11 +63,12 @@ const runtime_credentials_1 = require("../heart/runtime-credentials");
|
|
|
63
63
|
const runtime_1 = require("../nerves/runtime");
|
|
64
64
|
const nerves_1 = require("../nerves");
|
|
65
65
|
const sanctuary_interactive_control_1 = require("./sanctuary-interactive-control");
|
|
66
|
+
const sanctuary_runtime_1 = require("./sanctuary-runtime");
|
|
66
67
|
const sanctuary_sab_1 = require("./sanctuary-sab");
|
|
67
68
|
const sanctuary_download_credit_presentation_1 = require("./sanctuary-download-credit-presentation");
|
|
68
69
|
const shared_turn_1 = require("./shared-turn");
|
|
69
70
|
const telegram_client_1 = require("./telegram-client");
|
|
70
|
-
const
|
|
71
|
+
const sanctuary_runtime_2 = require("./sanctuary-runtime");
|
|
71
72
|
const sanctuary_full_visibility_contract_1 = require("./sanctuary-full-visibility-contract");
|
|
72
73
|
const sanctuary_storage_optimization_contract_1 = require("./sanctuary-storage-optimization-contract");
|
|
73
74
|
const sanctuary_grounding_1 = require("./sanctuary-grounding");
|
|
@@ -87,11 +88,14 @@ const pending_1 = require("../mind/pending");
|
|
|
87
88
|
const relationship_authorization_1 = require("../repertoire/relationship-authorization");
|
|
88
89
|
const telegram_admission_1 = require("./telegram-admission");
|
|
89
90
|
const telegram_effect_adapter_1 = require("./telegram-effect-adapter");
|
|
90
|
-
|
|
91
|
+
function createFullVisibilityProgress() {
|
|
92
|
+
const progress = {};
|
|
93
|
+
return { progress, emptyResponseFallback: () => progress.fallback?.() };
|
|
94
|
+
}
|
|
91
95
|
function createSabQueueProtectiveStateVerifier(options = {}) {
|
|
92
96
|
let client = null;
|
|
93
97
|
return async (action) => {
|
|
94
|
-
client ??= (0, sanctuary_sab_1.createSanctuarySabClient)({
|
|
98
|
+
client ??= (0, sanctuary_sab_1.createSanctuarySabClient)({ loadApiKey: options.loadApiKey ?? (() => (0, sanctuary_runtime_1.loadSanctuarySabApiKey)("sanctuary")), fetch: options.fetch, now: options.now });
|
|
95
99
|
const snapshot = await client.readQueue();
|
|
96
100
|
const paused = snapshot.paused;
|
|
97
101
|
const digest = (0, node_crypto_1.createHash)("sha256").update(`sabnzbd.queue.paused=${String(paused)}`).digest("hex");
|
|
@@ -685,7 +689,7 @@ function createTelegramSenseApp(options) {
|
|
|
685
689
|
? new telegram_admission_1.FileTelegramAdmissionStore(path.join(agentRoot, "state", "senses", "telegram", "admissions"))
|
|
686
690
|
: undefined;
|
|
687
691
|
const runTurn = options.runTurn ?? options._runTurn ?? shared_turn_1.runSenseTurn;
|
|
688
|
-
const collectToolReceipts = options._runWithToolReceiptCollection ??
|
|
692
|
+
const collectToolReceipts = options._runWithToolReceiptCollection ?? sanctuary_runtime_2.runWithSanctuaryToolReceiptCollection;
|
|
689
693
|
const useSanctuaryRuntime = options.agentName === "sanctuary" && !options.runTurn;
|
|
690
694
|
const readScenarioHandleDigest = () => (options.acceptanceMarker
|
|
691
695
|
? options.acceptanceMarker()
|
|
@@ -878,7 +882,7 @@ function createTelegramSenseApp(options) {
|
|
|
878
882
|
let approvalTransport;
|
|
879
883
|
let interactiveControl;
|
|
880
884
|
try {
|
|
881
|
-
toolContext = useSanctuaryRuntime ? (options._toolContext ?? (0,
|
|
885
|
+
toolContext = useSanctuaryRuntime ? (options._toolContext ?? (0, sanctuary_runtime_2.createSanctuaryToolContext)(options.agentName)) : undefined;
|
|
882
886
|
if (toolContext && options.telegramContactManager)
|
|
883
887
|
toolContext.telegramContactManager = options.telegramContactManager;
|
|
884
888
|
approvalRuntime = options.approvalRuntime ?? (useSanctuaryRuntime ? (options._createApprovalRuntime ?? telegram_approval_runtime_1.createTelegramApprovalRuntime)({
|
|
@@ -1076,10 +1080,21 @@ function createTelegramSenseApp(options) {
|
|
|
1076
1080
|
};
|
|
1077
1081
|
return async ({ runAgentOptions }) => {
|
|
1078
1082
|
const relationshipAuthorization = await resolveLiveRelationshipAuthorization();
|
|
1079
|
-
const
|
|
1083
|
+
const storageOptimization = options.agentName === "sanctuary" && relationshipAuthorization.profileId === "sanctuary-owner"
|
|
1080
1084
|
? (0, sanctuary_storage_optimization_contract_1.sanctuaryStorageOptimizationRequiredToolCalls)(input.userMessage, relationshipAuthorization.advertisedToolNames)
|
|
1081
|
-
?? (0, sanctuary_full_visibility_contract_1.sanctuaryFullVisibilityRequiredToolCalls)(input.userMessage, relationshipAuthorization.advertisedToolNames)
|
|
1082
1085
|
: undefined;
|
|
1086
|
+
const fullVisibility = options.agentName === "sanctuary" && relationshipAuthorization.profileId === "sanctuary-owner" && !storageOptimization
|
|
1087
|
+
? (0, sanctuary_full_visibility_contract_1.sanctuaryFullVisibilityRequiredToolCalls)(input.userMessage, relationshipAuthorization.advertisedToolNames)
|
|
1088
|
+
: undefined;
|
|
1089
|
+
if (input.fullVisibilityProgress && fullVisibility)
|
|
1090
|
+
input.fullVisibilityProgress.fallback = fullVisibility.emptyResponseFallback;
|
|
1091
|
+
const requiredToolCalls = storageOptimization ?? (fullVisibility ? {
|
|
1092
|
+
names: fullVisibility.names,
|
|
1093
|
+
retryMessage: fullVisibility.retryMessage,
|
|
1094
|
+
requireSuccessfulResults: fullVisibility.requireSuccessfulResults,
|
|
1095
|
+
validateRequiredToolResult: fullVisibility.validateRequiredToolResult,
|
|
1096
|
+
validateTerminalAnswer: fullVisibility.validateTerminalAnswer,
|
|
1097
|
+
} : undefined);
|
|
1083
1098
|
return {
|
|
1084
1099
|
...runAgentOptions,
|
|
1085
1100
|
...(requiredToolCalls ? { requiredToolCalls } : {}),
|
|
@@ -1118,6 +1133,7 @@ function createTelegramSenseApp(options) {
|
|
|
1118
1133
|
},
|
|
1119
1134
|
}
|
|
1120
1135
|
: undefined;
|
|
1136
|
+
const fullVisibility = createFullVisibilityProgress();
|
|
1121
1137
|
const result = await runTurn({
|
|
1122
1138
|
agentName: options.agentName,
|
|
1123
1139
|
channel: "telegram",
|
|
@@ -1130,7 +1146,8 @@ function createTelegramSenseApp(options) {
|
|
|
1130
1146
|
...(orientationFrame ? { orientationFrame } : {}),
|
|
1131
1147
|
toolContext: { ...(toolContext ?? {}), attachmentIds: input.attachmentIds ?? [] },
|
|
1132
1148
|
prepareRunAgentOptions: prepareRelationshipRunAgentOptions({ friendId: input.friendId, requestId: input.requestId,
|
|
1133
|
-
sessionEventId: input.eventId, userId: input.userId, chatId: input.chatId, sessionKey: input.sessionKey, userMessage: input.text }),
|
|
1149
|
+
sessionEventId: input.eventId, userId: input.userId, chatId: input.chatId, sessionKey: input.sessionKey, userMessage: input.text, fullVisibilityProgress: fullVisibility.progress }),
|
|
1150
|
+
emptyResponseFallback: fullVisibility.emptyResponseFallback,
|
|
1134
1151
|
deliverySink: { onDelivery: (delivery) => deliver(delivery.text, delivery.kind === "settle") },
|
|
1135
1152
|
});
|
|
1136
1153
|
if (effects.length === 0 && result.response.trim())
|
|
@@ -1309,6 +1326,7 @@ function createTelegramSenseApp(options) {
|
|
|
1309
1326
|
inbound: { text: message.text, reference: inboundReference, attachmentIds: hydrated.attachmentIds, relations: ingressRelations },
|
|
1310
1327
|
})
|
|
1311
1328
|
: null;
|
|
1329
|
+
const fullVisibility = createFullVisibilityProgress();
|
|
1312
1330
|
const collected = await collectToolReceipts(() => runTurn({
|
|
1313
1331
|
agentName: options.agentName,
|
|
1314
1332
|
channel: "telegram",
|
|
@@ -1324,6 +1342,7 @@ function createTelegramSenseApp(options) {
|
|
|
1324
1342
|
ingressRelations,
|
|
1325
1343
|
...(ingressReceipt ? { precommittedIngress: ingressReceipt } : {}),
|
|
1326
1344
|
turnMetricsObserver,
|
|
1345
|
+
emptyResponseFallback: fullVisibility.emptyResponseFallback,
|
|
1327
1346
|
deliverySink: {
|
|
1328
1347
|
onDelivery: async (delivery) => {
|
|
1329
1348
|
if (groundingIntentTool) {
|
|
@@ -1347,6 +1366,7 @@ function createTelegramSenseApp(options) {
|
|
|
1347
1366
|
chatId: message.chatId,
|
|
1348
1367
|
sessionKey: currentSessionKey,
|
|
1349
1368
|
userMessage: message.text,
|
|
1369
|
+
fullVisibilityProgress: fullVisibility.progress,
|
|
1350
1370
|
}),
|
|
1351
1371
|
} : {}),
|
|
1352
1372
|
...(approvalRuntime ? { approvalCoordinatorFactory: approvalRuntime.coordinator } : {}),
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.768",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.768",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|