@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 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": [
@@ -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.773",
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.773</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>
@@ -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;
@@ -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
- const vaultAdapter = adapter(config.vaultAdapter, "vaultAdapter");
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
- if (!token)
886
- throw new Error("Telegram token descriptor is empty");
887
- const bot = object(await telegramRequest(deps, token, "getMe"), "Telegram getMe result");
888
- if (String(bot.id) !== expectedBotId || bot.username !== expectedUsername)
889
- throw new Error("Telegram bot identity mismatch");
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
- const updates = await telegramRequest(deps, token, "getUpdates", {
913
- offset: nextOffset,
914
- timeout: pollTimeoutSeconds,
915
- allowed_updates: ["message"],
916
- }, (pollTimeoutSeconds + 5) * 1_000);
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
- const stored = object(await deps.runAdapter(vaultAdapter, {
956
- operation: "store_telegram_bootstrap",
957
- botToken: token,
958
- authorizedUserId: userId,
959
- authorizedChatId: chatId,
960
- }), "vault adapter result");
961
- if (stored.stored !== true)
962
- throw new Error("Telegram vault adapter did not attest storage");
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 telegramBootstrap(config, deps);
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
- return commitMutation(recordPath, retryState(record, now, maxAttempts, baseDelayMs, "execution lease expired", "execution_lease_expired"), now);
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) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.773",
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.773",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.773",
3
+ "version": "0.1.0-alpha.774",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },