@ouro.bot/cli 0.1.0-alpha.733 → 0.1.0-alpha.734

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.
Files changed (89) hide show
  1. package/changelog.json +25 -0
  2. package/deploy/unraid/Dockerfile +27 -0
  3. package/deploy/unraid/README.txt +1368 -0
  4. package/deploy/unraid/audit-container-spec.sh +4 -0
  5. package/deploy/unraid/container-runtime.json +1 -0
  6. package/deploy/unraid/sanctuary-acceptance-adapter.sh +37 -0
  7. package/deploy/unraid/sanctuary-acceptance-contract.json +200 -0
  8. package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
  9. package/deploy/unraid/sanctuary-deployment-target.mjs +705 -0
  10. package/deploy/unraid/sanctuary-safe-rename.py +24 -0
  11. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +1777 -0
  12. package/deploy/unraid/sanctuary-unit16-run.sh +502 -0
  13. package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
  14. package/deploy/unraid/sanctuary.ouro/agent.json +24 -0
  15. package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
  16. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
  17. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
  18. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +18 -0
  19. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -0
  20. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -0
  21. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +3 -0
  22. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +5 -0
  23. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +7 -0
  24. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +21 -0
  25. package/deploy/unraid/sanctuary.xml +25 -0
  26. package/dist/heart/approval-files.js +113 -0
  27. package/dist/heart/approval-store.js +168 -9
  28. package/dist/heart/auth/auth-flow.js +17 -1
  29. package/dist/heart/autonomy-budget.js +44 -1
  30. package/dist/heart/awaiting/await-scheduler.js +6 -2
  31. package/dist/heart/config.js +8 -0
  32. package/dist/heart/core.js +117 -132
  33. package/dist/heart/daemon/cli-exec.js +73 -1
  34. package/dist/heart/daemon/cli-help.js +5 -0
  35. package/dist/heart/daemon/cli-parse.js +6 -4
  36. package/dist/heart/daemon/container-credential-bootstrap.js +185 -0
  37. package/dist/heart/daemon/container-healthcheck.js +94 -0
  38. package/dist/heart/daemon/container-runtime.js +84 -0
  39. package/dist/heart/daemon/container-spec-auditor-main.js +156 -0
  40. package/dist/heart/daemon/container-spec-auditor.js +156 -0
  41. package/dist/heart/daemon/daemon-bootstrap-startup.js +41 -0
  42. package/dist/heart/daemon/daemon-entry.js +116 -28
  43. package/dist/heart/daemon/daemon-health.js +10 -0
  44. package/dist/heart/daemon/daemon.js +92 -5
  45. package/dist/heart/daemon/runtime-logging.js +6 -0
  46. package/dist/heart/daemon/runtime-mode.js +13 -2
  47. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2365 -0
  48. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1624 -0
  49. package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
  50. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +985 -0
  51. package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
  52. package/dist/heart/daemon/sanctuary-scheduler-origin.js +126 -0
  53. package/dist/heart/daemon/sense-manager.js +25 -6
  54. package/dist/heart/daemon/supercronic-supervisor.js +267 -0
  55. package/dist/heart/habits/habit-scheduler.js +7 -3
  56. package/dist/heart/identity.js +13 -3
  57. package/dist/heart/provider-credentials.js +9 -1
  58. package/dist/heart/provider-models.js +8 -0
  59. package/dist/heart/provider-ping.js +5 -2
  60. package/dist/heart/providers/openai-compatible.js +177 -0
  61. package/dist/heart/runtime-credentials.js +217 -8
  62. package/dist/heart/sense-truth.js +2 -0
  63. package/dist/heart/tool-approval.js +1 -0
  64. package/dist/heart/turn-context.js +8 -0
  65. package/dist/mind/prompt.js +9 -0
  66. package/dist/nerves/coverage/source-scanner.js +4 -4
  67. package/dist/nerves/index.js +75 -9
  68. package/dist/nerves/runtime.js +12 -0
  69. package/dist/repertoire/tool-arguments.js +39 -0
  70. package/dist/repertoire/tools-unraid.js +353 -0
  71. package/dist/repertoire/tools.js +19 -1
  72. package/dist/repertoire/unraid-client.js +155 -0
  73. package/dist/repertoire/unraid-restart.js +130 -0
  74. package/dist/senses/private-runtime-worker.js +3 -2
  75. package/dist/senses/sanctuary-grounding.js +118 -0
  76. package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
  77. package/dist/senses/sanctuary-health-acceptance-probe.js +772 -0
  78. package/dist/senses/sanctuary-health-runner.js +146 -0
  79. package/dist/senses/sanctuary-health.js +447 -0
  80. package/dist/senses/sanctuary-interactive-control.js +306 -0
  81. package/dist/senses/sanctuary-runtime.js +206 -0
  82. package/dist/senses/shared-turn.js +11 -3
  83. package/dist/senses/telegram-approval-runtime.js +526 -0
  84. package/dist/senses/telegram-audit-ledger.js +220 -0
  85. package/dist/senses/telegram-client.js +1471 -0
  86. package/dist/senses/telegram-entry.js +80 -0
  87. package/dist/senses/telegram.js +1232 -0
  88. package/npm-shrinkwrap.json +6558 -0
  89. package/package.json +4 -2
@@ -0,0 +1,21 @@
1
+ {
2
+ "version": 1,
3
+ "profiles": {
4
+ "sanctuary-telegram": [
5
+ "unraid_list_containers",
6
+ "unraid_get_container_logs",
7
+ "unraid_get_storage",
8
+ "unraid_get_disks",
9
+ "unraid_get_notifications",
10
+ "unraid_get_system",
11
+ "unraid_restart_container",
12
+ "ponder",
13
+ "settle",
14
+ "speak"
15
+ ],
16
+ "sanctuary-health-private": [
17
+ "send_message",
18
+ "rest"
19
+ ]
20
+ }
21
+ }
@@ -0,0 +1,25 @@
1
+ <?xml version="1.0"?>
2
+ <Container version="2">
3
+ <Name>ouro-butler</Name>
4
+ <Repository>sha256:REPLACE_WITH_EXACT_LOCAL_IMAGE_ID</Repository>
5
+ <Registry/>
6
+ <Network>host</Network>
7
+ <Shell>sh</Shell>
8
+ <Privileged>false</Privileged>
9
+ <Support/>
10
+ <Project/>
11
+ <Overview>Mendelow Cloud Butler</Overview>
12
+ <Category>Tools:</Category>
13
+ <WebUI/>
14
+ <TemplateURL/>
15
+ <Icon/>
16
+ <ExtraParams>--restart=unless-stopped --user=10001:10001</ExtraParams>
17
+ <PostArgs/>
18
+ <CPUset/>
19
+ <DateInstalled>0</DateInstalled>
20
+ <DonateText/>
21
+ <DonateLink/>
22
+ <Requires/>
23
+ <Config Name="Runtime state" Target="/home/ouro/.ouro-cli" Default="/mnt/user/appdata/ouro-butler/runtime/.ouro-cli" Mode="rw" Description="Machine-scoped runtime" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ouro-butler/runtime/.ouro-cli</Config>
24
+ <Config Name="Agent bundle" Target="/home/ouro/AgentBundles/sanctuary.ouro" Default="/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro" Mode="rw" Description="Sanctuary bundle" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro</Config>
25
+ </Container>
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FileApprovalTokenStore = exports.FileApprovalCheckpointStore = void 0;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const node_crypto_1 = require("node:crypto");
40
+ const tool_approval_1 = require("./tool-approval");
41
+ const runtime_1 = require("../nerves/runtime");
42
+ function readObject(filePath) {
43
+ try {
44
+ const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
45
+ if (!value || typeof value !== "object" || Array.isArray(value))
46
+ throw new Error("state is not an object");
47
+ return value;
48
+ }
49
+ catch (error) {
50
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
51
+ return {};
52
+ throw new Error(`approval state is corrupt: ${path.basename(filePath)}`, { cause: error });
53
+ }
54
+ }
55
+ function atomicWrite(filePath, value) {
56
+ const directory = path.dirname(filePath);
57
+ fs.mkdirSync(directory, { recursive: true });
58
+ const temporary = `${filePath}.tmp-${process.pid}-${(0, node_crypto_1.randomUUID)()}`;
59
+ fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
60
+ const temporaryHandle = fs.openSync(temporary, "r");
61
+ try {
62
+ fs.fsyncSync(temporaryHandle);
63
+ }
64
+ finally {
65
+ fs.closeSync(temporaryHandle);
66
+ }
67
+ fs.renameSync(temporary, filePath);
68
+ fs.chmodSync(filePath, 0o600);
69
+ const directoryHandle = fs.openSync(directory, "r");
70
+ try {
71
+ fs.fsyncSync(directoryHandle);
72
+ }
73
+ finally {
74
+ fs.closeSync(directoryHandle);
75
+ }
76
+ }
77
+ class FileApprovalCheckpointStore {
78
+ filePath;
79
+ constructor(filePath) {
80
+ this.filePath = filePath;
81
+ }
82
+ write(draft) {
83
+ const checkpoint = {
84
+ ...structuredClone(draft),
85
+ checkpointDigest: (0, tool_approval_1.digestApprovalSuspensionCheckpointPayload)(draft),
86
+ suspendedSessionRevision: draft.baseSessionRevision,
87
+ };
88
+ const records = readObject(this.filePath);
89
+ records[checkpoint.approvalId] = checkpoint;
90
+ atomicWrite(this.filePath, records);
91
+ (0, runtime_1.emitNervesEvent)({ component: "engine", event: "engine.approval_checkpoint_written", message: "approval checkpoint written", meta: { approvalId: checkpoint.approvalId } });
92
+ return { checkpointDigest: checkpoint.checkpointDigest, suspendedSessionRevision: checkpoint.suspendedSessionRevision };
93
+ }
94
+ read(approvalId) { return structuredClone(readObject(this.filePath)[approvalId] ?? null); }
95
+ list() { return Object.values(readObject(this.filePath)).map((record) => structuredClone(record)); }
96
+ remove(approvalId) {
97
+ const records = readObject(this.filePath);
98
+ delete records[approvalId];
99
+ atomicWrite(this.filePath, records);
100
+ }
101
+ }
102
+ exports.FileApprovalCheckpointStore = FileApprovalCheckpointStore;
103
+ class FileApprovalTokenStore {
104
+ filePath;
105
+ constructor(filePath) {
106
+ this.filePath = filePath;
107
+ }
108
+ put(approvalId, token) { const records = readObject(this.filePath); records[approvalId] = token; atomicWrite(this.filePath, records); }
109
+ has(approvalId) { return approvalId in readObject(this.filePath); }
110
+ get(approvalId) { return readObject(this.filePath)[approvalId] ?? null; }
111
+ remove(approvalId) { const records = readObject(this.filePath); delete records[approvalId]; atomicWrite(this.filePath, records); }
112
+ }
113
+ exports.FileApprovalTokenStore = FileApprovalTokenStore;
@@ -40,6 +40,7 @@ exports.ApprovalStoreError = void 0;
40
40
  exports.canonicalApprovalArguments = canonicalApprovalArguments;
