@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 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": [
@@ -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"; then
1694
+ && verify_sanctuary_sab_readiness "$IMAGE_ID" \
1695
+ && verify_sanctuary_telegram_readiness "$IMAGE_ID"; then
1680
1696
  :
1681
1697
  else
1682
- SAB_READINESS_STATUS=$?
1683
- (exit "$SAB_READINESS_STATUS")
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 staging starts:
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
- Put the entire post-rename staging phase in one explicit conditional so
1782
- `set -eu` cannot bypass rollback at the create, effective-audit, start, or
1783
- bounded-readiness boundary. Staging uses the exact image ID with no command,
1784
- environment, port, device, capability, privilege, or extra mount override.
1785
- A passing staging container is stopped and removed inside the same condition,
1786
- completing the poller handoff before production creation:
1787
- if docker create --name ouro-butler-staging --network host --restart unless-stopped --user 10001:10001 \
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 reads the new bot token from host file
2292
- descriptor 3; callback injection reads the reviewed saved callback-update JSON
2293
- from the same descriptor. The launcher explicitly maps host fd 3 to Docker
2294
- stdin and then to in-container fd 3 only for those two commands. Neither value
2295
- belongs in argv, shell history, a config file, or an unrelated command's stdin.
2296
- Define this Bash helper in the root shell. It disables terminal echo while
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
- run_unit16_telegram_bootstrap
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 telegram-bootstrap|callback-inject) test -r /proc/self/fd/3 || exit 2 ;; esac
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=yes; BUNDLE_MODE=readonly; BROKER=no ;;
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 -i --pull=never --network "$NETWORK" \
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 /bin/sh "$IMAGE_ID" -ceu \
441
- 'exec 3<&0; exec /opt/ouro/deploy/unraid/sanctuary-acceptance-harness.sh "$@" 3<&3' \
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,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.772",
2
+ "runtimeVersion": "0.1.0-alpha.774",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-08-30T00:00:00.000Z"
5
5
  }
@@ -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.772</Repository>
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 = deps.readRecentDaemonLogLines?.(DEFAULT_DAEMON_STARTUP_LOG_LINES) ?? [];
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 a verified external event to an owning agent and optionally wake its private runtime",
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
- const error = new Error(REDACTED_BOOTSTRAP_STARTUP_ERROR);
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)("startupFailure", error);
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: "daemon entrypoint failed before server startup",
22
- meta: { error: REDACTED_BOOTSTRAP_STARTUP_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
  }