@ouro.bot/cli 0.1.0-alpha.773 → 0.1.0-alpha.774
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 +6 -0
- package/deploy/unraid/README.txt +34 -73
- package/deploy/unraid/sanctuary-acceptance-adapter.sh +6 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +1 -4
- package/deploy/unraid/sanctuary-acceptance-harness.sh +1 -1
- package/deploy/unraid/sanctuary-unit16-run.sh +5 -6
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/daemon/cli-help.js +3 -3
- package/dist/heart/daemon/cli-parse.js +5 -0
- package/dist/heart/daemon/daemon.js +20 -0
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +52 -21
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +69 -23
- package/dist/heart/external-events/router.js +204 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
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.774",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Make Sanctuary updates side-effect-safe, move Telegram acceptance to canonical vault credentials, and repair consumed event recoveries through a reviewed daemon command."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.773",
|
|
6
12
|
"changes": [
|
package/deploy/unraid/README.txt
CHANGED
|
@@ -1331,6 +1331,21 @@ NODE
|
|
|
1331
1331
|
! docker container inspect ouro-butler-provider-readiness >/dev/null 2>&1 || return 1
|
|
1332
1332
|
validate_sanctuary_roots "$READINESS_RUNTIME_ROOT" "$READINESS_AGENT_ROOT" || return $?
|
|
1333
1333
|
}
|
|
1334
|
+
verify_sanctuary_telegram_readiness() {
|
|
1335
|
+
TELEGRAM_READINESS_IMAGE_ID=$1
|
|
1336
|
+
validate_exact_image_id "$TELEGRAM_READINESS_IMAGE_ID" || return $?
|
|
1337
|
+
TELEGRAM_READINESS_RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
|
|
1338
|
+
TELEGRAM_READINESS_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
|
|
1339
|
+
! docker container inspect ouro-butler-telegram-readiness >/dev/null 2>&1 || return 1
|
|
1340
|
+
docker run --rm --pull=never --network host --name ouro-butler-telegram-readiness --user 10001:10001 \
|
|
1341
|
+
--read-only --cap-drop ALL --security-opt no-new-privileges \
|
|
1342
|
+
--mount "type=bind,src=$TELEGRAM_READINESS_RUNTIME_ROOT,dst=/home/ouro/.ouro-cli" \
|
|
1343
|
+
--mount "type=bind,src=$TELEGRAM_READINESS_AGENT_ROOT,dst=/home/ouro/AgentBundles/sanctuary.ouro,readonly" \
|
|
1344
|
+
--entrypoint /opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh \
|
|
1345
|
+
"$TELEGRAM_READINESS_IMAGE_ID" telegram-readiness >/dev/null || return $?
|
|
1346
|
+
! docker container inspect ouro-butler-telegram-readiness >/dev/null 2>&1 || return 1
|
|
1347
|
+
validate_sanctuary_roots "$TELEGRAM_READINESS_RUNTIME_ROOT" "$TELEGRAM_READINESS_AGENT_ROOT" || return $?
|
|
1348
|
+
}
|
|
1334
1349
|
run_sanctuary_docker() {
|
|
1335
1350
|
/usr/bin/timeout -s KILL 20 /usr/bin/docker "$@"
|
|
1336
1351
|
}
|
|
@@ -1676,11 +1691,12 @@ Update:
|
|
|
1676
1691
|
cleanup_event_asset_stage
|
|
1677
1692
|
trap - EXIT
|
|
1678
1693
|
if provision_sanctuary_sab_credential "$IMAGE_ID" \
|
|
1679
|
-
&& verify_sanctuary_sab_readiness "$IMAGE_ID"
|
|
1694
|
+
&& verify_sanctuary_sab_readiness "$IMAGE_ID" \
|
|
1695
|
+
&& verify_sanctuary_telegram_readiness "$IMAGE_ID"; then
|
|
1680
1696
|
:
|
|
1681
1697
|
else
|
|
1682
|
-
|
|
1683
|
-
(exit "$
|
|
1698
|
+
PRECUTOVER_READINESS_STATUS=$?
|
|
1699
|
+
(exit "$PRECUTOVER_READINESS_STATUS")
|
|
1684
1700
|
fi
|
|
1685
1701
|
Guard the atomic autostart disable separately. If it fails, production has not
|
|
1686
1702
|
been touched and the captured status is propagated:
|
|
@@ -1734,7 +1750,7 @@ ouro-butler-rollback
|
|
|
1734
1750
|
readiness and autostart have passed, commit first durably renames the record;
|
|
1735
1751
|
a kill then keeps target production in place and the next run validates its
|
|
1736
1752
|
exact topology/readiness before finishing commit. Migration failure then enters the same exact container
|
|
1737
|
-
rollback arm before
|
|
1753
|
+
rollback arm before target production starts:
|
|
1738
1754
|
if docker stop ouro-butler \
|
|
1739
1755
|
&& remove_stopped_rollback_if_present "$ROLLBACK_IMAGE_ID" \
|
|
1740
1756
|
&& docker rename ouro-butler ouro-butler-rollback \
|
|
@@ -1778,52 +1794,13 @@ ouro-butler-rollback
|
|
|
1778
1794
|
both recoveries revalidate, start, bounded-wait, atomically restore production-only
|
|
1779
1795
|
autostart, and propagate the original failure. If neither exact container can
|
|
1780
1796
|
be found, the failure propagates with Butler autostart disabled.
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
1789
|
-
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1790
|
-
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
1791
|
-
"$IMAGE_ID" \
|
|
1792
|
-
&& audit_effective ouro-butler-staging "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" \
|
|
1793
|
-
&& docker start ouro-butler-staging \
|
|
1794
|
-
&& assert_only_running_butler ouro-butler-staging \
|
|
1795
|
-
&& wait_butler_ready ouro-butler-staging \
|
|
1796
|
-
&& docker stop ouro-butler-staging \
|
|
1797
|
-
&& docker rm ouro-butler-staging \
|
|
1798
|
-
&& assert_only_running_butler -; then
|
|
1799
|
-
:
|
|
1800
|
-
else
|
|
1801
|
-
STAGING_ACTIVATION_STATUS=$?
|
|
1802
|
-
if docker container inspect ouro-butler-staging >/dev/null 2>&1; then
|
|
1803
|
-
docker stop ouro-butler-staging >/dev/null 2>&1 || true
|
|
1804
|
-
PARTIAL_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-staging)
|
|
1805
|
-
test "$PARTIAL_STAGING_IMAGE_ID" = "$IMAGE_ID"
|
|
1806
|
-
docker rm --force ouro-butler-staging >/dev/null 2>&1 || true
|
|
1807
|
-
fi
|
|
1808
|
-
! docker container inspect ouro-butler-staging >/dev/null 2>&1
|
|
1809
|
-
CURRENT_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback)
|
|
1810
|
-
test "$CURRENT_ROLLBACK_IMAGE_ID" = "$ROLLBACK_IMAGE_ID"
|
|
1811
|
-
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" rollback
|
|
1812
|
-
docker rename ouro-butler-rollback ouro-butler
|
|
1813
|
-
assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1814
|
-
docker start ouro-butler
|
|
1815
|
-
assert_only_running_butler ouro-butler
|
|
1816
|
-
wait_butler_ready ouro-butler
|
|
1817
|
-
enable_butler_autostart
|
|
1818
|
-
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" commit
|
|
1819
|
-
(exit "$STAGING_ACTIVATION_STATUS")
|
|
1820
|
-
fi
|
|
1821
|
-
The failure arm safely handles staging that was never created, remains
|
|
1822
|
-
stopped, is running, or exited: it force-removes any partial staging state,
|
|
1823
|
-
verifies the name is absent, restores and revalidates the old production against
|
|
1824
|
-
its exact pre-recorded image ID, starts and bounded-waits it, atomically
|
|
1825
|
-
restores production-only autostart, and propagates the original failure.
|
|
1826
|
-
At no point may production and staging run together against the same Telegram token.
|
|
1797
|
+
Do not start a target-image daemon between the production rename and final
|
|
1798
|
+
production activation. The exact-image static audit, SAB readiness, and
|
|
1799
|
+
vault-backed Telegram identity check have already passed before autostart or
|
|
1800
|
+
container mutation. Provider and complete daemon readiness are exercised only
|
|
1801
|
+
by the transactional production activation below, whose failure arm restores
|
|
1802
|
+
and revalidates the exact prior production. This prevents a disposable daemon
|
|
1803
|
+
from reconciling or claiming live external-event state before cutover.
|
|
1827
1804
|
Create and activate production from the same exact image ID and exact authority
|
|
1828
1805
|
in one explicit conditional so `set -eu` cannot exit before rollback. Only a
|
|
1829
1806
|
successful create, effective audit, start, stopped rollback assertion, and
|
|
@@ -2288,28 +2265,12 @@ Packaged Unit 16 acceptance execution:
|
|
|
2288
2265
|
the config from the packaged fixed contract and requires byte-for-byte equality.
|
|
2289
2266
|
Unit 16d-2 stops at the pre-model quarantine boundary: use a genuinely distinct private Telegram sender, confirm the fixed acknowledgement and owner admission card, and do not approve the contact during this scenario. The production-identical allow-to-one-turn continuation is covered by the Telegram admission integration suite when a second live account is unavailable. Unit 16h is acceptance-only: it exercises the delivery path against isolated state, restores exact health and cron bytes, and does not activate a production daily digest.
|
|
2290
2267
|
The cursor snapshot is deliberately materialized and executed twice around the
|
|
2291
|
-
live scenario. Telegram bootstrap
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
reading the token, opens an anonymous descriptor for the launcher, and unsets
|
|
2298
|
-
the short-lived shell value on either launcher success or failure:
|
|
2299
|
-
run_unit16_telegram_bootstrap() {
|
|
2300
|
-
local UNIT16_BOT_TOKEN UNIT16_BOT_STATUS
|
|
2301
|
-
printf 'Telegram bot token: ' >&2
|
|
2302
|
-
IFS= read -r -s UNIT16_BOT_TOKEN || return $?
|
|
2303
|
-
printf '\n' >&2
|
|
2304
|
-
if "$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final telegram-bootstrap telegram-bootstrap.json \
|
|
2305
|
-
3< <(printf '%s\n' "$UNIT16_BOT_TOKEN"); then
|
|
2306
|
-
UNIT16_BOT_STATUS=0
|
|
2307
|
-
else
|
|
2308
|
-
UNIT16_BOT_STATUS=$?
|
|
2309
|
-
fi
|
|
2310
|
-
unset UNIT16_BOT_TOKEN
|
|
2311
|
-
return "$UNIT16_BOT_STATUS"
|
|
2312
|
-
}
|
|
2268
|
+
live scenario. Telegram bootstrap refreshes the canonical agent vault
|
|
2269
|
+
`runtime/config` and keeps the bot token inside the consuming harness process.
|
|
2270
|
+
It never reads the retired container credential file or carries the token in a
|
|
2271
|
+
descriptor, argument, environment variable, shell variable, config, evidence,
|
|
2272
|
+
or output. Callback injection alone maps its reviewed saved callback-update
|
|
2273
|
+
JSON from host fd 3 through Docker stdin to in-container fd 3.
|
|
2313
2274
|
Stage the reviewed callback JSON at the fixed path below in the root-owned
|
|
2314
2275
|
tmpfs inbox, then use this single fail-closed helper. It opens the input once,
|
|
2315
2276
|
validates the opened descriptor and its original path refer to the same
|
|
@@ -2378,7 +2339,7 @@ Packaged Unit 16 acceptance execution:
|
|
|
2378
2339
|
)
|
|
2379
2340
|
}
|
|
2380
2341
|
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize telegram-bootstrap
|
|
2381
|
-
|
|
2342
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final telegram-bootstrap telegram-bootstrap.json
|
|
2382
2343
|
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize cursor-snapshot before
|
|
2383
2344
|
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final cursor-snapshot cursor-snapshot.json
|
|
2384
2345
|
# Perform the live scenario whose cursor movement is being measured.
|
|
@@ -6,6 +6,7 @@ VAULT_ENTRY='import("/opt/ouro/dist/heart/daemon/sanctuary-acceptance-adapter.js
|
|
|
6
6
|
REVOKED_ENTRY='import fs from "node:fs"; import("/opt/ouro/dist/heart/daemon/sanctuary-acceptance-adapter.js").then(async (module) => { const result = await module.executeSanctuaryAcceptanceRevokedProbe(process.argv[1], process.argv[2], fs.readFileSync(3, "utf8")); process.stdout.write(JSON.stringify(result)); }).catch(() => { process.exitCode = 1; });'
|
|
7
7
|
CALLBACK_ENTRY='import fs from "node:fs"; import("/opt/ouro/dist/heart/daemon/sanctuary-acceptance-adapter.js").then(async (module) => { const result = await module.executeSanctuaryAcceptanceCallbackProbe(JSON.parse(fs.readFileSync(0, "utf8")), process.argv[1] === "replay"); process.stdout.write(JSON.stringify(result)); }).catch(() => { process.exitCode = 1; });'
|
|
8
8
|
MATERIALIZE_ENTRY='import("/opt/ouro/dist/heart/daemon/sanctuary-acceptance-adapter.js").then(async (module) => { const payload = { operation: "materialize_config", command: process.argv[1] }; if (process.argv[2]) payload.phase = process.argv[2]; const result = await module.executeSanctuaryAcceptanceAdapter(payload); process.stdout.write(JSON.stringify(result)); }).catch(() => { process.exitCode = 1; });'
|
|
9
|
+
TELEGRAM_READINESS_ENTRY='import("/opt/ouro/dist/heart/daemon/sanctuary-acceptance-adapter.js").then(async (module) => { const result = await module.executeSanctuaryAcceptanceAdapter({ operation: "telegram_readiness" }); process.stdout.write(JSON.stringify(result)); }).catch(() => { process.exitCode = 1; });'
|
|
9
10
|
|
|
10
11
|
if test "${1:-}" = vault-probe; then
|
|
11
12
|
test "$#" -eq 3 || exit 2
|
|
@@ -29,6 +30,11 @@ if test "${1:-}" = materialize-config; then
|
|
|
29
30
|
exec node --input-type=module -e "$MATERIALIZE_ENTRY" "$2" "${3:-}"
|
|
30
31
|
fi
|
|
31
32
|
|
|
33
|
+
if test "${1:-}" = telegram-readiness; then
|
|
34
|
+
test "$#" -eq 1 || exit 2
|
|
35
|
+
exec node --input-type=module -e "$TELEGRAM_READINESS_ENTRY"
|
|
36
|
+
fi
|
|
37
|
+
|
|
32
38
|
test "$#" -eq 0 || exit 2
|
|
33
39
|
|
|
34
40
|
if test -e /proc/self/fd/3; then
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
},
|
|
30
30
|
"adapters": {
|
|
31
31
|
"telegram-poller-quiescence": { "operation": "quiesce_telegram_poller", "authority": "fixed-managed-telegram-poller-stop-and-count", "modelReachable": false, "timeoutMs": 15000 },
|
|
32
|
-
"telegram-vault-store": { "operation": "store_telegram_bootstrap", "authority": "fixed-runtime-vault-merge", "modelReachable": false, "timeoutMs": 15000 },
|
|
33
32
|
"cursor-snapshot": { "operation": "snapshot", "authority": "fixed-telegram-state-read", "modelReachable": false, "timeoutMs": 15000 },
|
|
34
33
|
"callback-live": { "operation": "callback_playback_preflight|inject_callbacks_concurrently|inject_callback_replay", "authority": "fixed-live-telegram-callback-probe", "modelReachable": false, "timeoutMs": 15000 },
|
|
35
34
|
"key-inventory": { "operation": "inventory_keys", "authority": "fixed-unraid-key-directory-read", "modelReachable": false, "timeoutMs": 15000 },
|
|
@@ -91,12 +90,10 @@
|
|
|
91
90
|
"offsetPath": "/evidence/telegram-bootstrap-offset.json",
|
|
92
91
|
"noncePath": "/evidence/telegram-bootstrap-nonce.txt",
|
|
93
92
|
"pollerAdapter": "/opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh",
|
|
94
|
-
"vaultAdapter": "/opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh",
|
|
95
93
|
"deadlineMs": 600000,
|
|
96
94
|
"pollTimeoutSeconds": 20
|
|
97
95
|
},
|
|
98
|
-
"dynamic": ["expectedBotId:getMe.id", "expectedUsername:getMe.username", "currentOffset:fixed-cursor-read"]
|
|
99
|
-
"privateInputFd": 3
|
|
96
|
+
"dynamic": ["expectedBotId:getMe.id", "expectedUsername:getMe.username", "currentOffset:fixed-cursor-read"]
|
|
100
97
|
},
|
|
101
98
|
"cursor-snapshot": {
|
|
102
99
|
"fixed": {
|
|
@@ -17,7 +17,7 @@ elif test "$#" -gt 1; then
|
|
|
17
17
|
exit 2
|
|
18
18
|
fi
|
|
19
19
|
|
|
20
|
-
if test -e /proc/self/fd/3; then
|
|
20
|
+
if test "$COMMAND" = callback-inject && test -e /proc/self/fd/3; then
|
|
21
21
|
exec node --input-type=module -e "$ENTRY" "$COMMAND" "$CONFIG_PATH" 3<&3
|
|
22
22
|
fi
|
|
23
23
|
exec node --input-type=module -e "$ENTRY" "$COMMAND" "$CONFIG_PATH"
|
|
@@ -360,13 +360,13 @@ if test "$COMMAND" = cursor-snapshot; then
|
|
|
360
360
|
fi
|
|
361
361
|
materialize_config "$EXPECTED_CONFIG" "$SNAPSHOT_PHASE"
|
|
362
362
|
cmp -s "$EXPECTED_CONFIG" "$CONFIG_PATH" || exit 1
|
|
363
|
-
case "$COMMAND" in
|
|
363
|
+
case "$COMMAND" in callback-inject) test -r /proc/self/fd/3 || exit 2 ;; esac
|
|
364
364
|
prepare_live_facts
|
|
365
365
|
if test "$COMMAND" = telegram-bootstrap; then quiesce_production_telegram_poller; fi
|
|
366
366
|
if test "$COMMAND" = unraid-key-rotate; then stop_exact_production_container; fi
|
|
367
367
|
|
|
368
368
|
case "$COMMAND" in
|
|
369
|
-
telegram-bootstrap) TIME_LIMIT=900; NETWORK=host; INPUT=
|
|
369
|
+
telegram-bootstrap) TIME_LIMIT=900; NETWORK=host; INPUT=no; BUNDLE_MODE=readonly; BROKER=no ;;
|
|
370
370
|
callback-inject) TIME_LIMIT=120; NETWORK=host; INPUT=yes; BUNDLE_MODE=rw; BROKER=no ;;
|
|
371
371
|
unraid-key-rotate) TIME_LIMIT=600; NETWORK=host; INPUT=no; BUNDLE_MODE=readonly; BROKER=yes ;;
|
|
372
372
|
evidence-snapshot) TIME_LIMIT=4950; NETWORK=host; INPUT=no; BUNDLE_MODE=readonly; BROKER=yes ;;
|
|
@@ -424,7 +424,7 @@ fi
|
|
|
424
424
|
run_harness() {
|
|
425
425
|
if test "$BUNDLE_MODE" = rw; then BUNDLE_SUFFIX=; else BUNDLE_SUFFIX=,readonly; fi
|
|
426
426
|
if test "$COMMAND" = telegram-bootstrap; then
|
|
427
|
-
/usr/bin/timeout -s KILL "$TIME_LIMIT" /usr/bin/docker run --rm
|
|
427
|
+
/usr/bin/timeout -s KILL "$TIME_LIMIT" /usr/bin/docker run --rm --pull=never --network "$NETWORK" \
|
|
428
428
|
--user 10001:10001 --read-only --cap-drop ALL --security-opt no-new-privileges \
|
|
429
429
|
--mount "type=bind,src=$CONFIG_PATH,dst=/run/ouro-acceptance/config.json,readonly" \
|
|
430
430
|
--mount "type=bind,src=$EVIDENCE_ROOT,dst=/evidence" \
|
|
@@ -437,9 +437,8 @@ run_harness() {
|
|
|
437
437
|
--mount "type=bind,src=$HEALTH_FACT,dst=/run/ouro-acceptance/postboot-health.json,readonly" \
|
|
438
438
|
--mount "type=bind,src=$CONTAINER_INSPECT_FACT,dst=/run/ouro-acceptance/container-inspect.json,readonly" \
|
|
439
439
|
--mount "type=bind,src=/proc/sys/kernel/random/boot_id,dst=/run/ouro-acceptance/boot-id,readonly" \
|
|
440
|
-
--entrypoint /
|
|
441
|
-
|
|
442
|
-
sanctuary-unit16 "$COMMAND" --config /run/ouro-acceptance/config.json <&3
|
|
440
|
+
--entrypoint /opt/ouro/deploy/unraid/sanctuary-acceptance-harness.sh "$IMAGE_ID" \
|
|
441
|
+
"$COMMAND" --config /run/ouro-acceptance/config.json
|
|
443
442
|
elif test "$COMMAND" = callback-inject; then
|
|
444
443
|
/usr/bin/timeout -s KILL "$TIME_LIMIT" /usr/bin/docker run --rm -i --pull=never --network "$NETWORK" \
|
|
445
444
|
--user 10001:10001 --read-only --cap-drop ALL --security-opt no-new-privileges \
|
|
@@ -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.774</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -127,10 +127,10 @@ exports.COMMAND_REGISTRY = {
|
|
|
127
127
|
},
|
|
128
128
|
event: {
|
|
129
129
|
category: "Tasks",
|
|
130
|
-
description: "Submit
|
|
131
|
-
usage: "ouro event submit --agent <agent> --source <source> --type <event-type> --id <provider-id> [--summary <text>] [--evidence <path>] [--payload <path>] [--priority high|normal|low] [--no-wake]",
|
|
130
|
+
description: "Submit an external event or apply a reviewed receipt-repair manifest through the owning daemon",
|
|
131
|
+
usage: "ouro event submit --agent <agent> --source <source> --type <event-type> --id <provider-id> [--summary <text>] [--evidence <path>] [--payload <path>] [--priority high|normal|low] [--no-wake] | ouro event repair --manifest <path>",
|
|
132
132
|
example: "ouro event submit --agent slugger --source app-store-connect --type feedback.created --id evt_123 --evidence /tmp/feedback",
|
|
133
|
-
subcommands: ["submit"],
|
|
133
|
+
subcommands: ["submit", "repair"],
|
|
134
134
|
},
|
|
135
135
|
poke: {
|
|
136
136
|
category: "Tasks",
|
|
@@ -201,6 +201,11 @@ function parseMessageCommand(args) {
|
|
|
201
201
|
}
|
|
202
202
|
function parseEventCommand(args) {
|
|
203
203
|
const sub = args[0];
|
|
204
|
+
if (sub === "repair") {
|
|
205
|
+
if (args.length !== 3 || args[1] !== "--manifest" || !args[2])
|
|
206
|
+
throw new Error(`Usage\n${usage()}`);
|
|
207
|
+
return { kind: "external.event.repair", manifestPath: args[2] };
|
|
208
|
+
}
|
|
204
209
|
if (sub !== "submit")
|
|
205
210
|
throw new Error(`Usage\n${usage()}`);
|
|
206
211
|
let agent;
|
|
@@ -2014,6 +2014,26 @@ class OuroDaemon {
|
|
|
2014
2014
|
data: dispatched,
|
|
2015
2015
|
};
|
|
2016
2016
|
}
|
|
2017
|
+
case "external.event.repair": {
|
|
2018
|
+
if (this.externalEventReconcileRunning)
|
|
2019
|
+
return { ok: false, error: "external event reconciliation is already active; repair made no changes" };
|
|
2020
|
+
this.externalEventReconcileRunning = true;
|
|
2021
|
+
try {
|
|
2022
|
+
try {
|
|
2023
|
+
const result = (0, router_1.repairExternalEventsFromManifest)(command.manifestPath, { root: this.externalEventRootPath() });
|
|
2024
|
+
return { ok: result.failed.length === 0, message: `external event repair applied=${result.applied.length} alreadyApplied=${result.alreadyApplied.length} failed=${result.failed.length} pending=${result.pending.length}`, data: result };
|
|
2025
|
+
}
|
|
2026
|
+
catch (error) {
|
|
2027
|
+
const message = (error instanceof Error ? error.message : /* v8 ignore next -- defensive: repair parser and filesystem dependencies throw Error instances @preserve */ String(error)).slice(0, 1_000);
|
|
2028
|
+
const result = { applied: [], alreadyApplied: [], failed: [{ identity: "<manifest>", error: message }], pending: [] };
|
|
2029
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "daemon", event: "daemon.external_event_repair", message: "rejected external event repair manifest", meta: { applied: 0, alreadyApplied: 0, failed: 1, pending: 0 } });
|
|
2030
|
+
return { ok: false, error: `external event repair rejected: ${message}`, data: result };
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
finally {
|
|
2034
|
+
this.externalEventReconcileRunning = false;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2017
2037
|
case "private.decisions": {
|
|
2018
2038
|
const requestedLimit = Number.isInteger(command.limit) && command.limit && command.limit > 0 ? command.limit : 20;
|
|
2019
2039
|
const limit = Math.min(requestedLimit, 1000);
|
|
@@ -255,6 +255,7 @@ function createSanctuaryAcceptanceAdapterDependencies(secretFd = 3, options = {}
|
|
|
255
255
|
interactiveRuntime: executeSanctuaryInteractiveRuntimeOperation,
|
|
256
256
|
hostRequest: options.hostRequest ?? ((payload) => defaultHostRequest(payload, hostBrokerSocket, adapterTimeoutMs)),
|
|
257
257
|
telegramCredentials: () => (0, telegram_1.loadTelegramSenseCredentials)(TARGET_ID),
|
|
258
|
+
createTelegramApi: telegram_client_1.createTelegramBotApi,
|
|
258
259
|
readLiveGrounding: readIndependentSanctuaryGrounding,
|
|
259
260
|
runProductionBoundaryProbe: runSanctuaryProductionBoundaryProbe,
|
|
260
261
|
};
|
|
@@ -1827,12 +1828,6 @@ function exactKeys(value, keys, label) {
|
|
|
1827
1828
|
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...keys].sort()))
|
|
1828
1829
|
throw new Error(`${label} shape is invalid`);
|
|
1829
1830
|
}
|
|
1830
|
-
function positiveDecimal(value, label) {
|
|
1831
|
-
const result = text(value, label);
|
|
1832
|
-
if (!/^[1-9][0-9]*$/u.test(result))
|
|
1833
|
-
throw new Error(`${label} is invalid`);
|
|
1834
|
-
return result;
|
|
1835
|
-
}
|
|
1836
1831
|
function sha256(value) {
|
|
1837
1832
|
return (0, node_crypto_1.createHash)("sha256").update(value, "utf8").digest("hex");
|
|
1838
1833
|
}
|
|
@@ -1845,18 +1840,6 @@ async function runtimeConfig(reader, label, ...args) {
|
|
|
1845
1840
|
throw new Error(`${label} is unavailable`);
|
|
1846
1841
|
return result.config;
|
|
1847
1842
|
}
|
|
1848
|
-
async function storeTelegramBootstrap(payload, deps) {
|
|
1849
|
-
const patch = {
|
|
1850
|
-
telegramBotToken: text(payload.botToken, "Telegram bot credential"),
|
|
1851
|
-
telegramAuthorizedUserId: positiveDecimal(payload.authorizedUserId, "Telegram authorized user"),
|
|
1852
|
-
telegramAuthorizedChatId: positiveDecimal(payload.authorizedChatId, "Telegram authorized chat"),
|
|
1853
|
-
};
|
|
1854
|
-
const stored = await dependency(deps.mergeRuntime, "runtime vault writer")(TARGET_ID, patch);
|
|
1855
|
-
if (!stored.ok || Object.entries(patch).some(([key, value]) => stored.config[key] !== value)) {
|
|
1856
|
-
throw new Error("Telegram bootstrap vault readback failed");
|
|
1857
|
-
}
|
|
1858
|
-
return { stored: true };
|
|
1859
|
-
}
|
|
1860
1843
|
function cursorSnapshot(payload, deps) {
|
|
1861
1844
|
exactKeys(payload, ["allowGenesis", "operation", "schema"], "Telegram cursor snapshot request");
|
|
1862
1845
|
if (payload.schema !== "telegram-cursor-v1" || typeof payload.allowGenesis !== "boolean")
|
|
@@ -2306,6 +2289,54 @@ function materializeConfig(payload, deps) {
|
|
|
2306
2289
|
}
|
|
2307
2290
|
return config;
|
|
2308
2291
|
}
|
|
2292
|
+
async function telegramReadiness(payload, deps) {
|
|
2293
|
+
exactKeys(payload, ["operation"], "Telegram readiness payload");
|
|
2294
|
+
let refreshed;
|
|
2295
|
+
try {
|
|
2296
|
+
refreshed = await dependency(deps.refreshRuntime, "runtime credential refresher")(TARGET_ID);
|
|
2297
|
+
}
|
|
2298
|
+
catch {
|
|
2299
|
+
throw new Error("Telegram runtime credentials are unavailable; actor: human-required; unlock or repair vault runtime/config");
|
|
2300
|
+
}
|
|
2301
|
+
if (!refreshed.ok)
|
|
2302
|
+
throw new Error("Telegram runtime credentials are unavailable; actor: human-required; unlock or repair vault runtime/config");
|
|
2303
|
+
let credentials;
|
|
2304
|
+
try {
|
|
2305
|
+
credentials = dependency(deps.telegramCredentials, "Telegram credentials")();
|
|
2306
|
+
(0, telegram_1.telegramBotIdFromToken)(credentials.botToken);
|
|
2307
|
+
}
|
|
2308
|
+
catch {
|
|
2309
|
+
throw new Error("Telegram runtime credentials are invalid; actor: human-required; repair vault runtime/config");
|
|
2310
|
+
}
|
|
2311
|
+
let api;
|
|
2312
|
+
try {
|
|
2313
|
+
api = dependency(deps.createTelegramApi, "Telegram API factory")({ token: credentials.botToken });
|
|
2314
|
+
}
|
|
2315
|
+
catch {
|
|
2316
|
+
throw new Error("Telegram client initialization failed; actor: agent-runnable; retry Telegram readiness");
|
|
2317
|
+
}
|
|
2318
|
+
let bot;
|
|
2319
|
+
try {
|
|
2320
|
+
bot = await api.request("getMe", {}, AbortSignal.timeout(30_000));
|
|
2321
|
+
}
|
|
2322
|
+
catch {
|
|
2323
|
+
throw new Error("Telegram getMe failed; actor: agent-runnable; retry Telegram readiness");
|
|
2324
|
+
}
|
|
2325
|
+
finally {
|
|
2326
|
+
try {
|
|
2327
|
+
api.stop();
|
|
2328
|
+
}
|
|
2329
|
+
catch {
|
|
2330
|
+
throw new Error("Telegram client cleanup failed; actor: agent-runnable; retry Telegram readiness");
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
if (!bot || typeof bot !== "object" || Array.isArray(bot)
|
|
2334
|
+
|| String(bot.id) !== "8541786263"
|
|
2335
|
+
|| bot.username !== "MendelowCloudButlerBot") {
|
|
2336
|
+
throw new Error("Telegram bot identity mismatch; actor: human-required; repair vault runtime/config");
|
|
2337
|
+
}
|
|
2338
|
+
return { ready: true, identityMatches: true };
|
|
2339
|
+
}
|
|
2309
2340
|
async function executeSanctuaryAcceptanceAdapter(rawPayload, deps = createSanctuaryAcceptanceAdapterDependencies()) {
|
|
2310
2341
|
const payload = object(rawPayload, "acceptance adapter payload");
|
|
2311
2342
|
const operation = text(payload.operation, "operation");
|
|
@@ -2325,9 +2356,6 @@ async function executeSanctuaryAcceptanceAdapter(rawPayload, deps = createSanctu
|
|
|
2325
2356
|
case "revoked-key-auth-rejection":
|
|
2326
2357
|
result = await revokedKeyAuthRejection(payload, deps);
|
|
2327
2358
|
break;
|
|
2328
|
-
case "store_telegram_bootstrap":
|
|
2329
|
-
result = await storeTelegramBootstrap(payload, deps);
|
|
2330
|
-
break;
|
|
2331
2359
|
case "quiesce_telegram_poller":
|
|
2332
2360
|
result = telegramPollerQuiescence(payload, deps);
|
|
2333
2361
|
break;
|
|
@@ -2410,6 +2438,9 @@ async function executeSanctuaryAcceptanceAdapter(rawPayload, deps = createSanctu
|
|
|
2410
2438
|
case "materialize_config":
|
|
2411
2439
|
result = materializeConfig(payload, deps);
|
|
2412
2440
|
break;
|
|
2441
|
+
case "telegram_readiness":
|
|
2442
|
+
result = await telegramReadiness(payload, deps);
|
|
2443
|
+
break;
|
|
2413
2444
|
default: throw new Error("unknown Sanctuary acceptance adapter operation");
|
|
2414
2445
|
}
|
|
2415
2446
|
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_acceptance_adapter_end", message: "Sanctuary acceptance adapter completed", meta: { operation } });
|
|
@@ -45,6 +45,8 @@ const node_fs_1 = require("node:fs");
|
|
|
45
45
|
const node_fs_2 = require("node:fs");
|
|
46
46
|
const path = __importStar(require("node:path"));
|
|
47
47
|
const runtime_1 = require("../../nerves/runtime");
|
|
48
|
+
const telegram_1 = require("../../senses/telegram");
|
|
49
|
+
const runtime_credentials_1 = require("../runtime-credentials");
|
|
48
50
|
const MAX_ADAPTER_OUTPUT = 1_048_576;
|
|
49
51
|
const DEFAULT_ADAPTER_TIMEOUT_MS = 240_000;
|
|
50
52
|
const DEFAULT_TELEGRAM_TIMEOUT_MS = 10_000;
|
|
@@ -59,6 +61,9 @@ function createSanctuaryAcceptanceHarnessDependencies(secretFd = 3, options = {}
|
|
|
59
61
|
const telegramTimeoutMs = options.telegramTimeoutMs ?? DEFAULT_TELEGRAM_TIMEOUT_MS;
|
|
60
62
|
return {
|
|
61
63
|
readSecret: () => (0, node_fs_1.readFileSync)(secretFd, "utf8"),
|
|
64
|
+
refreshRuntime: runtime_credentials_1.refreshRuntimeCredentialConfig,
|
|
65
|
+
mergeRuntime: runtime_credentials_1.mergeRuntimeCredentialConfig,
|
|
66
|
+
telegramCredentials: telegram_1.loadTelegramSenseCredentials,
|
|
62
67
|
runAdapter: async (executable, payload, remainingMs) => {
|
|
63
68
|
requireAbsoluteExecutable(executable);
|
|
64
69
|
const result = (0, node_child_process_1.spawnSync)(executable, [], {
|
|
@@ -92,6 +97,25 @@ function createSanctuaryAcceptanceHarnessDependencies(secretFd = 3, options = {}
|
|
|
92
97
|
telegramTimeoutMs,
|
|
93
98
|
};
|
|
94
99
|
}
|
|
100
|
+
async function canonicalTelegramBootstrapToken(deps) {
|
|
101
|
+
let refreshed;
|
|
102
|
+
try {
|
|
103
|
+
refreshed = await deps.refreshRuntime("sanctuary");
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new Error("Telegram runtime credentials are unavailable; actor: human-required; unlock or repair vault runtime/config");
|
|
107
|
+
}
|
|
108
|
+
if (!refreshed.ok)
|
|
109
|
+
throw new Error("Telegram runtime credentials are unavailable; actor: human-required; unlock or repair vault runtime/config");
|
|
110
|
+
try {
|
|
111
|
+
const token = deps.telegramCredentials("sanctuary").botToken.trim();
|
|
112
|
+
(0, telegram_1.telegramBotIdFromToken)(token);
|
|
113
|
+
return token;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new Error("Telegram runtime credentials are invalid; actor: human-required; repair vault runtime/config");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
95
119
|
function object(value, label) {
|
|
96
120
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
97
121
|
throw new Error(`${label} must be an object`);
|
|
@@ -322,6 +346,10 @@ async function telegramRequest(deps, token, method, body, requestTimeoutMs = dep
|
|
|
322
346
|
throw new Error("Telegram request failed");
|
|
323
347
|
return envelope.result;
|
|
324
348
|
}
|
|
349
|
+
function telegramBootstrapRequestError(method, error) {
|
|
350
|
+
const outcome = error instanceof Error && /timed out/iu.test(error.message) ? "timed out" : "failed";
|
|
351
|
+
return new Error(`Telegram ${method} ${outcome}; actor: agent-runnable; retry Telegram bootstrap`);
|
|
352
|
+
}
|
|
325
353
|
exports.SANCTUARY_UNIT_16_EVIDENCE_LABELS = [
|
|
326
354
|
"unit-12c-1-opaque-identity",
|
|
327
355
|
"unit-14b-3-opaque-identity-live",
|
|
@@ -871,10 +899,8 @@ async function telegramBootstrap(config, deps) {
|
|
|
871
899
|
const noncePath = confinedPath(root, config.noncePath, "noncePath");
|
|
872
900
|
refuseExistingCheckpoint(root, noncePath);
|
|
873
901
|
const pollerAdapter = adapter(config.pollerAdapter, "pollerAdapter");
|
|
874
|
-
|
|
875
|
-
if (pollerAdapter !== PACKAGED_PROVENANCE_ADAPTER || vaultAdapter !== PACKAGED_PROVENANCE_ADAPTER) {
|
|
902
|
+
if (pollerAdapter !== PACKAGED_PROVENANCE_ADAPTER)
|
|
876
903
|
throw new Error("Telegram bootstrap requires the fixed packaged acceptance adapter");
|
|
877
|
-
}
|
|
878
904
|
const deadlineMs = integer(config.deadlineMs, "deadlineMs", 300_000);
|
|
879
905
|
if (deadlineMs > 900_000)
|
|
880
906
|
throw new Error("Telegram bootstrap deadline exceeds 15 minutes");
|
|
@@ -882,11 +908,17 @@ async function telegramBootstrap(config, deps) {
|
|
|
882
908
|
if (pollTimeoutSeconds > 50)
|
|
883
909
|
throw new Error("Telegram poll timeout exceeds 50 seconds");
|
|
884
910
|
const token = deps.readSecret().trim();
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
911
|
+
let getMe;
|
|
912
|
+
try {
|
|
913
|
+
getMe = await telegramRequest(deps, token, "getMe");
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
throw telegramBootstrapRequestError("getMe", error);
|
|
917
|
+
}
|
|
918
|
+
const bot = object(getMe, "Telegram getMe result");
|
|
919
|
+
if (String(bot.id) !== expectedBotId || bot.username !== expectedUsername) {
|
|
920
|
+
throw new Error("Telegram bot identity mismatch; actor: human-required; repair vault runtime/config");
|
|
921
|
+
}
|
|
890
922
|
const nonce = deps.randomBytes(16).toString("hex");
|
|
891
923
|
const base = {
|
|
892
924
|
schemaVersion: 1,
|
|
@@ -909,11 +941,17 @@ async function telegramBootstrap(config, deps) {
|
|
|
909
941
|
let nextOffset = currentOffset;
|
|
910
942
|
let match;
|
|
911
943
|
while (deps.now() < deadline && !match) {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
944
|
+
let updates;
|
|
945
|
+
try {
|
|
946
|
+
updates = await telegramRequest(deps, token, "getUpdates", {
|
|
947
|
+
offset: nextOffset,
|
|
948
|
+
timeout: pollTimeoutSeconds,
|
|
949
|
+
allowed_updates: ["message"],
|
|
950
|
+
}, (pollTimeoutSeconds + 5) * 1_000);
|
|
951
|
+
}
|
|
952
|
+
catch (error) {
|
|
953
|
+
throw telegramBootstrapRequestError("getUpdates", error);
|
|
954
|
+
}
|
|
917
955
|
if (!Array.isArray(updates))
|
|
918
956
|
throw new Error("Telegram getUpdates result must be an array");
|
|
919
957
|
const parsed = updates.map((entry) => object(entry, "Telegram update"));
|
|
@@ -952,14 +990,20 @@ async function telegramBootstrap(config, deps) {
|
|
|
952
990
|
offsetDigest: digest(nextUpdateId),
|
|
953
991
|
};
|
|
954
992
|
replaceCheckpoint(root, evidencePath, confirmed);
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
993
|
+
let stored;
|
|
994
|
+
try {
|
|
995
|
+
stored = await deps.mergeRuntime("sanctuary", {
|
|
996
|
+
telegramAuthorizedUserId: userId,
|
|
997
|
+
telegramAuthorizedChatId: chatId,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
catch {
|
|
1001
|
+
throw new Error("Telegram bootstrap vault update failed; actor: agent-runnable; retry Telegram bootstrap");
|
|
1002
|
+
}
|
|
1003
|
+
if (!stored.ok || stored.config.telegramBotToken !== token
|
|
1004
|
+
|| stored.config.telegramAuthorizedUserId !== userId || stored.config.telegramAuthorizedChatId !== chatId) {
|
|
1005
|
+
throw new Error("Telegram bootstrap vault readback failed; actor: agent-runnable; retry Telegram bootstrap");
|
|
1006
|
+
}
|
|
963
1007
|
replaceCheckpoint(root, evidencePath, { ...confirmed, phase: "vault_committed" });
|
|
964
1008
|
atomicPrivateJson(root, offsetPath, { nextUpdateId });
|
|
965
1009
|
replaceCheckpoint(root, evidencePath, { ...confirmed, phase: "complete", completedAt: deps.now() });
|
|
@@ -1779,9 +1823,11 @@ async function executeSanctuaryAcceptanceHarness(command, rawConfig, deps = crea
|
|
|
1779
1823
|
try {
|
|
1780
1824
|
const config = object(rawConfig, "acceptance config");
|
|
1781
1825
|
switch (command) {
|
|
1782
|
-
case "telegram-bootstrap":
|
|
1783
|
-
await
|
|
1826
|
+
case "telegram-bootstrap": {
|
|
1827
|
+
const token = await canonicalTelegramBootstrapToken(deps);
|
|
1828
|
+
await telegramBootstrap(config, { ...deps, readSecret: () => token });
|
|
1784
1829
|
break;
|
|
1830
|
+
}
|
|
1785
1831
|
case "cursor-snapshot":
|
|
1786
1832
|
await cursorSnapshot(config, deps);
|
|
1787
1833
|
break;
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.getExternalEventRoot = getExternalEventRoot;
|
|
37
37
|
exports.externalEventRecordPath = externalEventRecordPath;
|
|
38
38
|
exports.readExternalEventRecord = readExternalEventRecord;
|
|
39
|
+
exports.repairExternalEventsFromManifest = repairExternalEventsFromManifest;
|
|
39
40
|
exports.listExternalEventStatus = listExternalEventStatus;
|
|
40
41
|
exports.buildExternalEventMessage = buildExternalEventMessage;
|
|
41
42
|
exports.recordExternalEvent = recordExternalEvent;
|
|
@@ -333,6 +334,200 @@ function readExternalEventRecord(recordPath) {
|
|
|
333
334
|
}
|
|
334
335
|
return parsed;
|
|
335
336
|
}
|
|
337
|
+
const MAX_REPAIR_MANIFEST_BYTES = 1024 * 1024;
|
|
338
|
+
const MAX_REPAIR_ENTRIES = 512;
|
|
339
|
+
const SHA256_HEX = /^[a-f0-9]{64}$/u;
|
|
340
|
+
function hasExactKeys(value, expected) {
|
|
341
|
+
const actual = Object.keys(value).sort();
|
|
342
|
+
return actual.length === expected.length && actual.every((key, index) => key === [...expected].sort()[index]);
|
|
343
|
+
}
|
|
344
|
+
function repairIdentity(entry) {
|
|
345
|
+
return `${entry.agent}/${entry.source}/${entry.eventId}`;
|
|
346
|
+
}
|
|
347
|
+
function repairedRecord(record, repairedAt) {
|
|
348
|
+
return {
|
|
349
|
+
...record,
|
|
350
|
+
version: record.version + 1,
|
|
351
|
+
updatedAt: repairedAt,
|
|
352
|
+
failureProvenance: { class: "execution_lease_expired", failedAt: repairedAt },
|
|
353
|
+
recoveryGrant: undefined,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function recordBytes(record) {
|
|
357
|
+
return `${JSON.stringify(record, null, 2)}\n`;
|
|
358
|
+
}
|
|
359
|
+
function digestBytes(raw) {
|
|
360
|
+
return (0, node_crypto_1.createHash)("sha256").update(raw).digest("hex");
|
|
361
|
+
}
|
|
362
|
+
function parseRepairManifest(manifestPath) {
|
|
363
|
+
const descriptor = fs.openSync(manifestPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
364
|
+
let raw;
|
|
365
|
+
try {
|
|
366
|
+
const stat = fs.fstatSync(descriptor);
|
|
367
|
+
if (!stat.isFile() || stat.size > MAX_REPAIR_MANIFEST_BYTES)
|
|
368
|
+
throw new Error("External event repair manifest must be a bounded regular file");
|
|
369
|
+
raw = fs.readFileSync(descriptor);
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
fs.closeSync(descriptor);
|
|
373
|
+
}
|
|
374
|
+
let parsed;
|
|
375
|
+
try {
|
|
376
|
+
parsed = JSON.parse(raw.toString("utf8"));
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
throw new Error("External event repair manifest is invalid JSON");
|
|
380
|
+
}
|
|
381
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
382
|
+
throw new Error("External event repair manifest is invalid");
|
|
383
|
+
const value = parsed;
|
|
384
|
+
if (!hasExactKeys(value, ["schemaVersion", "repairedAt", "reason", "evidence", "requestedBy", "reviewedBy", "entries"])
|
|
385
|
+
|| value.schemaVersion !== 1 || !canonicalIso(value.repairedAt)
|
|
386
|
+
|| typeof value.reason !== "string" || value.reason.length < 1 || Buffer.byteLength(value.reason) > 2_000
|
|
387
|
+
|| typeof value.requestedBy !== "string" || value.requestedBy.length < 1 || Buffer.byteLength(value.requestedBy) > 256
|
|
388
|
+
|| typeof value.reviewedBy !== "string" || value.reviewedBy.length < 1 || Buffer.byteLength(value.reviewedBy) > 256
|
|
389
|
+
|| value.requestedBy === value.reviewedBy
|
|
390
|
+
|| !Array.isArray(value.evidence) || value.evidence.length < 1 || value.evidence.length > 32
|
|
391
|
+
|| value.evidence.some((item) => typeof item !== "string" || item.length < 1 || Buffer.byteLength(item) > 1_000)
|
|
392
|
+
|| !Array.isArray(value.entries) || value.entries.length < 1 || value.entries.length > MAX_REPAIR_ENTRIES)
|
|
393
|
+
throw new Error("External event repair manifest is invalid or unbounded");
|
|
394
|
+
const seen = new Set();
|
|
395
|
+
for (const candidate of value.entries) {
|
|
396
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
397
|
+
throw new Error("External event repair manifest entry is invalid");
|
|
398
|
+
const entry = candidate;
|
|
399
|
+
const identity = repairIdentity(entry);
|
|
400
|
+
if (!hasExactKeys(entry, ["agent", "source", "eventId", "preimageSha256", "postimageSha256", "version", "generation", "failureProvenance", "recoveryGrant"])
|
|
401
|
+
|| !hasExactKeys(entry.failureProvenance ?? {}, ["class", "failedAt"])
|
|
402
|
+
|| !hasExactKeys(entry.recoveryGrant ?? {}, ["generation", "consumedAt"])
|
|
403
|
+
|| !entry.agent || !entry.source || !entry.eventId || entry.agent !== entry.agent.trim() || entry.source !== entry.source.trim() || entry.eventId !== entry.eventId.trim()
|
|
404
|
+
|| Buffer.byteLength(entry.agent) > 160 || Buffer.byteLength(entry.source) > 160 || Buffer.byteLength(entry.eventId) > 160
|
|
405
|
+
|| !SHA256_HEX.test(entry.preimageSha256) || !SHA256_HEX.test(entry.postimageSha256)
|
|
406
|
+
|| entry.preimageSha256 === entry.postimageSha256
|
|
407
|
+
|| !Number.isSafeInteger(entry.version) || entry.version < 1 || !Number.isSafeInteger(entry.generation) || entry.generation < 1
|
|
408
|
+
|| entry.failureProvenance?.class !== "provider_lane_unavailable" || !canonicalIso(entry.failureProvenance.failedAt)
|
|
409
|
+
|| entry.recoveryGrant?.generation !== entry.generation || !canonicalIso(entry.recoveryGrant.consumedAt))
|
|
410
|
+
throw new Error(`External event repair entry is invalid for ${identity}`);
|
|
411
|
+
if (seen.has(identity))
|
|
412
|
+
throw new Error(`External event repair manifest has duplicate identity ${identity}`);
|
|
413
|
+
seen.add(identity);
|
|
414
|
+
}
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
function assertRepairRecordPath(root, entry) {
|
|
418
|
+
const resolvedRoot = path.resolve(root);
|
|
419
|
+
const recordPath = externalEventRecordPath(resolvedRoot, entry);
|
|
420
|
+
const relative = path.relative(resolvedRoot, path.resolve(recordPath));
|
|
421
|
+
/* v8 ignore next -- defense in depth: safePathSegment cannot emit traversal @preserve */
|
|
422
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
423
|
+
throw new Error(`External event repair path escapes its root for ${repairIdentity(entry)}`);
|
|
424
|
+
for (let candidate = resolvedRoot;; candidate = path.dirname(candidate)) {
|
|
425
|
+
const stat = fs.lstatSync(candidate);
|
|
426
|
+
if (stat.isSymbolicLink())
|
|
427
|
+
throw new Error(`External event repair refuses symlink root for ${repairIdentity(entry)}`);
|
|
428
|
+
if (candidate === path.dirname(candidate))
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
const expected = [
|
|
432
|
+
[resolvedRoot, "directory"],
|
|
433
|
+
[path.join(resolvedRoot, safePathSegment(entry.agent)), "directory"],
|
|
434
|
+
[path.join(resolvedRoot, safePathSegment(entry.agent), safePathSegment(entry.source)), "directory"],
|
|
435
|
+
[recordPath, "file"],
|
|
436
|
+
];
|
|
437
|
+
for (const [candidate, kind] of expected) {
|
|
438
|
+
const stat = fs.lstatSync(candidate);
|
|
439
|
+
if (stat.isSymbolicLink())
|
|
440
|
+
throw new Error(`External event repair refuses symlink path for ${repairIdentity(entry)}`);
|
|
441
|
+
if ((kind === "directory" && !stat.isDirectory()) || (kind === "file" && !stat.isFile()))
|
|
442
|
+
throw new Error(`External event repair path shape is invalid for ${repairIdentity(entry)}`);
|
|
443
|
+
}
|
|
444
|
+
return recordPath;
|
|
445
|
+
}
|
|
446
|
+
function assertRepairPostimage(record, entry, repairedAt) {
|
|
447
|
+
if (record.agent !== entry.agent || record.source !== entry.source || record.eventId !== entry.eventId
|
|
448
|
+
|| record.version !== entry.version + 1 || record.generation !== entry.generation
|
|
449
|
+
|| record.executionState !== "dead_letter"
|
|
450
|
+
|| record.failureProvenance?.class !== "execution_lease_expired" || record.failureProvenance.failedAt !== repairedAt
|
|
451
|
+
|| record.recoveryGrant !== undefined)
|
|
452
|
+
throw new Error(`External event repair postimage is invalid for ${repairIdentity(entry)}`);
|
|
453
|
+
}
|
|
454
|
+
function emitRepairResult(result) {
|
|
455
|
+
(0, runtime_1.emitNervesEvent)({
|
|
456
|
+
...(result.failed.length > 0 ? { level: "error" } : {}),
|
|
457
|
+
component: "daemon",
|
|
458
|
+
event: "daemon.external_event_repair",
|
|
459
|
+
message: "processed reviewed external event repair manifest",
|
|
460
|
+
meta: { applied: result.applied.length, alreadyApplied: result.alreadyApplied.length, failed: result.failed.length, pending: result.pending.length },
|
|
461
|
+
});
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
function repairExternalEventsFromManifest(manifestPath, options) {
|
|
465
|
+
const manifest = parseRepairManifest(manifestPath);
|
|
466
|
+
const root = options.root;
|
|
467
|
+
const prepared = [];
|
|
468
|
+
const result = { applied: [], alreadyApplied: [], failed: [], pending: [] };
|
|
469
|
+
for (const entry of manifest.entries) {
|
|
470
|
+
const identity = repairIdentity(entry);
|
|
471
|
+
try {
|
|
472
|
+
const recordPath = assertRepairRecordPath(root, entry);
|
|
473
|
+
const raw = fs.readFileSync(recordPath);
|
|
474
|
+
const digest = digestBytes(raw);
|
|
475
|
+
const record = readExternalEventRecord(recordPath);
|
|
476
|
+
if (digest === entry.postimageSha256) {
|
|
477
|
+
assertRepairPostimage(record, entry, manifest.repairedAt);
|
|
478
|
+
prepared.push({ entry, recordPath, state: "post" });
|
|
479
|
+
result.alreadyApplied.push(identity);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (digest !== entry.preimageSha256)
|
|
483
|
+
throw new Error(`External event repair preimage conflict for ${identity}`);
|
|
484
|
+
if (record.agent !== entry.agent || record.source !== entry.source || record.eventId !== entry.eventId
|
|
485
|
+
|| record.version !== entry.version || record.generation !== entry.generation
|
|
486
|
+
|| record.failureProvenance?.class !== entry.failureProvenance.class || record.failureProvenance.failedAt !== entry.failureProvenance.failedAt
|
|
487
|
+
|| record.recoveryGrant?.generation !== entry.recoveryGrant.generation || record.recoveryGrant.consumedAt !== entry.recoveryGrant.consumedAt
|
|
488
|
+
|| record.executionState !== "dead_letter"
|
|
489
|
+
|| digestBytes(recordBytes(repairedRecord(record, manifest.repairedAt))) !== entry.postimageSha256)
|
|
490
|
+
throw new Error(`External event repair preimage is unauthorized for ${identity}`);
|
|
491
|
+
prepared.push({ entry, recordPath, state: "pre" });
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
result.failed.push({ identity, error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: validation and filesystem dependencies throw Error instances @preserve */ String(error) });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (result.failed.length > 0) {
|
|
498
|
+
result.pending.push(...prepared.filter(({ state }) => state === "pre").map(({ entry }) => repairIdentity(entry)));
|
|
499
|
+
return emitRepairResult(result);
|
|
500
|
+
}
|
|
501
|
+
for (let index = 0; index < prepared.length; index += 1) {
|
|
502
|
+
const item = prepared[index];
|
|
503
|
+
const identity = repairIdentity(item.entry);
|
|
504
|
+
if (item.state === "post")
|
|
505
|
+
continue;
|
|
506
|
+
try {
|
|
507
|
+
withRecordLock(item.recordPath, () => {
|
|
508
|
+
assertRepairRecordPath(root, item.entry);
|
|
509
|
+
const raw = fs.readFileSync(item.recordPath);
|
|
510
|
+
const digest = digestBytes(raw);
|
|
511
|
+
if (digest === item.entry.postimageSha256) {
|
|
512
|
+
assertRepairPostimage(readExternalEventRecord(item.recordPath), item.entry, manifest.repairedAt);
|
|
513
|
+
result.alreadyApplied.push(identity);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (digest !== item.entry.preimageSha256)
|
|
517
|
+
throw new Error("record advanced after prevalidation");
|
|
518
|
+
const record = readExternalEventRecord(item.recordPath);
|
|
519
|
+
atomicWrite(item.recordPath, repairedRecord(record, manifest.repairedAt));
|
|
520
|
+
result.applied.push(identity);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
result.failed.push({ identity, error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: validation and filesystem dependencies throw Error instances @preserve */ String(error) });
|
|
525
|
+
result.pending.push(...prepared.slice(index + 1).map(({ entry }) => repairIdentity(entry)));
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return emitRepairResult(result);
|
|
530
|
+
}
|
|
336
531
|
function listExternalEventStatus(root) {
|
|
337
532
|
if (!fs.existsSync(root))
|
|
338
533
|
return [];
|
|
@@ -1160,7 +1355,15 @@ function reconcileExternalEvent(recordPath, options = {}) {
|
|
|
1160
1355
|
return record;
|
|
1161
1356
|
const maxAttempts = options.maxAttempts ?? 5;
|
|
1162
1357
|
const baseDelayMs = options.baseDelayMs ?? 1_000;
|
|
1163
|
-
|
|
1358
|
+
const retried = retryState(record, now, maxAttempts, baseDelayMs, "execution lease expired", "execution_lease_expired");
|
|
1359
|
+
const consumedProviderRecoveryExpired = retried.executionState === "dead_letter"
|
|
1360
|
+
&& record.failureProvenance?.class === "provider_lane_unavailable"
|
|
1361
|
+
&& record.recoveryGrant?.generation === record.generation;
|
|
1362
|
+
return commitMutation(recordPath, consumedProviderRecoveryExpired ? {
|
|
1363
|
+
...retried,
|
|
1364
|
+
failureProvenance: { class: "execution_lease_expired", failedAt: now },
|
|
1365
|
+
recoveryGrant: undefined,
|
|
1366
|
+
} : retried, now);
|
|
1164
1367
|
});
|
|
1165
1368
|
}
|
|
1166
1369
|
function advanceExternalEventFromAwait(recordPath, input) {
|
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.774",
|
|
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.774",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|