41
41
  exports.parseApprovalRecord = parseApprovalRecord;
42
42
  exports.openApprovalStore = openApprovalStore;
43
+ exports.readApprovalsByScenarioHandleDigest = readApprovalsByScenarioHandleDigest;
43
44
  const node_crypto_1 = require("node:crypto");
44
45
  const fs = __importStar(require("node:fs"));
45
46
  const path = __importStar(require("node:path"));
@@ -162,8 +163,10 @@ function validStateShape(record) {
162
163
  if (record.state === "abandoned_before_attempt") {
163
164
  const validPreparingRecovery = !hasOwner && record.epoch === 0
164
165
  && record.suspendedSessionRevision === null && record.transportMessageId === null;
166
+ const validPromptBindingRecovery = !hasOwner && record.epoch === 0
167
+ && hasSuspension && record.transportMessageId === null;
165
168
  const validClaimRecovery = hasOwner && record.epoch > 0 && hasSuspension && hasPromptBinding;
166
- return (validPreparingRecovery || validClaimRecovery)
169
+ return (validPreparingRecovery || validPromptBindingRecovery || validClaimRecovery)
167
170
  && !hasAttempt && record.result === null && isNonEmpty(record.reason);
168
171
  }
169
172
  if (record.state === "drifted") {
@@ -483,6 +486,15 @@ function openApprovalStore(options) {
483
486
  attempted_at TEXT,
484
487
  continued_at TEXT
485
488
  )
489
+ ;
490
+ CREATE TABLE IF NOT EXISTS approval_acceptance_scenarios (
491
+ approval_id TEXT PRIMARY KEY,
492
+ scenario_handle_digest TEXT NOT NULL,
493
+ FOREIGN KEY (approval_id) REFERENCES approval_actions(approval_id)
494
+ )
495
+ ;
496
+ CREATE INDEX IF NOT EXISTS approval_acceptance_scenario_digest
497
+ ON approval_acceptance_scenarios (scenario_handle_digest)
486
498
  `);
487
499
  const now = options.now ?? (() => new Date());
488
500
  const randomUUID = options.randomUUID ?? node_crypto_1.randomUUID;
@@ -501,9 +513,22 @@ function openApprovalStore(options) {
501
513
  fail("corrupt_record");
502
514
  }
503
515
  };
504
- const insert = (record) => {
505
- database.prepare("INSERT INTO approval_actions (approval_id, state, epoch, record_json) VALUES (?, ?, ?, ?)")
506
- .run(record.approvalId, record.state, record.epoch, JSON.stringify(record));
516
+ const insert = (record, scenarioHandleDigest) => {
517
+ const transaction = database.transaction(() => {
518
+ database.prepare("INSERT INTO approval_actions (approval_id, state, epoch, record_json) VALUES (?, ?, ?, ?)")
519
+ .run(record.approvalId, record.state, record.epoch, JSON.stringify(record));
520
+ if (scenarioHandleDigest)
521
+ database.prepare("INSERT INTO approval_acceptance_scenarios (approval_id, scenario_handle_digest) VALUES (?, ?)")
522
+ .run(record.approvalId, scenarioHandleDigest);
523
+ });
524
+ transaction();
525
+ if (scenarioHandleDigest)
526
+ (0, runtime_1.emitNervesEvent)({ component: "heart", event: "approval.acceptance_transition", message: "acceptance-bound approval transitioned", meta: { approvalId: record.approvalId, scenarioHandleDigest, toolName: record.toolName, argumentDigest: record.argumentDigest, state: record.state } });
527
+ };
528
+ const acceptanceDigest = (approvalId) => {
529
+ const row = database.prepare("SELECT scenario_handle_digest FROM approval_acceptance_scenarios WHERE approval_id = ?")
530
+ .get(approvalId);
531
+ return row?.scenario_handle_digest ?? null;
507
532
  };
508
533
  const cas = (previous, next) => {
509
534
  const changed = database.prepare(`
@@ -512,6 +537,9 @@ function openApprovalStore(options) {
512
537
  `).run(next.state, next.epoch, JSON.stringify(next), previous.approvalId, previous.state, previous.epoch, JSON.stringify(previous));
513
538
  if (changed.changes !== 1)
514
539
  fail("transition_conflict");
540
+ const scenarioHandleDigest = acceptanceDigest(next.approvalId);
541
+ if (scenarioHandleDigest)
542
+ (0, runtime_1.emitNervesEvent)({ component: "heart", event: "approval.acceptance_transition", message: "acceptance-bound approval transitioned", meta: { approvalId: next.approvalId, scenarioHandleDigest, toolName: next.toolName, argumentDigest: next.argumentDigest, state: next.state } });
515
543
  return structuredClone(next);
516
544
  };
517
545
  const requireRecord = (approvalId) => rawRead(approvalId) ?? fail("approval_not_found");
@@ -539,6 +567,9 @@ function openApprovalStore(options) {
539
567
  `).run(to, timestamp(), input.approvalId, input.ownerId, input.epoch, from);
540
568
  if (changed.changes !== 1)
541
569
  fail(`continuation_${to}_not_eligible`);
570
+ const scenarioHandleDigest = acceptanceDigest(input.approvalId);
571
+ if (scenarioHandleDigest)
572
+ (0, runtime_1.emitNervesEvent)({ component: "heart", event: "approval.acceptance_continuation_transition", message: "acceptance-bound approval continuation transitioned", meta: { approvalId: approval.approvalId, scenarioHandleDigest, toolName: approval.toolName, argumentDigest: approval.argumentDigest, state: to } });
542
573
  return combineContinuation(approval, readContinuationRow(input.approvalId));
543
574
  };
544
575
  return {
@@ -585,7 +616,9 @@ function openApprovalStore(options) {
585
616
  reason: null,
586
617
  frozenAssistantMessage: structuredClone(input.frozenAssistantMessage),
587
618
  };
588
- insert(parseApprovalRecord(record));
619
+ if (input.scenarioHandleDigest !== undefined)
620
+ assertHash(input.scenarioHandleDigest, "invalid_proposal");
621
+ insert(parseApprovalRecord(record), input.scenarioHandleDigest);
589
622
  return { record: structuredClone(record), decisionToken };
590
623
  });
