@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.
@@ -74,7 +74,6 @@ const runtime_credentials_1 = require("../runtime-credentials");
74
74
  const machine_identity_1 = require("../machine-identity");
75
75
  const container_credential_bootstrap_1 = require("./container-credential-bootstrap");
76
76
  const daemon_bootstrap_startup_1 = require("./daemon-bootstrap-startup");
77
- const auth_flow_1 = require("../auth/auth-flow");
78
77
  const sanctuary_health_runner_1 = require("../../senses/sanctuary-health-runner");
79
78
  const sanctuary_acceptance_marker_1 = require("./sanctuary-acceptance-marker");
80
79
  const sanctuary_scheduler_liveness_1 = require("./sanctuary-scheduler-liveness");
@@ -588,77 +587,40 @@ function writeStopCommandHealthState() {
588
587
  // Health writes are best-effort during shutdown.
589
588
  }
590
589
  }
591
- function providerPreloadTargets(agent) {
592
- try {
593
- const { config } = (0, auth_flow_1.readAgentConfigForAgent)(agent, (0, identity_1.getAgentBundlesRoot)());
594
- return [...new Set([config.humanFacing.provider, config.agentFacing.provider])];
595
- }
596
- catch (error) {
597
- (0, runtime_1.emitNervesEvent)({
598
- level: "warn",
599
- component: "daemon",
600
- event: "daemon.provider_preload_skipped",
601
- message: "skipping provider credential preload because agent config could not be read",
602
- meta: {
603
- agent,
604
- error: error instanceof Error ? error.message : /* v8 ignore next -- defensive non-Error config-read failures @preserve */ String(error),
605
- },
606
- });
607
- return [];
608
- }
609
- }
610
- function providerPreloadMissingTargets(result, providers) {
611
- return providers.filter((provider) => !result.pool.providers[provider]);
612
- }
613
- async function preloadProviderCredentialPools() {
614
- await Promise.all(managedAgents.map(async (agent) => {
615
- const providers = providerPreloadTargets(agent);
616
- if (providers.length === 0)
617
- return;
618
- const result = await (0, provider_credentials_1.refreshProviderCredentialPool)(agent, { preserveCachedOnFailure: true, providers });
619
- if (result.ok) {
620
- const missingProviders = providerPreloadMissingTargets(result, providers);
621
- if (missingProviders.length > 0) {
622
- (0, runtime_1.emitNervesEvent)({
623
- level: "warn",
624
- component: "daemon",
625
- event: "daemon.provider_preload_unavailable",
626
- message: "provider credential preload returned an incomplete selected provider cache",
627
- meta: {
628
- agent,
629
- reason: "missing",
630
- error: `missing selected providers: ${missingProviders.join(", ")}`,
631
- },
632
- });
633
- return;
634
- }
635
- const records = Object.values(result.pool.providers).filter((record) => !!record);
636
- (0, provider_credentials_1.cacheProviderCredentialRecords)(agent, records, new Date(result.pool.updatedAt));
637
- return;
590
+ async function prepareProviderRuntime() {
591
+ const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
592
+ const readiness = await Promise.all(managedAgents.map(async (agent) => {
593
+ try {
594
+ const result = await (0, agent_config_check_1.checkAgentConfigWithProviderHealth)(agent, bundlesRoot);
595
+ if (result.ok)
596
+ return null;
597
+ (0, runtime_1.emitNervesEvent)({
598
+ level: "warn",
599
+ component: "daemon",
600
+ event: "daemon.provider_readiness_unavailable",
601
+ message: "fresh provider readiness was unavailable before daemon startup",
602
+ meta: { agent },
603
+ });
604
+ return result.issue ?? {
605
+ summary: `${agent}: provider runtime unavailable`,
606
+ actions: [{ actor: "agent-runnable", command: "ouro doctor" }],
607
+ };
608
+ }
609
+ catch {
610
+ (0, runtime_1.emitNervesEvent)({
611
+ level: "warn",
612
+ component: "daemon",
613
+ event: "daemon.provider_readiness_unavailable",
614
+ message: "fresh provider readiness check failed before daemon startup",
615
+ meta: { agent },
616
+ });
617
+ throw new Error("provider runtime preparation failed");
638
618
  }
639
- (0, runtime_1.emitNervesEvent)({
640
- level: "warn",
641
- component: "daemon",
642
- event: "daemon.provider_preload_unavailable",
643
- message: "provider credential preload could not refresh selected provider cache",
644
- meta: {
645
- agent,
646
- reason: result.reason,
647
- error: result.error,
648
- },
649
- });
650
619
  }));
651
- }
652
- function startProviderCredentialPoolPreload() {
653
- return preloadProviderCredentialPools().catch((error) => {
654
- (0, runtime_1.emitNervesEvent)({
655
- level: "error",
656
- component: "daemon",
657
- event: "daemon.provider_preload_error",
658
- message: "provider credential preload failed after daemon startup",
659
- meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive non-Error provider preload failures @preserve */ String(error) },
660
- });
661
- });
620
+ const failures = readiness.filter((entry) => entry !== null);
621
+ if (failures.length > 0) {
622
+ throw (0, daemon_bootstrap_startup_1.createProviderReadinessPreparationFailure)(failures);
623
+ }
662
624
  }
663
625
  function scheduleStartupSentinelAfterProviderPreload(agent, preload) {
664
626
  void preload.then(async () => {
@@ -693,6 +655,7 @@ function scheduleStartupSentinelAfterProviderPreload(agent, preload) {
693
655
  /* v8 ignore start -- habit wiring: lambdas delegate to processManager/fs; tested via HabitScheduler unit tests @preserve */
694
656
  void (0, daemon_bootstrap_startup_1.startDaemonAfterContainerCredentialBootstrap)({
695
657
  loadBootstrap: () => (0, container_credential_bootstrap_1.loadContainerCredentialBootstrap)(managedAgents),
658
+ prepareDaemon: prepareProviderRuntime,
696
659
  startDaemon: () => daemon.start(),
697
660
  markStartupFailure: () => { _tombstoneWritten = true; },
698
661
  exit: (code) => process.exit(code),
@@ -700,7 +663,7 @@ void (0, daemon_bootstrap_startup_1.startDaemonAfterContainerCredentialBootstrap
700
663
  if (!started)
701
664
  return;
702
665
  supercronicSupervisor?.start();
703
- const providerPreload = startProviderCredentialPoolPreload();
666
+ const providerPreload = Promise.resolve();
704
667
  const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
705
668
  const ouroPath = supercronicSupervisor
706
669
  ? "/usr/local/bin/node /opt/ouro/dist/heart/daemon/ouro-entry.js"
@@ -618,6 +618,7 @@ class OuroDaemon {
618
618
  senseAutostartTimer = null;
619
619
  externalEventReconcileTimer = null;
620
620
  externalEventReconcileRunning = false;
621
+ constructedAtMs = Date.now();
621
622
  mailboxServerFactory;
622
623
  privateRuntimePolicyDeps;
623
624
  onStopCommandComplete;
@@ -1503,7 +1504,7 @@ class OuroDaemon {
1503
1504
  }
1504
1505
  return { type: "message", privateTurnDecision: decision, ...(externalEvent ? { externalEvent } : {}) };
1505
1506
  }
1506
- queueExternalEventForPrivateRuntime(record) {
1507
+ queueExternalEventForPrivateRuntime(record, quiet = false) {
1507
1508
  const pendingDir = path.join(this.bundlesRoot, `${record.agent}.ouro`, "state", "pending", pending_1.PRIVATE_RUNTIME_PENDING.friendId, pending_1.PRIVATE_RUNTIME_PENDING.channel, pending_1.PRIVATE_RUNTIME_PENDING.key);
1508
1509
  const originKey = `${record.source}:${record.eventId}`;
1509
1510
  const parsedReceivedAt = Date.parse(record.receivedAt);
@@ -1523,18 +1524,20 @@ class OuroDaemon {
1523
1524
  mode: "relay",
1524
1525
  packetId: `external-event:${record.agent}:${record.source}:${record.eventId}:generation:${record.generation}:attempt:${record.attemptCount}`,
1525
1526
  });
1526
- (0, runtime_1.emitNervesEvent)({
1527
- component: "daemon",
1528
- event: "daemon.external_event_private_runtime_queued",
1529
- message: "queued external event for private-runtime attention",
1530
- meta: {
1531
- agent: record.agent,
1532
- source: record.source,
1533
- eventType: record.eventType,
1534
- eventId: record.eventId,
1535
- pendingDir,
1536
- },
1537
- });
1527
+ if (!quiet) {
1528
+ (0, runtime_1.emitNervesEvent)({
1529
+ component: "daemon",
1530
+ event: "daemon.external_event_private_runtime_queued",
1531
+ message: "queued external event for private-runtime attention",
1532
+ meta: {
1533
+ agent: record.agent,
1534
+ source: record.source,
1535
+ eventType: record.eventType,
1536
+ eventId: record.eventId,
1537
+ pendingDir,
1538
+ },
1539
+ });
1540
+ }
1538
1541
  }
1539
1542
  externalEventRootPath() {
1540
1543
  return this.externalEventRoot ?? (0, router_1.getExternalEventRoot)();
@@ -1575,8 +1578,19 @@ class OuroDaemon {
1575
1578
  source: primary.source,
1576
1579
  eventType: primary.eventType,
1577
1580
  eventId: primary.eventId,
1578
- }, receipt.id, primary.generation, primary.attemptCount), () => { for (const record of claimed)
1579
- this.queueExternalEventForPrivateRuntime(record); }, lease);
1581
+ }, receipt.id, primary.generation, primary.attemptCount), () => {
1582
+ const batched = claimed.length > 1;
1583
+ for (const record of claimed)
1584
+ this.queueExternalEventForPrivateRuntime(record, batched);
1585
+ if (batched) {
1586
+ (0, runtime_1.emitNervesEvent)({
1587
+ component: "daemon",
1588
+ event: "daemon.external_event_private_runtime_batch_queued",
1589
+ message: "queued an external-event batch for private-runtime attention",
1590
+ meta: { agent: primary.agent, source: primary.source, eventCount: claimed.length },
1591
+ });
1592
+ }
1593
+ }, lease);
1580
1594
  failureClass = wake.denialCode === "managed_runtime_unavailable"
1581
1595
  ? "managed_runtime_unavailable"
1582
1596
  : wake.data?.decision?.denialCode === "provider_lane_unavailable"
@@ -1616,6 +1630,9 @@ class OuroDaemon {
1616
1630
  }
1617
1631
  const statuses = (0, router_1.listExternalEventStatus)(this.externalEventRootPath());
1618
1632
  const dueRecords = [];
1633
+ const providerEvidenceByAgent = new Map();
1634
+ const runtimeEvidenceByAgent = new Map();
1635
+ const recoveredByClass = new Map();
1619
1636
  for (const status of statuses) {
1620
1637
  if (status.corrupt)
1621
1638
  continue;
@@ -1629,15 +1646,35 @@ class OuroDaemon {
1629
1646
  let evidence = null;
1630
1647
  try {
1631
1648
  if (failure.class === "provider_lane_unavailable") {
1632
- const binding = (0, provider_binding_resolver_1.resolveEffectiveProviderBinding)({
1633
- agentName: record.agent,
1634
- agentRoot: path.join(this.bundlesRoot, `${record.agent}.ouro`),
1635
- lane: "inner",
1636
- });
1637
- if (binding.ok && binding.binding.credential.status === "present"
1638
- && binding.binding.readiness.status === "ready" && binding.binding.readiness.checkedAt
1639
- && Date.parse(binding.binding.readiness.checkedAt) > Date.parse(failure.failedAt)) {
1640
- evidence = { class: failure.class, observedAt: binding.binding.readiness.checkedAt };
1649
+ if (!providerEvidenceByAgent.has(record.agent)) {
1650
+ providerEvidenceByAgent.set(record.agent, null);
1651
+ const binding = (0, provider_binding_resolver_1.resolveEffectiveProviderBinding)({
1652
+ agentName: record.agent,
1653
+ agentRoot: path.join(this.bundlesRoot, `${record.agent}.ouro`),
1654
+ lane: "inner",
1655
+ });
1656
+ providerEvidenceByAgent.set(record.agent, binding.ok && binding.binding.credential.status === "present"
1657
+ && binding.binding.readiness.status === "ready" && binding.binding.readiness.checkedAt
1658
+ ? { observedAt: binding.binding.readiness.checkedAt }
1659
+ : null);
1660
+ }
1661
+ const current = providerEvidenceByAgent.get(record.agent);
1662
+ const readinessIsCurrentBoot = current && Date.parse(current.observedAt) >= this.constructedAtMs;
1663
+ if (current && ((0, router_1.isExactLegacyProviderRecoveryFailure)(record)
1664
+ ? readinessIsCurrentBoot
1665
+ : Date.parse(current.observedAt) > Date.parse(failure.failedAt))) {
1666
+ evidence = { class: failure.class, observedAt: current.observedAt };
1667
+ }
1668
+ }
1669
+ else if (failure.class === "execution_lease_expired") {
1670
+ if (!runtimeEvidenceByAgent.has(record.agent)) {
1671
+ const snapshot = this.processManager.listAgentSnapshots().find((candidate) => candidate.name === record.agent && candidate.channel === "private-runtime"
1672
+ && candidate.status === "running" && candidate.pid !== null && candidate.startedAt !== null);
1673
+ runtimeEvidenceByAgent.set(record.agent, snapshot?.startedAt ? { observedAt: snapshot.startedAt } : null);
1674
+ }
1675
+ const current = runtimeEvidenceByAgent.get(record.agent);
1676
+ if (current && Date.parse(current.observedAt) > Date.parse(failure.failedAt)) {
1677
+ evidence = { class: failure.class, observedAt: current.observedAt };
1641
1678
  }
1642
1679
  }
1643
1680
  else if (this.hasManagedPrivateRuntime(record.agent) && Date.parse(now) > Date.parse(failure.failedAt)) {
@@ -1645,8 +1682,9 @@ class OuroDaemon {
1645
1682
  }
1646
1683
  if (!evidence)
1647
1684
  continue;
1648
- const revival = (0, router_1.reviveExternalEventAfterRecovery)(record.recordPath, { expectedVersion: record.version, expectedGeneration: record.generation, evidence, now: () => now });
1685
+ const revival = (0, router_1.reviveExternalEventAfterRecovery)(record.recordPath, { expectedVersion: record.version, expectedGeneration: record.generation, evidence, now: () => now, quiet: true });
1649
1686
  record = revival.record;
1687
+ recoveredByClass.set(failure.class, (recoveredByClass.get(failure.class) ?? 0) + 1);
1650
1688
  }
1651
1689
  catch (error) {
1652
1690
  (0, runtime_1.emitNervesEvent)({
@@ -1668,6 +1706,14 @@ class OuroDaemon {
1668
1706
  if (due)
1669
1707
  dueRecords.push(record);
1670
1708
  }
1709
+ if (recoveredByClass.size > 0) {
1710
+ (0, runtime_1.emitNervesEvent)({
1711
+ component: "daemon",
1712
+ event: "daemon.external_event_recovery_batch",
1713
+ message: "requeued external events after cached infrastructure recovery evidence",
1714
+ meta: { recovered: Object.fromEntries(recoveredByClass) },
1715
+ });
1716
+ }
1671
1717
  const batches = new Map();
1672
1718
  for (const record of dueRecords) {
1673
1719
  const key = `${record.agent}\0${record.source}`;
@@ -1968,6 +2014,26 @@ class OuroDaemon {
1968
2014
  data: dispatched,
1969
2015
  };
1970
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
+ }
1971
2037
  case "private.decisions": {
1972
2038
  const requestedLimit = Number.isInteger(command.limit) && command.limit && command.limit > 0 ? command.limit : 20;
1973
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;
@@ -15,6 +15,7 @@ exports.assessStability = assessStability;
15
15
  exports.renderStartupProgress = renderStartupProgress;
16
16
  exports.renderWaitingForDaemon = renderWaitingForDaemon;
17
17
  exports.pollDaemonStartup = pollDaemonStartup;
18
+ exports.defaultIsProcessAlive = defaultIsProcessAlive;
18
19
  const cli_render_1 = require("./cli-render");
19
20
  const runtime_1 = require("../../nerves/runtime");
20
21
  const terminal_ui_1 = require("./terminal-ui");