@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,267 @@
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.SupercronicSupervisor = void 0;
37
+ const node_child_process_1 = require("node:child_process");
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const runtime_1 = require("../../nerves/runtime");
41
+ const NAMESPACE = /^(?:habit|await):[a-z0-9](?:[a-z0-9._-]{0,62})$/u;
42
+ const SAFE_JOB = /^[^\r\n\0]+$/u;
43
+ const RESTART_DELAYS_MS = [1_000, 5_000, 15_000];
44
+ function defaultDurableWrite(filePath, content, mode) {
45
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
46
+ const temporary = `${filePath}.${process.pid}.tmp`;
47
+ const fd = fs.openSync(temporary, "w", mode);
48
+ try {
49
+ fs.writeFileSync(fd, content, "utf8");
50
+ fs.fsyncSync(fd);
51
+ }
52
+ finally {
53
+ fs.closeSync(fd);
54
+ }
55
+ fs.chmodSync(temporary, mode);
56
+ fs.renameSync(temporary, filePath);
57
+ const directory = fs.openSync(path.dirname(filePath), "r");
58
+ try {
59
+ fs.fsyncSync(directory);
60
+ }
61
+ finally {
62
+ fs.closeSync(directory);
63
+ }
64
+ }
65
+ function defaultDeps() {
66
+ return {
67
+ mkdir: (target) => fs.mkdirSync(target, { recursive: true }),
68
+ readFile: (target) => fs.readFileSync(target, "utf8"),
69
+ durableWrite: defaultDurableWrite,
70
+ removeFile: (target) => { try {
71
+ fs.unlinkSync(target);
72
+ }
73
+ catch (error) {
74
+ if (error.code !== "ENOENT")
75
+ throw error;
76
+ } },
77
+ processAlive: (pid) => { try {
78
+ process.kill(pid, 0);
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ } },
84
+ spawn: (binary, args) => (0, node_child_process_1.spawn)(binary, args, { stdio: ["ignore", "inherit", "inherit"] }),
85
+ setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
86
+ clearTimer: (timer) => clearTimeout(timer),
87
+ };
88
+ }
89
+ class SupercronicSupervisor {
90
+ deps;
91
+ binaryPath;
92
+ crontabPath;
93
+ pidPath;
94
+ onFatal;
95
+ manifests = new Map();
96
+ child = null;
97
+ restartTimer = null;
98
+ restartCount = 0;
99
+ stopping = false;
100
+ constructor(options) {
101
+ this.binaryPath = options.binaryPath;
102
+ this.crontabPath = options.crontabPath;
103
+ this.pidPath = options.pidPath ?? `${options.crontabPath}.pid`;
104
+ this.deps = options.deps ?? defaultDeps();
105
+ this.onFatal = options.onFatal ?? ((error) => { throw error; });
106
+ }
107
+ start() {
108
+ if (this.child || this.restartTimer)
109
+ return;
110
+ this.deps.mkdir(path.dirname(this.crontabPath));
111
+ const priorPid = this.readPriorPid();
112
+ if (priorPid !== null && this.deps.processAlive(priorPid))
113
+ throw new Error(`Supercronic is already running with PID ${priorPid}`);
114
+ this.render();
115
+ this.stopping = false;
116
+ this.spawnChild();
117
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.supercronic_state", message: "Supercronic supervisor started", meta: { crontabPath: this.crontabPath } });
118
+ }
119
+ namespace(namespace) {
120
+ if (!NAMESPACE.test(namespace))
121
+ throw new Error(`invalid Supercronic namespace: ${namespace}`);
122
+ return {
123
+ sync: (jobs) => {
124
+ const next = new Map();
125
+ for (const job of jobs) {
126
+ if (!SAFE_JOB.test(job.id) || !SAFE_JOB.test(job.taskId) || !SAFE_JOB.test(job.schedule) || !SAFE_JOB.test(job.command)) {
127
+ throw new Error("invalid Supercronic job");
128
+ }
129
+ if (next.has(job.id))
130
+ throw new Error(`duplicate Supercronic job: ${job.id}`);
131
+ next.set(job.id, structuredClone(job));
132
+ }
133
+ this.manifests.set(namespace, next);
134
+ this.render();
135
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.supercronic_state", message: "Supercronic manifest synchronized", meta: { namespace, jobCount: jobs.length } });
136
+ },
137
+ removeAll: () => {
138
+ this.manifests.delete(namespace);
139
+ this.render();
140
+ },
141
+ list: () => [...(this.manifests.get(namespace)?.values() ?? [])].map((job) => job.taskId).sort(),
142
+ };
143
+ }
144
+ verifyNamespace(namespace, jobs) {
145
+ if (!this.isHealthy())
146
+ return false;
147
+ const manifest = this.manifests.get(namespace);
148
+ if (!manifest || manifest.size !== jobs.length)
149
+ return false;
150
+ for (const job of jobs) {
151
+ const existing = manifest.get(job.id);
152
+ if (!existing || existing.schedule !== job.schedule || existing.command !== job.command)
153
+ return false;
154
+ }
155
+ try {
156
+ return this.deps.readFile(this.crontabPath) === this.renderedContent();
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ }
162
+ verificationOutput() {
163
+ if (!this.isHealthy())
164
+ return "";
165
+ const expected = this.renderedContent();
166
+ try {
167
+ return this.deps.readFile(this.crontabPath) === expected ? expected : "";
168
+ }
169
+ catch {
170
+ return "";
171
+ }
172
+ }
173
+ authenticatedSnapshot(namespace) {
174
+ if (!NAMESPACE.test(namespace))
175
+ throw new Error(`invalid Supercronic namespace: ${namespace}`);
176
+ if (!this.isHealthy() || typeof this.child?.pid !== "number")
177
+ throw new Error("Supercronic supervisor has no healthy child");
178
+ const manifest = this.manifests.get(namespace);
179
+ if (!manifest)
180
+ throw new Error(`Supercronic supervisor has no manifest for ${namespace}`);
181
+ const renderedCrontab = this.verificationOutput();
182
+ if (renderedCrontab.length === 0)
183
+ throw new Error("Supercronic supervisor crontab is not verified");
184
+ return {
185
+ schemaVersion: "supercronic-supervisor-snapshot-v1",
186
+ daemonPid: process.pid,
187
+ childCount: 1,
188
+ childPid: this.child.pid,
189
+ healthy: true,
190
+ binaryPath: this.binaryPath,
191
+ args: ["-split-logs", "-inotify", this.crontabPath],
192
+ crontabPath: this.crontabPath,
193
+ namespace,
194
+ manifest: [...manifest.values()].sort((left, right) => left.id.localeCompare(right.id)).map((job) => structuredClone(job)),
195
+ renderedCrontab,
196
+ };
197
+ }
198
+ isHealthy() {
199
+ return this.child !== null && typeof this.child.pid === "number" && this.deps.processAlive(this.child.pid);
200
+ }
201
+ async stop() {
202
+ this.stopping = true;
203
+ if (this.restartTimer)
204
+ this.deps.clearTimer(this.restartTimer);
205
+ this.restartTimer = null;
206
+ const child = this.child;
207
+ if (!child) {
208
+ this.deps.removeFile(this.pidPath);
209
+ return;
210
+ }
211
+ await new Promise((resolve) => {
212
+ let settled = false;
213
+ const finish = () => { if (settled)
214
+ return; settled = true; resolve(); };
215
+ child.once("exit", finish);
216
+ const timer = this.deps.setTimer(() => { child.kill("SIGKILL"); finish(); }, 10_000);
217
+ child.once("exit", () => this.deps.clearTimer(timer));
218
+ child.kill("SIGTERM");
219
+ });
220
+ this.child = null;
221
+ this.deps.removeFile(this.pidPath);
222
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.supercronic_state", message: "Supercronic supervisor stopped", meta: { crontabPath: this.crontabPath } });
223
+ }
224
+ readPriorPid() {
225
+ try {
226
+ const value = Number(this.deps.readFile(this.pidPath).trim());
227
+ return Number.isSafeInteger(value) && value > 0 ? value : null;
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ }
233
+ renderedContent() {
234
+ const lines = [];
235
+ for (const namespace of [...this.manifests.keys()].sort()) {
236
+ const jobs = [...this.manifests.get(namespace).values()].sort((left, right) => left.id.localeCompare(right.id));
237
+ for (const job of jobs)
238
+ lines.push(`# ouro:${namespace}:${job.id}`, `${job.schedule} ${job.command}`);
239
+ }
240
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
241
+ }
242
+ render() {
243
+ this.deps.durableWrite(this.crontabPath, this.renderedContent(), 0o600);
244
+ }
245
+ spawnChild() {
246
+ const child = this.deps.spawn(this.binaryPath, ["-split-logs", "-inotify", this.crontabPath]);
247
+ if (!child.pid || child.pid <= 0)
248
+ throw new Error("Supercronic did not return a child PID");
249
+ this.child = child;
250
+ this.deps.durableWrite(this.pidPath, `${child.pid}\n`, 0o600);
251
+ child.once("exit", (code, signal) => {
252
+ if (this.child !== child)
253
+ return;
254
+ this.child = null;
255
+ this.deps.removeFile(this.pidPath);
256
+ if (this.stopping)
257
+ return;
258
+ if (this.restartCount >= RESTART_DELAYS_MS.length) {
259
+ this.onFatal(new Error(`Supercronic exited repeatedly (code=${String(code)}, signal=${String(signal)})`));
260
+ return;
261
+ }
262
+ const delay = RESTART_DELAYS_MS[this.restartCount++];
263
+ this.restartTimer = this.deps.setTimer(() => { this.restartTimer = null; this.spawnChild(); }, delay);
264
+ });
265
+ }
266
+ }
267
+ exports.SupercronicSupervisor = SupercronicSupervisor;
@@ -49,6 +49,7 @@ class HabitScheduler {
49
49
  onHabitFire;
50
50
  deps;
51
51
  execForVerify;
52
+ verifyJobs;
52
53
  platform;
53
54
  watcher = null;
54
55
  debounceTimer = null;
@@ -65,6 +66,7 @@ class HabitScheduler {
65
66
  this.onHabitFire = options.onHabitFire;
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() {
@@ -268,11 +270,13 @@ class HabitScheduler {
268
270
  }, intervalMs);
269
271
  }
270
272
  verifyCronAndCreateFallbacks(state) {
271
- if (!this.execForVerify || state.jobs.length === 0)
273
+ if ((!this.execForVerify && !this.verifyJobs) || state.jobs.length === 0)
272
274
  return state;
273
275
  let currentState = state;
274
276
  for (let attempt = 1; attempt <= MAX_CRON_VERIFICATION_ATTEMPTS; attempt += 1) {
275
- const verifiedLabels = this.verifyCronEntries();
277
+ const verifiedLabels = this.verifyJobs?.(currentState.jobs)
278
+ ? new Set(currentState.jobs.map((job) => this.platform === "darwin" ? `bot.ouro.${job.agent}.${job.taskId}` : job.taskId))
279
+ : this.execForVerify ? this.verifyCronEntries() : new Set();
276
280
  const revalidatedState = this.revalidateAndCorrectJobs(currentState);
277
281
  if (revalidatedState === null)
278
282
  return null;
@@ -586,7 +590,7 @@ class HabitScheduler {
586
590
  taskId: habit.name,
587
591
  schedule: cronSchedule,
588
592
  lastRun: habit.lastRun,
589
- command: `${this.deps.ouroPath} poke ${this.agent} --habit ${habit.name} --trigger launchd`,
593
+ command: `${this.deps.ouroPath} poke ${this.agent} --habit ${habit.name} --trigger ${this.platform === "darwin" ? "launchd" : "cron"}`,
590
594
  taskPath: path.join(this.habitsDir, `${habit.name}.md`),
591
595
  });
592
596
  }
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.HARNESS_CANONICAL_REPO_URL = exports.DEFAULT_AGENT_SENSES = exports.LEGACY_VAULT_SERVER_URL_ALIASES = exports.DEFAULT_VAULT_SERVER_URL = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = exports.PROVIDER_CREDENTIALS = void 0;
36
+ exports.HARNESS_CANONICAL_REPO_URL = exports.DEFAULT_AGENT_SENSES = exports.LEGACY_VAULT_SERVER_URL_ALIASES = exports.DEFAULT_VAULT_SERVER_URL = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = exports.PROVIDER_CREDENTIALS = exports.normalizeHabitPaidTurnsPerDay = void 0;
37
37
  exports.normalizeVaultServerUrl = normalizeVaultServerUrl;
38
38
  exports.getVaultServerUrlCandidates = getVaultServerUrlCandidates;
39
39
  exports.defaultStableVaultEmail = defaultStableVaultEmail;
@@ -62,6 +62,9 @@ const os = __importStar(require("os"));
62
62
  const path = __importStar(require("path"));
63
63
  const runtime_1 = require("../nerves/runtime");
64
64
  const migrate_config_1 = require("./migrate-config");
65
+ const autonomy_budget_1 = require("./autonomy-budget");
66
+ var autonomy_budget_2 = require("./autonomy-budget");
67
+ Object.defineProperty(exports, "normalizeHabitPaidTurnsPerDay", { enumerable: true, get: function () { return autonomy_budget_2.normalizeHabitPaidTurnsPerDay; } });
65
68
  /** Single source of truth for per-provider credential field names, env var mappings, and prompt labels. */
66
69
  exports.PROVIDER_CREDENTIALS = {
67
70
  anthropic: { required: ["setupToken"], envVars: { ANTHROPIC_API_KEY: "setupToken" }, promptLabels: { setupToken: "Anthropic setup-token" } },
@@ -69,6 +72,8 @@ exports.PROVIDER_CREDENTIALS = {
69
72
  azure: { required: ["apiKey", "endpoint", "deployment"], envVars: { AZURE_OPENAI_API_KEY: "apiKey", AZURE_OPENAI_KEY: "apiKey", AZURE_OPENAI_ENDPOINT: "endpoint", AZURE_OPENAI_DEPLOYMENT: "deployment" }, promptLabels: { apiKey: "Azure API key", endpoint: "Azure endpoint", deployment: "Azure deployment" } },
70
73
  minimax: { required: ["apiKey"], envVars: { MINIMAX_API_KEY: "apiKey" }, promptLabels: { apiKey: "MiniMax API key" } },
71
74
  "github-copilot": { required: ["githubToken", "baseUrl"], envVars: { GH_TOKEN: "githubToken", GITHUB_TOKEN: "githubToken" }, promptLabels: { githubToken: "GitHub token" } },
75
+ "openai-compatible": { required: ["apiKey", "baseUrl"], envVars: {}, promptLabels: { apiKey: "Z.ai API key", baseUrl: "Z.ai canonical base URL" } },
76
+ "openai-compatible-gemini": { required: ["apiKey", "baseUrl"], envVars: {}, promptLabels: { apiKey: "Gemini API key", baseUrl: "Gemini canonical base URL" } },
72
77
  };
73
78
  exports.DEFAULT_AGENT_CONTEXT = {
74
79
  maxTokens: 80000,
@@ -138,6 +143,7 @@ exports.DEFAULT_AGENT_SENSES = {
138
143
  mail: { enabled: false },
139
144
  voice: { enabled: false },
140
145
  a2a: { enabled: false },
146
+ telegram: { enabled: false },
141
147
  workbench: { enabled: false },
142
148
  };
143
149
  function normalizeSenses(value, configFile) {
@@ -148,6 +154,7 @@ function normalizeSenses(value, configFile) {
148
154
  mail: { ...exports.DEFAULT_AGENT_SENSES.mail },
149
155
  voice: { ...exports.DEFAULT_AGENT_SENSES.voice },
150
156
  a2a: { ...exports.DEFAULT_AGENT_SENSES.a2a },
157
+ telegram: { ...exports.DEFAULT_AGENT_SENSES.telegram },
151
158
  workbench: { ...exports.DEFAULT_AGENT_SENSES.workbench },
152
159
  };
153
160
  if (value === undefined) {
@@ -164,7 +171,7 @@ function normalizeSenses(value, configFile) {
164
171
  throw new Error(`agent.json at ${configFile} must include senses as an object when present.`);
165
172
  }
166
173
  const raw = value;
167
- const senseNames = ["cli", "teams", "bluebubbles", "mail", "voice", "a2a", "workbench"];
174
+ const senseNames = ["cli", "teams", "bluebubbles", "mail", "voice", "a2a", "telegram", "workbench"];
168
175
  for (const senseName of senseNames) {
169
176
  const rawSense = raw[senseName];
170
177
  if (rawSense === undefined) {
@@ -202,6 +209,7 @@ function buildDefaultAgentTemplate(_agentName) {
202
209
  humanFacing: { provider: "anthropic", model: "claude-opus-4-6" },
203
210
  agentFacing: { provider: "anthropic", model: "claude-opus-4-6" },
204
211
  context: { ...exports.DEFAULT_AGENT_CONTEXT },
212
+ habitPaidTurnsPerDay: autonomy_budget_1.DEFAULT_HABIT_PAID_TURNS_PER_DAY,
205
213
  senses: {
206
214
  cli: { ...exports.DEFAULT_AGENT_SENSES.cli },
207
215
  teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
@@ -209,6 +217,7 @@ function buildDefaultAgentTemplate(_agentName) {
209
217
  mail: { ...exports.DEFAULT_AGENT_SENSES.mail },
210
218
  voice: { ...exports.DEFAULT_AGENT_SENSES.voice },
211
219
  a2a: { ...exports.DEFAULT_AGENT_SENSES.a2a },
220
+ telegram: { ...exports.DEFAULT_AGENT_SENSES.telegram },
212
221
  workbench: { ...exports.DEFAULT_AGENT_SENSES.workbench },
213
222
  },
214
223
  phrases: {
@@ -301,7 +310,7 @@ function getAgentMailroomRoot(agentName) {
301
310
  function getAgentToolsRoot(agentName) {
302
311
  return path.join(getAgentStateRoot(resolveOptionalAgentName(agentName)), "tools");
303
312
  }
304
- const VALID_PROVIDERS = ["azure", "minimax", "anthropic", "openai-codex", "github-copilot"];
313
+ const VALID_PROVIDERS = ["azure", "minimax", "anthropic", "openai-codex", "github-copilot", "openai-compatible", "openai-compatible-gemini"];
305
314
  function isValidProvider(value) {
306
315
  return typeof value === "string" && VALID_PROVIDERS.includes(value);
307
316
  }
@@ -464,6 +473,7 @@ function loadAgentConfig() {
464
473
  enabled,
465
474
  humanFacing,
466
475
  agentFacing,
476
+ habitPaidTurnsPerDay: (0, autonomy_budget_1.normalizeHabitPaidTurnsPerDay)(getAgentName(), parsed.habitPaidTurnsPerDay, configFile),
467
477
  senses: normalizeSenses(parsed.senses, configFile),
468
478
  phrases: parsed.phrases,
469
479
  };
@@ -53,7 +53,7 @@ const crypto = __importStar(require("node:crypto"));
53
53
  const os = __importStar(require("node:os"));
54
54
  const runtime_1 = require("../nerves/runtime");
55
55
  const credential_access_1 = require("../repertoire/credential-access");
56
- const VALID_PROVIDERS = ["azure", "minimax", "anthropic", "openai-codex", "github-copilot"];
56
+ const VALID_PROVIDERS = ["azure", "minimax", "anthropic", "openai-codex", "github-copilot", "openai-compatible", "openai-compatible-gemini"];
57
57
  const VALID_PROVENANCE_SOURCES = ["auth-flow", "manual"];
58
58
  const VAULT_ITEM_PREFIX = "providers/";
59
59
  exports.PROVIDER_CREDENTIAL_POOL_NOT_LOADED_ERROR = "provider credentials have not been loaded from vault";
@@ -78,6 +78,14 @@ const PROVIDER_FIELD_SPLITS = {
78
78
  credentials: ["githubToken"],
79
79
  config: ["baseUrl"],
80
80
  },
81
+ "openai-compatible": {
82
+ credentials: ["apiKey"],
83
+ config: ["baseUrl"],
84
+ },
85
+ "openai-compatible-gemini": {
86
+ credentials: ["apiKey"],
87
+ config: ["baseUrl"],
88
+ },
81
89
  };
82
90
  let cachedPools = new Map();
83
91
  function isRecord(value) {
@@ -14,6 +14,8 @@ exports.DEFAULT_PROVIDER_MODELS = {
14
14
  minimax: "MiniMax-M2.7",
15
15
  "openai-codex": "gpt-5.4",
16
16
  "github-copilot": "claude-sonnet-4.6",
17
+ "openai-compatible": "glm-5.2",
18
+ "openai-compatible-gemini": "gemini-3.6-flash",
17
19
  };
18
20
  const PROVIDER_NAMES = {
19
21
  anthropic: "Anthropic",
@@ -21,6 +23,8 @@ const PROVIDER_NAMES = {
21
23
  minimax: "MiniMax",
22
24
  "openai-codex": "OpenAI Codex",
23
25
  "github-copilot": "GitHub Copilot",
26
+ "openai-compatible": "Z.ai OpenAI-compatible",
27
+ "openai-compatible-gemini": "Gemini OpenAI-compatible",
24
28
  };
25
29
  function normalized(model) {
26
30
  return model.trim().toLowerCase();
@@ -46,6 +50,10 @@ function isModelClearlyIncompatibleWithProvider(provider, model) {
46
50
  return value.startsWith("claude-") || value.startsWith("minimax");
47
51
  case "github-copilot":
48
52
  return false;
53
+ case "openai-compatible":
54
+ return value !== "glm-5.2" && value !== "glm-5-turbo";
55
+ case "openai-compatible-gemini":
56
+ return value !== "gemini-3.6-flash";
49
57
  }
50
58
  }
51
59
  function resolveModelForProviderSelection(provider, currentModel) {
@@ -11,6 +11,7 @@ const azure_1 = require("./providers/azure");
11
11
  const minimax_1 = require("./providers/minimax");
12
12
  const openai_codex_1 = require("./providers/openai-codex");
13
13
  const github_copilot_1 = require("./providers/github-copilot");
14
+ const openai_compatible_1 = require("./providers/openai-compatible");
14
15
  const provider_models_1 = require("./provider-models");
15
16
  const runtime_1 = require("../nerves/runtime");
16
17
  const provider_attempt_1 = require("./provider-attempt");
@@ -177,7 +178,9 @@ function createProviderRuntimeForConfig(provider, config, options = {}) {
177
178
  return (0, openai_codex_1.createOpenAICodexProviderRuntime)(resolvedModel, config);
178
179
  case "github-copilot":
179
180
  return (0, github_copilot_1.createGithubCopilotProviderRuntime)(resolvedModel, config);
180
- /* v8 ignore next 2 -- exhaustive: all providers handled above @preserve */
181
+ case "openai-compatible":
182
+ case "openai-compatible-gemini":
183
+ return (0, openai_compatible_1.createOpenAICompatibleProviderRuntime)(provider, resolvedModel, config);
181
184
  default:
182
185
  throw new Error(`unsupported provider for ping: ${provider}`);
183
186
  }
@@ -233,7 +236,7 @@ async function pingProvider(provider, config, options = {}) {
233
236
  attempts: attempt.attempts,
234
237
  };
235
238
  }
236
- const PINGABLE_PROVIDERS = ["anthropic", "openai-codex", "azure", "minimax", "github-copilot"];
239
+ const PINGABLE_PROVIDERS = ["anthropic", "openai-codex", "azure", "minimax", "github-copilot", "openai-compatible", "openai-compatible-gemini"];
237
240
  async function runHealthInventory(agentName, currentProvider, deps = {}) {
238
241
  /* v8 ignore next -- default: tests inject ping dep @preserve */
239
242
  const ping = deps.ping ?? pingProvider;
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createOpenAICompatibleProviderRuntime = createOpenAICompatibleProviderRuntime;
4
+ const bundle_manifest_1 = require("../../mind/bundle-manifest");
5
+ const runtime_1 = require("../../nerves/runtime");
6
+ const error_classification_1 = require("./error-classification");
7
+ const CANONICAL_BASE_URLS = {
8
+ "openai-compatible": "https://api.z.ai/api/paas/v4/",
9
+ "openai-compatible-gemini": "https://generativelanguage.googleapis.com/v1beta/openai/",
10
+ };
11
+ function canonicalBaseUrl(provider, value) {
12
+ let parsed;
13
+ try {
14
+ parsed = new URL(value);
15
+ }
16
+ catch {
17
+ throw new Error(`${provider} requires its canonical base URL`);
18
+ }
19
+ const normalized = parsed.toString().replace(/\/+$/u, "/");
20
+ const normalizedInput = value.replace(/\/+$/u, "/");
21
+ if (normalizedInput !== CANONICAL_BASE_URLS[provider] || normalized !== CANONICAL_BASE_URLS[provider]) {
22
+ throw new Error(`${provider} requires its canonical base URL ${CANONICAL_BASE_URLS[provider]}`);
23
+ }
24
+ return normalized;
25
+ }
26
+ function safeError(error, apiKey) {
27
+ const redact = (value) => value.split(apiKey).join("[redacted]");
28
+ const message = redact(error instanceof Error ? error.message : String(error));
29
+ const safe = new Error(message);
30
+ if (error instanceof Error) {
31
+ const source = error;
32
+ if (typeof source.status === "number" && Number.isFinite(source.status))
33
+ safe.status = source.status;
34
+ if (typeof source.code === "string")
35
+ safe.code = redact(source.code);
36
+ }
37
+ return safe;
38
+ }
39
+ function requiredString(value, label) {
40
+ if (typeof value !== "string" || value.trim().length === 0)
41
+ throw new Error(`OpenAI-compatible response ${label} is missing`);
42
+ return value;
43
+ }
44
+ function parseUsage(value) {
45
+ if (!value || typeof value !== "object" || Array.isArray(value))
46
+ return undefined;
47
+ const usage = value;
48
+ const input = usage.prompt_tokens;
49
+ const output = usage.completion_tokens;
50
+ const total = usage.total_tokens;
51
+ if (![input, output, total].every((entry) => Number.isSafeInteger(entry) && entry >= 0))
52
+ return undefined;
53
+ const details = usage.completion_tokens_details;
54
+ const reasoning = details && typeof details === "object" && !Array.isArray(details)
55
+ && Number.isSafeInteger(details.reasoning_tokens)
56
+ ? Number(details.reasoning_tokens)
57
+ : 0;
58
+ return { input_tokens: Number(input), output_tokens: Number(output), reasoning_tokens: reasoning, total_tokens: Number(total) };
59
+ }
60
+ function parseCompletion(value, callbacks) {
61
+ if (!value || typeof value !== "object" || Array.isArray(value))
62
+ throw new Error("OpenAI-compatible response envelope is invalid");
63
+ const choices = value.choices;
64
+ if (!Array.isArray(choices) || choices.length !== 1 || !choices[0] || typeof choices[0] !== "object") {
65
+ throw new Error("OpenAI-compatible response must contain exactly one choice");
66
+ }
67
+ const choice = choices[0];
68
+ const message = choice.message;
69
+ if (!message || typeof message !== "object" || Array.isArray(message))
70
+ throw new Error("OpenAI-compatible choice message is invalid");
71
+ const record = message;
72
+ if (Object.prototype.hasOwnProperty.call(record, "function_call")) {
73
+ throw new Error("OpenAI-compatible legacy function_call is unsupported");
74
+ }
75
+ const rawCalls = record.tool_calls;
76
+ const calls = rawCalls === undefined || rawCalls === null ? [] : rawCalls;
77
+ if (!Array.isArray(calls))
78
+ throw new Error("OpenAI-compatible tool_calls must be an array");
79
+ const toolCalls = calls.map((call) => {
80
+ if (!call || typeof call !== "object" || Array.isArray(call))
81
+ throw new Error("OpenAI-compatible tool call is invalid");
82
+ const callRecord = call;
83
+ const fn = callRecord.function;
84
+ if (callRecord.type !== "function" || !fn || typeof fn !== "object" || Array.isArray(fn)) {
85
+ throw new Error("OpenAI-compatible tool call must be a function");
86
+ }
87
+ return {
88
+ id: requiredString(callRecord.id, "tool call id"),
89
+ name: requiredString(fn.name, "tool name"),
90
+ arguments: requiredString(fn.arguments, "tool arguments"),
91
+ };
92
+ });
93
+ const content = typeof record.content === "string" ? record.content : "";
94
+ if (choice.finish_reason === "stop") {
95
+ if (toolCalls.length !== 0 || content.trim().length === 0)
96
+ throw new Error("OpenAI-compatible stop response shape is invalid");
97
+ }
98
+ else if (choice.finish_reason === "tool_calls") {
99
+ if (toolCalls.length < 1 || toolCalls.length > 8 || content.trim().length !== 0) {
100
+ throw new Error("OpenAI-compatible tool_calls response shape is invalid");
101
+ }
102
+ }
103
+ else {
104
+ throw new Error("OpenAI-compatible finish_reason is unsupported");
105
+ }
106
+ callbacks.onModelStreamStart();
107
+ if (content)
108
+ callbacks.onTextChunk(content);
109
+ return {
110
+ content,
111
+ toolCalls,
112
+ outputItems: [],
113
+ usage: parseUsage(value.usage),
114
+ };
115
+ }
116
+ function createOpenAICompatibleProviderRuntime(provider, model, config, deps = {}) {
117
+ const apiKey = config.apiKey?.trim();
118
+ if (!apiKey)
119
+ throw new Error(`provider '${provider}' is selected but apiKey is missing in the agent vault`);
120
+ const baseUrl = canonicalBaseUrl(provider, config.baseUrl);
121
+ const fetchImpl = deps.fetch ?? fetch;
122
+ const packageVersion = deps.packageVersion ?? (0, bundle_manifest_1.getPackageVersion)();
123
+ (0, runtime_1.emitNervesEvent)({
124
+ component: "engine",
125
+ event: "engine.provider_init",
126
+ message: "OpenAI-compatible provider init",
127
+ meta: { provider },
128
+ });
129
+ const request = async (messages, tools, signal, toolChoiceRequired = false) => {
130
+ const body = { model, messages, tools, stream: false };
131
+ if (provider === "openai-compatible")
132
+ body.temperature = 0;
133
+ if (toolChoiceRequired)
134
+ body.tool_choice = "required";
135
+ try {
136
+ const response = await fetchImpl(new URL("chat/completions", baseUrl), {
137
+ method: "POST",
138
+ headers: {
139
+ Authorization: `Bearer ${apiKey}`,
140
+ "Content-Type": "application/json",
141
+ ...(provider === "openai-compatible-gemini"
142
+ ? { "x-goog-api-client": `ouroboros-harness-oai/${packageVersion}` }
143
+ : {}),
144
+ },
145
+ body: JSON.stringify(body),
146
+ signal,
147
+ });
148
+ if (!response.ok)
149
+ throw Object.assign(new Error(`provider request failed with HTTP ${response.status}`), { status: response.status });
150
+ return await response.json();
151
+ }
152
+ catch (error) {
153
+ throw safeError(error, apiKey);
154
+ }
155
+ };
156
+ return {
157
+ id: provider,
158
+ model,
159
+ client: { baseUrl },
160
+ capabilities: new Set(),
161
+ resetTurnState() { },
162
+ appendToolOutput() { },
163
+ async streamTurn(turnRequest) {
164
+ return parseCompletion(await request(turnRequest.messages, turnRequest.activeTools, turnRequest.signal, turnRequest.toolChoiceRequired), turnRequest.callbacks);
165
+ },
166
+ async ping(signal) {
167
+ const callbacks = {
168
+ onModelStreamStart() { },
169
+ onTextChunk() { },
170
+ };
171
+ parseCompletion(await request([{ role: "user", content: "ping" }], [], signal), callbacks);
172
+ },
173
+ classifyError(error) {
174
+ return (0, error_classification_1.classifyHttpError)(error);
175
+ },
176
+ };
177
+ }