591
624
  },
@@ -602,21 +635,41 @@ function openApprovalStore(options) {
602
635
  return observeBindPrompt(() => {
603
636
  const previous = requireRecord(input.approvalId);
604
637
  if (previous.state !== "awaiting_prompt_binding" || input.transport !== previous.transport
605
- || input.transportChatId !== previous.transportChatId || !isNonEmpty(input.transportMessageId))
638
+ || input.transportChatId !== previous.transportChatId || !isNonEmpty(input.transportMessageId)
639
+ || (input.expiresAt !== undefined && !isTimestamp(input.expiresAt)))
606
640
  fail("invalid_prompt_binding");
607
- return cas(previous, { ...previous, state: "proposed", transportMessageId: input.transportMessageId, updatedAt: timestamp() });
641
+ return cas(previous, { ...previous, state: "proposed", transportMessageId: input.transportMessageId, expiresAt: input.expiresAt ?? previous.expiresAt, updatedAt: timestamp() });
642
+ });
643
+ },
644
+ abandonPromptBinding(input) {
645
+ return observeAbandonBeforeAttempt(() => {
646
+ const previous = requireRecord(input.approvalId);
647
+ if (previous.state === "abandoned_before_attempt" && previous.ownerId === null
648
+ && previous.epoch === 0 && previous.reason === input.reason)
649
+ return previous;
650
+ if (previous.state !== "awaiting_prompt_binding" || !isNonEmpty(input.reason))
651
+ fail("prompt_abandon_not_eligible");
652
+ return cas(previous, {
653
+ ...previous,
654
+ state: "abandoned_before_attempt",
655
+ reason: input.reason,
656
+ updatedAt: timestamp(),
657
+ });
608
658
  });
609
659
  },
