@ouro.bot/cli 0.1.0-alpha.772 → 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 +15 -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-defaults.js +4 -0
- package/dist/heart/daemon/cli-exec.js +39 -1
- package/dist/heart/daemon/cli-help.js +3 -3
- package/dist/heart/daemon/cli-parse.js +5 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +40 -4
- package/dist/heart/daemon/daemon-entry.js +34 -71
- package/dist/heart/daemon/daemon.js +91 -25
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +52 -21
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +69 -23
- package/dist/heart/daemon/startup-tui.js +1 -0
- package/dist/heart/external-events/router.js +227 -14
- 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.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
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.773",
|
|
12
|
+
"changes": [
|
|
13
|
+
"Fail daemon startup closed until configured providers are live and their credentials are cached.",
|
|
14
|
+
"Surface provider startup failures immediately through a fresh redacted tombstone instead of waiting for a dead daemon or echoing raw logs.",
|
|
15
|
+
"Recover legacy provider dead letters from current-boot readiness once per generation without per-record alert noise.",
|
|
16
|
+
"Recover expired managed-runtime leases only after newer runtime liveness, with inspect-before-action turns and bounded batch wakeups."
|
|
17
|
+
]
|
|
18
|
+
},
|
|
4
19
|
{
|
|
5
20
|
"version": "0.1.0-alpha.772",
|
|
6
21
|
"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>
|
|
@@ -77,6 +77,8 @@ const provider_models_1 = require("../provider-models");
|
|
|
77
77
|
const cli_parse_1 = require("./cli-parse");
|
|
78
78
|
const provider_discovery_1 = require("./provider-discovery");
|
|
79
79
|
const provider_credentials_1 = require("../provider-credentials");
|
|
80
|
+
const startup_tui_1 = require("./startup-tui");
|
|
81
|
+
const daemon_tombstone_1 = require("./daemon-tombstone");
|
|
80
82
|
// ── Default implementations ──
|
|
81
83
|
async function defaultStartDaemonProcess(socketPath) {
|
|
82
84
|
const launchdStarted = await startDaemonProcessViaLaunchd(socketPath);
|
|
@@ -724,6 +726,7 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
|
|
|
724
726
|
readHealthState: daemon_health_1.readHealth,
|
|
725
727
|
readHealthUpdatedAt: defaultReadHealthUpdatedAt,
|
|
726
728
|
readRecentDaemonLogLines: defaultReadRecentDaemonLogLines,
|
|
729
|
+
readDaemonStartupTombstone: daemon_tombstone_1.readDaemonTombstone,
|
|
727
730
|
sleep: defaultSleep,
|
|
728
731
|
spawnBackgroundCli: defaultSpawnBackgroundCli,
|
|
729
732
|
now: () => Date.now(),
|
|
@@ -732,6 +735,7 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
|
|
|
732
735
|
startupStabilityWindowMs: 1_500,
|
|
733
736
|
startupTimeoutMs: 60_000,
|
|
734
737
|
startupRetryLimit: 1,
|
|
738
|
+
isProcessAlive: startup_tui_1.defaultIsProcessAlive,
|
|
735
739
|
listDiscoveredAgents: defaultListDiscoveredAgents,
|
|
736
740
|
runHatchFlow: hatch_flow_1.runHatchFlow,
|
|
737
741
|
promptInput: defaultPromptInput,
|
|
@@ -113,6 +113,7 @@ const readiness_repair_1 = require("./readiness-repair");
|
|
|
113
113
|
const human_readiness_1 = require("./human-readiness");
|
|
114
114
|
const human_command_screens_1 = require("./human-command-screens");
|
|
115
115
|
const startup_tui_1 = require("./startup-tui");
|
|
116
|
+
const daemon_bootstrap_startup_1 = require("./daemon-bootstrap-startup");
|
|
116
117
|
const stale_bundle_prune_1 = require("./stale-bundle-prune");
|
|
117
118
|
const up_progress_1 = require("./up-progress");
|
|
118
119
|
const provider_ping_progress_1 = require("./provider-ping-progress");
|
|
@@ -1192,6 +1193,7 @@ async function ensureDaemonRunning(deps, options = {}) {
|
|
|
1192
1193
|
writeRaw: deps.writeRaw ?? ((text) => process.stdout.write(text)),
|
|
1193
1194
|
/* v8 ignore next -- thin wrapper: real stdout TTY detection injected by default deps @preserve */
|
|
1194
1195
|
isTTY: deps.isTTY ?? process.stdout.isTTY === true,
|
|
1196
|
+
isProcessAlive: deps.isProcessAlive,
|
|
1195
1197
|
/* v8 ignore next -- thin wrapper: real Date.now fallback injected by default deps @preserve */
|
|
1196
1198
|
now: deps.now ?? (() => Date.now()),
|
|
1197
1199
|
/* v8 ignore next -- thin wrapper: real setTimeout fallback injected by default deps @preserve */
|
|
@@ -1240,6 +1242,7 @@ async function ensureDaemonRunning(deps, options = {}) {
|
|
|
1240
1242
|
writeRaw: deps.writeRaw ?? ((text) => process.stdout.write(text)),
|
|
1241
1243
|
/* v8 ignore next -- thin wrapper: real stdout TTY detection injected by default deps @preserve */
|
|
1242
1244
|
isTTY: deps.isTTY ?? process.stdout.isTTY === true,
|
|
1245
|
+
isProcessAlive: deps.isProcessAlive,
|
|
1243
1246
|
/* v8 ignore next -- thin wrapper: real Date.now fallback injected by default deps @preserve */
|
|
1244
1247
|
now: deps.now ?? (() => Date.now()),
|
|
1245
1248
|
/* v8 ignore next -- thin wrapper: real setTimeout fallback injected by default deps @preserve */
|
|
@@ -1295,7 +1298,9 @@ function formatDaemonStartupFailureMessage(pid, failure, deps) {
|
|
|
1295
1298
|
const lines = [
|
|
1296
1299
|
`background service started (pid ${pid ?? "unknown"}) but did not finish booting: ${failure.reason}`,
|
|
1297
1300
|
];
|
|
1298
|
-
const recentLogLines =
|
|
1301
|
+
const recentLogLines = failure.includeRecentLogs === false
|
|
1302
|
+
? []
|
|
1303
|
+
: deps.readRecentDaemonLogLines?.(DEFAULT_DAEMON_STARTUP_LOG_LINES) ?? [];
|
|
1299
1304
|
if (recentLogLines.length > 0) {
|
|
1300
1305
|
lines.push("recent daemon logs:");
|
|
1301
1306
|
lines.push(...recentLogLines.map((line) => ` ${line}`));
|
|
@@ -1303,6 +1308,19 @@ function formatDaemonStartupFailureMessage(pid, failure, deps) {
|
|
|
1303
1308
|
lines.push("Run `ouro logs` to watch live startup logs or `ouro doctor` for a deeper diagnosis.");
|
|
1304
1309
|
return lines.join("\n");
|
|
1305
1310
|
}
|
|
1311
|
+
function readFreshDaemonStartupFailure(deps, options) {
|
|
1312
|
+
const tombstone = deps.readDaemonStartupTombstone?.() ?? null;
|
|
1313
|
+
if (!tombstone || (tombstone.reason !== daemon_bootstrap_startup_1.PUBLIC_DAEMON_STARTUP_FAILURE_REASON && tombstone.reason !== "startupFailure"))
|
|
1314
|
+
return null;
|
|
1315
|
+
const timestampMs = Date.parse(tombstone.timestamp);
|
|
1316
|
+
if (!Number.isFinite(timestampMs) || timestampMs < options.bootStartedAtMs)
|
|
1317
|
+
return null;
|
|
1318
|
+
if (options.pid !== null && tombstone.pid !== options.pid)
|
|
1319
|
+
return null;
|
|
1320
|
+
return tombstone.reason === daemon_bootstrap_startup_1.PUBLIC_DAEMON_STARTUP_FAILURE_REASON
|
|
1321
|
+
? tombstone.message
|
|
1322
|
+
: "background service failed during startup; run `ouro doctor` for diagnosis";
|
|
1323
|
+
}
|
|
1306
1324
|
async function waitForDaemonStartup(deps, options) {
|
|
1307
1325
|
const now = deps.now ?? Date.now;
|
|
1308
1326
|
const sleep = deps.sleep ?? defaultSleep;
|
|
@@ -1316,6 +1334,16 @@ async function waitForDaemonStartup(deps, options) {
|
|
|
1316
1334
|
if (!useHealthMonitor) {
|
|
1317
1335
|
while (now() < deadline) {
|
|
1318
1336
|
await sleep(pollIntervalMs);
|
|
1337
|
+
const startupFailure = readFreshDaemonStartupFailure(deps, options);
|
|
1338
|
+
if (startupFailure)
|
|
1339
|
+
return { reason: startupFailure, retryable: false, includeRecentLogs: false };
|
|
1340
|
+
if (options.pid !== null && deps.isProcessAlive && !deps.isProcessAlive(options.pid)) {
|
|
1341
|
+
return {
|
|
1342
|
+
reason: `${options.serviceLabel} exited before answering`,
|
|
1343
|
+
retryable: false,
|
|
1344
|
+
includeRecentLogs: false,
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1319
1347
|
const latestEvent = options.readLatestDaemonEvent?.() ?? null;
|
|
1320
1348
|
deps.reportDaemonStartupPhase?.(formatDaemonStartupProgressLine(options.serviceLabel, "waiting", latestEvent));
|
|
1321
1349
|
if (await deps.checkSocketAlive(deps.socketPath))
|
|
@@ -1328,8 +1356,18 @@ async function waitForDaemonStartup(deps, options) {
|
|
|
1328
1356
|
}
|
|
1329
1357
|
while (now() < deadline) {
|
|
1330
1358
|
await sleep(pollIntervalMs);
|
|
1359
|
+
const startupFailure = readFreshDaemonStartupFailure(deps, options);
|
|
1360
|
+
if (startupFailure)
|
|
1361
|
+
return { reason: startupFailure, retryable: false, includeRecentLogs: false };
|
|
1331
1362
|
const aliveNow = await deps.checkSocketAlive(deps.socketPath);
|
|
1332
1363
|
if (!aliveNow) {
|
|
1364
|
+
if (options.pid !== null && deps.isProcessAlive && !deps.isProcessAlive(options.pid)) {
|
|
1365
|
+
return {
|
|
1366
|
+
reason: `${options.serviceLabel} exited before answering`,
|
|
1367
|
+
retryable: false,
|
|
1368
|
+
includeRecentLogs: false,
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1333
1371
|
if (sawSocket) {
|
|
1334
1372
|
return {
|
|
1335
1373
|
reason: `${options.serviceLabel} answered once and then disappeared during startup`,
|
|
@@ -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;
|
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PUBLIC_DAEMON_STARTUP_FAILURE_REASON = void 0;
|
|
4
|
+
exports.createProviderReadinessPreparationFailure = createProviderReadinessPreparationFailure;
|
|
3
5
|
exports.failFastContainerCredentialBootstrapStartup = failFastContainerCredentialBootstrapStartup;
|
|
4
6
|
exports.startDaemonAfterContainerCredentialBootstrap = startDaemonAfterContainerCredentialBootstrap;
|
|
5
7
|
const runtime_1 = require("../../nerves/runtime");
|
|
6
8
|
const daemon_tombstone_1 = require("./daemon-tombstone");
|
|
7
9
|
const REDACTED_BOOTSTRAP_STARTUP_ERROR = "container credential bootstrap rejected; recoverable claim retained for reconciliation";
|
|
10
|
+
const REDACTED_DAEMON_PREPARATION_ERROR = "provider runtime preparation failed before startup; run `ouro doctor` for diagnosis";
|
|
11
|
+
exports.PUBLIC_DAEMON_STARTUP_FAILURE_REASON = "startupFailurePublic";
|
|
12
|
+
class DaemonPreparationFailure extends Error {
|
|
13
|
+
name = "DaemonPreparationFailure";
|
|
14
|
+
}
|
|
15
|
+
function createProviderReadinessPreparationFailure(issues) {
|
|
16
|
+
const lines = ["Provider checks need attention"];
|
|
17
|
+
for (const issue of issues) {
|
|
18
|
+
lines.push(issue.summary);
|
|
19
|
+
lines.push(...issue.actions.map((action) => ` ${action.actor}: ${action.command}`));
|
|
20
|
+
}
|
|
21
|
+
return new DaemonPreparationFailure(lines.join("\n"));
|
|
22
|
+
}
|
|
8
23
|
function failFastContainerCredentialBootstrapStartup(input) {
|
|
9
|
-
|
|
24
|
+
failFastDaemonStartup({
|
|
25
|
+
exit: input.exit,
|
|
26
|
+
errorMessage: REDACTED_BOOTSTRAP_STARTUP_ERROR,
|
|
27
|
+
eventMessage: "daemon entrypoint failed before server startup",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function failFastDaemonStartup(input) {
|
|
31
|
+
const errorMessage = input.errorMessage;
|
|
32
|
+
const error = new Error(errorMessage);
|
|
10
33
|
try {
|
|
11
|
-
(0, daemon_tombstone_1.writeDaemonTombstone)(
|
|
34
|
+
(0, daemon_tombstone_1.writeDaemonTombstone)(exports.PUBLIC_DAEMON_STARTUP_FAILURE_REASON, error);
|
|
12
35
|
}
|
|
13
36
|
catch {
|
|
14
37
|
// Exit remains mandatory even if best-effort tombstone reporting fails.
|
|
@@ -18,8 +41,8 @@ function failFastContainerCredentialBootstrapStartup(input) {
|
|
|
18
41
|
level: "error",
|
|
19
42
|
component: "daemon",
|
|
20
43
|
event: "daemon.entry_error",
|
|
21
|
-
message:
|
|
22
|
-
meta: { error:
|
|
44
|
+
message: input.eventMessage,
|
|
45
|
+
meta: { error: errorMessage },
|
|
23
46
|
});
|
|
24
47
|
}
|
|
25
48
|
catch {
|
|
@@ -36,6 +59,19 @@ async function startDaemonAfterContainerCredentialBootstrap(input) {
|
|
|
36
59
|
failFastContainerCredentialBootstrapStartup({ exit: input.exit });
|
|
37
60
|
return false;
|
|
38
61
|
}
|
|
62
|
+
try {
|
|
63
|
+
await input.prepareDaemon?.();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
input.markStartupFailure();
|
|
67
|
+
const controlledMessage = error instanceof DaemonPreparationFailure ? error.message : null;
|
|
68
|
+
failFastDaemonStartup({
|
|
69
|
+
exit: input.exit,
|
|
70
|
+
errorMessage: controlledMessage ?? REDACTED_DAEMON_PREPARATION_ERROR,
|
|
71
|
+
eventMessage: controlledMessage ?? REDACTED_DAEMON_PREPARATION_ERROR,
|
|
72
|
+
});
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
39
75
|
await input.startDaemon();
|
|
40
76
|
return true;
|
|
41
77
|
}
|