610
660
  decide(input) {
611
661
  return observeDecide(() => {
612
662
  const previous = requireRecord(input.approvalId);
663
+ const decisionAt = input.decisionAt ?? now().getTime();
664
+ if (!Number.isSafeInteger(decisionAt))
665
+ fail("invalid_decision_time");
613
666
  if (!sameDecisionBinding(previous, input))
614
667
  fail("decision_binding_mismatch");
615
668
  if (previous.state === "denied" && input.decision === "deny")
616
669
  return previous;
617
670
  if (previous.state !== "proposed")
618
671
  fail("decision_not_eligible");
619
- if (now().getTime() >= Date.parse(previous.expiresAt)) {
672
+ if (decisionAt >= Date.parse(previous.expiresAt)) {
620
673
  return cas(previous, { ...previous, state: "expired", updatedAt: timestamp() });
621
674
  }
622
675
  if (input.decision === "deny") {
@@ -625,7 +678,7 @@ function openApprovalStore(options) {
625
678
  if (!isNonEmpty(input.ownerId))
626
679
  fail("invalid_owner");
627
680
  options.hooks?.beforeClaimCas?.();
628
- if (now().getTime() >= Date.parse(previous.expiresAt)) {
681
+ if ((input.decisionAt ?? now().getTime()) >= Date.parse(previous.expiresAt)) {
629
682
  return cas(previous, { ...previous, state: "expired", updatedAt: timestamp() });
630
683
  }
631
684
  return cas(previous, { ...previous, state: "claimed", ownerId: input.ownerId, epoch: previous.epoch + 1, updatedAt: timestamp() });
@@ -753,6 +806,9 @@ function openApprovalStore(options) {
753
806
  interruptedAfterAttempt = terminalized.changes === 1;
754
807
  row = readContinuationRow(input.approvalId);
755
808
  }
809
+ const scenarioHandleDigest = acceptanceDigest(input.approvalId);
810
+ if (scenarioHandleDigest)
811
+ (0, runtime_1.emitNervesEvent)({ component: "heart", event: "approval.acceptance_continuation_transition", message: "acceptance-bound approval continuation transitioned", meta: { approvalId: approval.approvalId, scenarioHandleDigest, toolName: approval.toolName, argumentDigest: approval.argumentDigest, state: row.state } });
756
812
  return {
757
813
  claimed,
758
814
  interruptedAfterAttempt,
@@ -777,10 +833,87 @@ function openApprovalStore(options) {
777
833
  `).run(completedAt, input.approvalId, input.ownerId, input.epoch);
778
834
  if (changed.changes !== 1)
779
835
  fail("continuation_completion_not_eligible");
836
+ const scenarioHandleDigest = acceptanceDigest(input.approvalId);
837
+ if (scenarioHandleDigest)
838
+ (0, runtime_1.emitNervesEvent)({ component: "heart", event: "approval.acceptance_continuation_transition", message: "acceptance-bound approval continuation transitioned", meta: { approvalId: approval.approvalId, scenarioHandleDigest, toolName: approval.toolName, argumentDigest: approval.argumentDigest, state: "completed" } });
780
839
  return combineContinuation(approval, readContinuationRow(input.approvalId));
781
840
  });
782
841
  },
783
842
  read(approvalId) { return observeRead(() => rawRead(approvalId)); },
843
+ readByScenarioHandleDigest(scenarioHandleDigest) {
844
+ assertHash(scenarioHandleDigest, "invalid_scenario_handle_digest");
845
+ const rows = database.prepare(`
846
+ SELECT a.record_json
847
+ FROM approval_actions a
848
+ INNER JOIN approval_acceptance_scenarios s ON s.approval_id = a.approval_id
849
+ WHERE s.scenario_handle_digest = ?
850
+ ORDER BY a.approval_id
851
+ `).all(scenarioHandleDigest);
852
+ return rows.map((row) => parseApprovalRecord(JSON.parse(row.record_json)));
853
+ },
854
+ listTelegramIdentitySubjects() {
855
+ const subjects = new Set();
856
+ const add = (value) => {
857
+ if (/^tg_[A-Za-z0-9_-]{43}$/u.test(value))
858
+ subjects.add(value);
859
+ };
860
+ const rows = database.prepare("SELECT record_json FROM approval_actions ORDER BY approval_id")
861
+ .all();
862
+ for (const row of rows) {
863
+ const record = parseApprovalRecord(JSON.parse(row.record_json));
864
+ if (record.transport !== "telegram")
865
+ continue;
866
+ add(record.requesterId);
867
+ add(record.transportUserId);
868
+ add(record.transportChatId);
869
+ if (record.sessionKey.startsWith("telegram:"))
870
+ add(record.sessionKey.slice("telegram:".length));
871
+ for (const match of record.sessionPath.matchAll(/tg_[A-Za-z0-9_-]{43}/gu))
872
+ add(match[0]);
873
+ }
874
+ return [...subjects].sort();
875
+ },
876
+ migrateTelegramIdentity(input) {
877
+ if (!isNonEmpty(input.legacyUserId) || !isNonEmpty(input.legacyChatId) || !/^tg_[A-Za-z0-9_-]{43}$/u.test(input.subject)) {
878
+ fail("invalid_telegram_identity_migration");
879
+ }
880
+ const rows = database.prepare("SELECT approval_id, record_json FROM approval_actions ORDER BY approval_id")
881
+ .all();
882
+ const migrate = database.transaction(() => {
883
+ let migrated = 0;
884
+ for (const row of rows) {
885
+ const previous = parseApprovalRecord(JSON.parse(row.record_json));
886
+ if (previous.transport !== "telegram")
887
+ continue;
888
+ const next = {
889
+ ...previous,
890
+ sessionKey: previous.sessionKey === `telegram:${input.legacyChatId}` ? `telegram:${input.subject}` : previous.sessionKey,
891
+ sessionPath: previous.sessionPath
892
+ .replace(`telegram-user:${input.legacyUserId}`, `telegram-user:${input.subject}`)
893
+ .replace(`telegram_${input.legacyChatId}.json`, `telegram_${input.subject}.json`),
894
+ requesterId: previous.requesterId === input.legacyUserId ? input.subject : previous.requesterId,
895
+ transportUserId: previous.transportUserId === input.legacyUserId ? input.subject : previous.transportUserId,
896
+ transportChatId: previous.transportChatId === input.legacyChatId ? input.subject : previous.transportChatId,
897
+ transportMessageId: previous.transportMessageId && !previous.transportMessageId.startsWith("tgm_")
898
+ ? `tgm_${(0, node_crypto_1.createHmac)("sha256", input.subject).update(`message:${previous.transportMessageId}`, "utf8").digest("base64url")}`
899
+ : previous.transportMessageId,
900
+ };
901
+ const nextJson = JSON.stringify(parseApprovalRecord(next));
902
+ if (nextJson === row.record_json)
903
+ continue;
904
+ const changed = database.prepare("UPDATE approval_actions SET record_json = ? WHERE approval_id = ? AND record_json = ?")
905
+ .run(nextJson, row.approval_id, row.record_json);
906
+ if (changed.changes !== 1)
907
+ fail("telegram_identity_migration_conflict");
908
+ migrated += 1;
909
+ }
910
+ return migrated;
911
+ });
912
+ const migrated = migrate();
913
+ if (migrated > 0)
914
+ database.exec("VACUUM");
915
+ return migrated;
916
+ },
784
917
  close() {
785
918
  return observeClose(() => {
786
919
  options.hooks?.beforeClose?.();
@@ -789,3 +922,29 @@ function openApprovalStore(options) {
789
922
  },
790
923
  };
791
924
  }
925
+ function readApprovalsByScenarioHandleDigest(databasePath, scenarioHandleDigest) {
926
+ assertHash(scenarioHandleDigest, "invalid_scenario_handle_digest");
927
+ const database = new better_sqlite3_1.default(databasePath, { readonly: true, fileMustExist: true });
928
+ try {
929
+ const rows = database.prepare(`
930
+ SELECT a.record_json, c.owner_id, c.owner_pid, c.epoch AS continuation_epoch, c.state AS continuation_state,
931
+ c.claimed_at, c.materialized_at, c.attempted_at AS continuation_attempted_at, c.continued_at
932
+ FROM approval_actions a
933
+ INNER JOIN approval_acceptance_scenarios s ON s.approval_id = a.approval_id
934
+ LEFT JOIN approval_continuations c ON c.approval_id = a.approval_id
935
+ WHERE s.scenario_handle_digest = ?
936
+ ORDER BY a.approval_id
937
+ `).all(scenarioHandleDigest);
938
+ return rows.map((row) => ({
939
+ approval: parseApprovalRecord(JSON.parse(row.record_json)),
940
+ continuation: row.continuation_state === null ? null : {
941
+ continuationOwnerId: row.owner_id, continuationOwnerPid: row.owner_pid, continuationEpoch: row.continuation_epoch,
942
+ continuationState: row.continuation_state, continuationClaimedAt: row.claimed_at, continuationMaterializedAt: row.materialized_at,
943
+ continuationAttemptedAt: row.continuation_attempted_at, continuedAt: row.continued_at,
944
+ },
945
+ }));
946
+ }
947
+ finally {
948
+ database.close();
949
+ }
950
+ }
@@ -92,7 +92,9 @@ function readAgentConfigForAgent(agentName, bundlesRoot = (0, identity_1.getAgen
92
92
  provider !== "anthropic" &&
93
93
  provider !== "minimax" &&
94
94
  provider !== "openai-codex" &&
95
- provider !== "github-copilot") {
95
+ provider !== "github-copilot" &&
96
+ provider !== "openai-compatible" &&
97
+ provider !== "openai-compatible-gemini") {
96
98
  throw new Error(`agent.json at ${configPath} has unsupported provider '${String(provider)}'`);
97
99
  }
98
100
  if (!agentFacing || typeof agentFacing !== "object") {
@@ -106,6 +108,7 @@ function readAgentConfigForAgent(agentName, bundlesRoot = (0, identity_1.getAgen
106
108
  const config = {
107
109
  ...parsed,
108
110
  senses: (0, identity_1.normalizeSenses)(parsed.senses, configPath),
111
+ habitPaidTurnsPerDay: (0, identity_1.normalizeHabitPaidTurnsPerDay)(agentName, parsed.habitPaidTurnsPerDay, configPath),
109
112
  };
110
113
  return {
111
114
  configPath,
@@ -272,6 +275,19 @@ async function collectRuntimeAuthCredentials(input, deps) {
272
275
  const spawnSync = deps.spawnSync ?? child_process_1.spawnSync;
273
276
  const homeDir = deps.homeDir ?? os.homedir();
274
277
  const now = deps.now ?? (() => new Date());
278
+ if (input.provider === "openai-compatible" || input.provider === "openai-compatible-gemini") {
279
+ if (!input.promptSecret)
280
+ throw new Error(`${input.provider} API key entry requires an interactive terminal so the secret can be hidden.`);
281
+ const apiKey = (await input.promptSecret(`${input.provider === "openai-compatible" ? "Z.ai" : "Gemini"} API key: `)).trim();
282
+ if (!apiKey)
283
+ throw new Error(`${input.provider} API key is required.`);
284
+ return {
285
+ apiKey,
286
+ baseUrl: input.provider === "openai-compatible"
287
+ ? "https://api.z.ai/api/paas/v4/"
288
+ : "https://generativelanguage.googleapis.com/v1beta/openai/",
289
+ };
290
+ }
275
291
  if (input.provider === "github-copilot") {
276
292
  let token = process.env.GH_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim() || "";
277
293
  if (!token) {
@@ -33,7 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.AUTONOMY_BUDGET_DEFAULT_POLICY = void 0;
36
+ exports.DEFAULT_HABIT_PAID_TURNS_PER_DAY = exports.AUTONOMY_BUDGET_DEFAULT_POLICY = void 0;
37
+ exports.normalizeHabitPaidTurnsPerDay = normalizeHabitPaidTurnsPerDay;
38
+ exports.resolveAutonomyBudgetPolicy = resolveAutonomyBudgetPolicy;
37
39
  exports.autonomyBudgetStatePath = autonomyBudgetStatePath;
38
40
  exports.autonomyReceiptsDir = autonomyReceiptsDir;
39
41
  exports.readAutonomyBudgetState = readAutonomyBudgetState;
@@ -56,6 +58,47 @@ exports.AUTONOMY_BUDGET_DEFAULT_POLICY = {
56
58
  stormFailureWindowMs: 10 * 60 * 1000,
57
59
  stormBlockMs: 30 * 60 * 1000,
58
60
  };
61
+ exports.DEFAULT_HABIT_PAID_TURNS_PER_DAY = 4;
62
+ function normalizeHabitPaidTurnsPerDay(agentName, value, configFile) {
63
+ if (value === undefined) {
64
+ if (agentName === "sanctuary") {
65
+ throw new Error(`agent.json at ${configFile} must explicitly set habitPaidTurnsPerDay for Sanctuary.`);
66
+ }
67
+ return exports.DEFAULT_HABIT_PAID_TURNS_PER_DAY;
68
+ }
69
+ if (!Number.isInteger(value) || value < 0 || value > 96) {
70
+ (0, runtime_1.emitNervesEvent)({
71
+ level: "error",
72
+ event: "config_identity.error",
73
+ component: "config/identity",
74
+ message: "agent config has invalid habit paid-turn budget",
75
+ meta: { path: configFile, value },
76
+ });
77
+ throw new Error(`agent.json at ${configFile} habitPaidTurnsPerDay must be an integer from 0 through 96.`);
78
+ }
79
+ return value;
80
+ }
81
+ function resolveAutonomyBudgetPolicy(agentRoot, agentName, basePolicy = exports.AUTONOMY_BUDGET_DEFAULT_POLICY) {
82
+ const configPath = path.join(agentRoot, "agent.json");
83
+ let configured;
84
+ if (fs.existsSync(configPath)) {
85
+ let parsed;
86
+ try {
87
+ parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
88
+ }
89
+ catch (error) {
90
+ throw new Error(`failed to read autonomy budget from ${configPath}: ${String(error)}`);
91
+ }
92
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
93
+ throw new Error(`agent.json at ${configPath} must be an object`);
94
+ }
95
+ configured = parsed.habitPaidTurnsPerDay;
96
+ }
97
+ return {
98
+ ...basePolicy,
99
+ habitPaidTurnsPerDay: normalizeHabitPaidTurnsPerDay(agentName, configured, configPath),
100
+ };
101
+ }
59
102
  function autonomyRoot(agentRoot) {
60
103
  return path.join(agentRoot, "state", "autonomy");
61
104
  }
@@ -50,6 +50,7 @@ class AwaitScheduler {
50
50
  onAwaitExpire;
51
51
  deps;
52
52
  execForVerify;
53
+ verifyJobs;
53
54
  platform;
54
55
  watcher = null;
55
56
  debounceTimer = null;
@@ -65,6 +66,7 @@ class AwaitScheduler {
65
66
  this.onAwaitExpire = options.onAwaitExpire;
66
67
  this.deps = options.deps;
67
68
  this.execForVerify = options.execForVerify;
69
+ this.verifyJobs = options.verifyJobs;
68
70
  this.platform = options.platform ?? process.platform;
69
71
  }
70
72
  start() {
@@ -242,9 +244,11 @@ class AwaitScheduler {
242
244
  }
243
245
  }
244
246
  verifyCronAndCreateFallbacks(jobs) {
245
- if (!this.execForVerify)
247
+ if (!this.execForVerify && !this.verifyJobs)
246
248
  return;
247
- const verifiedLabels = this.verifyCronEntries();
249
+ const verifiedLabels = this.verifyJobs?.(jobs)
250
+ ? new Set(jobs.map((job) => this.platform === "darwin" ? `bot.ouro.${job.agent}.${job.taskId}` : job.taskId.replace(/^await\./u, "")))
251
+ : this.execForVerify ? this.verifyCronEntries() : new Set();
248
252
  for (const job of jobs) {
249
253
  // job.taskId is already namespaced as "await.<name>". The bare name is
250
254
  // what we pass to `--await` and what shows up in the crontab regex
@@ -43,6 +43,7 @@ exports.getMinimaxConfig = getMinimaxConfig;
43
43
  exports.getAnthropicConfig = getAnthropicConfig;
44
44
  exports.getOpenAICodexConfig = getOpenAICodexConfig;
45
45
  exports.getGithubCopilotConfig = getGithubCopilotConfig;
46
+ exports.getOpenAICompatibleConfig = getOpenAICompatibleConfig;
46
47
  exports.getTeamsConfig = getTeamsConfig;
47
48
  exports.getTeamsSecondaryConfig = getTeamsSecondaryConfig;
48
49
  exports.getContextConfig = getContextConfig;
@@ -248,6 +249,13 @@ function getGithubCopilotConfig() {
248
249
  baseUrl: typeof raw.baseUrl === "string" ? raw.baseUrl : "",
249
250
  };
250
251
  }
252
+ function getOpenAICompatibleConfig(provider) {
253
+ const raw = readProviderConfig(provider);
254
+ return {
255
+ apiKey: typeof raw.apiKey === "string" ? raw.apiKey : "",
256
+ baseUrl: typeof raw.baseUrl === "string" ? raw.baseUrl : "",
257
+ };
258
+ }
251
259
  function getTeamsConfig() {
252
260
  const config = loadConfig();
253
261
  return { ...config.teams };