@ouro.bot/cli 0.1.0-alpha.955 → 0.1.0-alpha.957

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.
@@ -0,0 +1,429 @@
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.GATEWAY_MAX_REQUEST_BYTES = exports.GATEWAY_DEFAULT_COOLDOWN_MS = exports.GATEWAY_DEFAULT_UPSTREAM = exports.GATEWAY_DEFAULT_PORT = exports.GATEWAY_DEFAULT_HOST = void 0;
37
+ exports.isCreditExhaustedBody = isCreditExhaustedBody;
38
+ exports.creditGatewayOptionsFromCli = creditGatewayOptionsFromCli;
39
+ exports.startCreditGateway = startCreditGateway;
40
+ exports.waitForShutdownSignal = waitForShutdownSignal;
41
+ exports.runCreditGatewayCommand = runCreditGatewayCommand;
42
+ const http = __importStar(require("node:http"));
43
+ const https = __importStar(require("node:https"));
44
+ const path = __importStar(require("node:path"));
45
+ const zlib = __importStar(require("node:zlib"));
46
+ const node_crypto_1 = require("node:crypto");
47
+ const node_stream_1 = require("node:stream");
48
+ const runtime_1 = require("../nerves/runtime");
49
+ const keys_1 = require("./keys");
50
+ exports.GATEWAY_DEFAULT_HOST = "127.0.0.1";
51
+ exports.GATEWAY_DEFAULT_PORT = 8787;
52
+ exports.GATEWAY_DEFAULT_UPSTREAM = "https://api.anthropic.com";
53
+ exports.GATEWAY_DEFAULT_COOLDOWN_MS = 60 * 60 * 1000;
54
+ exports.GATEWAY_MAX_REQUEST_BYTES = 32 * 1024 * 1024;
55
+ const CREDIT_LOW_PHRASE = "credit balance is too low";
56
+ const CREDIT_CHECK_STATUSES = new Set([400, 402, 403]);
57
+ const HOP_BY_HOP_HEADERS = new Set([
58
+ "connection",
59
+ "keep-alive",
60
+ "proxy-authenticate",
61
+ "proxy-authorization",
62
+ "proxy-connection",
63
+ "te",
64
+ "trailer",
65
+ "transfer-encoding",
66
+ "upgrade",
67
+ ]);
68
+ class RequestTooLargeError extends Error {
69
+ }
70
+ function anthropicError(type, message) {
71
+ return JSON.stringify({ type: "error", error: { type, message } });
72
+ }
73
+ /* v8 ignore start -- defensive: Node and the HTTP stack always throw Error instances @preserve */
74
+ function errorMessage(error) {
75
+ return error instanceof Error ? error.message : String(error);
76
+ }
77
+ /* v8 ignore stop */
78
+ function digest(value) {
79
+ return (0, node_crypto_1.createHash)("sha256").update(value).digest();
80
+ }
81
+ /** Compares digests so the comparison takes the same time whatever the presented key's length. */
82
+ function matchesMasterKey(presented, masterDigest) {
83
+ if (!presented)
84
+ return false;
85
+ return (0, node_crypto_1.timingSafeEqual)(digest(presented), masterDigest);
86
+ }
87
+ function presentedKey(req) {
88
+ const apiKey = req.headers["x-api-key"];
89
+ if (typeof apiKey === "string" && apiKey.trim())
90
+ return apiKey.trim();
91
+ const bearer = /^Bearer\s+(.+)$/i.exec(req.headers.authorization ?? "");
92
+ return bearer ? bearer[1].trim() : undefined;
93
+ }
94
+ function readRequestBody(req, maxBytes) {
95
+ return new Promise((resolve, reject) => {
96
+ const chunks = [];
97
+ let size = 0;
98
+ req.on("data", (chunk) => {
99
+ size += chunk.length;
100
+ if (size > maxBytes) {
101
+ reject(new RequestTooLargeError());
102
+ req.pause();
103
+ return;
104
+ }
105
+ chunks.push(chunk);
106
+ });
107
+ req.on("end", () => resolve(Buffer.concat(chunks)));
108
+ req.on("error", reject);
109
+ });
110
+ }
111
+ function readAll(stream) {
112
+ return new Promise((resolve, reject) => {
113
+ const chunks = [];
114
+ stream.on("data", (chunk) => chunks.push(chunk));
115
+ stream.on("end", () => resolve(Buffer.concat(chunks)));
116
+ stream.on("error", reject);
117
+ });
118
+ }
119
+ function forwardedRequestHeaders(req, upstreamKey, bodyLength) {
120
+ const headers = {};
121
+ for (const [name, value] of Object.entries(req.headers)) {
122
+ if (HOP_BY_HOP_HEADERS.has(name) || name === "host" || name === "content-length" || name === "x-api-key" || name === "authorization")
123
+ continue;
124
+ headers[name] = value;
125
+ }
126
+ headers["x-api-key"] = upstreamKey;
127
+ if (bodyLength > 0)
128
+ headers["content-length"] = String(bodyLength);
129
+ return headers;
130
+ }
131
+ function responseHeaders(headers) {
132
+ const result = {};
133
+ for (const [name, value] of Object.entries(headers)) {
134
+ if (HOP_BY_HOP_HEADERS.has(name))
135
+ continue;
136
+ result[name] = value;
137
+ }
138
+ return result;
139
+ }
140
+ /** Decodes a compressed body for inspection only; the client still gets the original bytes. */
141
+ function decodeForInspection(body, encoding) {
142
+ try {
143
+ const normalized = (encoding ?? "").trim().toLowerCase();
144
+ if (normalized === "gzip" || normalized === "x-gzip")
145
+ return zlib.gunzipSync(body).toString("utf8");
146
+ if (normalized === "deflate")
147
+ return zlib.inflateSync(body).toString("utf8");
148
+ if (normalized === "br")
149
+ return zlib.brotliDecompressSync(body).toString("utf8");
150
+ return body.toString("utf8");
151
+ }
152
+ catch {
153
+ return body.toString("utf8");
154
+ }
155
+ }
156
+ function isCreditExhaustedBody(body, encoding) {
157
+ return decodeForInspection(body, encoding).toLowerCase().includes(CREDIT_LOW_PHRASE);
158
+ }
159
+ function upstreamUrl(base, requestUrl) {
160
+ const prefix = base.pathname.replace(/\/+$/, "");
161
+ return new URL(`${prefix}${requestUrl}`, base.origin);
162
+ }
163
+ function sendUpstream(target, method, headers, body) {
164
+ return new Promise((resolve, reject) => {
165
+ const transport = target.protocol === "https:" ? https : http;
166
+ const request = transport.request(target, { method, headers }, (response) => resolve({ response, request }));
167
+ request.on("error", reject);
168
+ request.end(body.length > 0 ? body : undefined);
169
+ });
170
+ }
171
+ function writeBuffered(res, buffered) {
172
+ res.writeHead(buffered.status, { ...buffered.headers, "content-length": String(buffered.body.length) });
173
+ res.end(buffered.body);
174
+ }
175
+ function writeJsonError(res, status, type, message) {
176
+ const body = Buffer.from(anthropicError(type, message));
177
+ writeBuffered(res, { status, headers: { "content-type": "application/json" }, body });
178
+ }
179
+ /** Maps `ouro gateway serve` flags to gateway options. */
180
+ function creditGatewayOptionsFromCli(command) {
181
+ return {
182
+ keysDir: command.keysDir,
183
+ ...(command.statePath ? { statePath: command.statePath } : {}),
184
+ ...(command.host ? { host: command.host } : {}),
185
+ ...(command.port !== undefined ? { port: command.port } : {}),
186
+ ...(command.upstream ? { upstream: command.upstream } : {}),
187
+ ...(command.cooldownMinutes !== undefined ? { cooldownMs: command.cooldownMinutes * 60 * 1000 } : {}),
188
+ };
189
+ }
190
+ /* v8 ignore start -- the default log writes to the real stderr; tests inject log @preserve */
191
+ function defaultLog(line) {
192
+ process.stderr.write(`${line}\n`);
193
+ }
194
+ /* v8 ignore stop */
195
+ /**
196
+ * Starts the credit gateway: a byte-passthrough proxy to the Anthropic API that accepts one master key
197
+ * and spends the upstream keys in a fixed order, moving to the next key only when the current one
198
+ * reports that its credit balance is too low.
199
+ */
200
+ async function startCreditGateway(options) {
201
+ const keys = (0, keys_1.loadGatewayKeys)(options.keysDir);
202
+ const statePath = options.statePath ?? path.join(options.keysDir, "gateway-state.json");
203
+ const state = (0, keys_1.loadGatewayState)(statePath);
204
+ const host = options.host ?? exports.GATEWAY_DEFAULT_HOST;
205
+ const upstreamBase = new URL(options.upstream ?? exports.GATEWAY_DEFAULT_UPSTREAM);
206
+ const cooldownMs = options.cooldownMs ?? exports.GATEWAY_DEFAULT_COOLDOWN_MS;
207
+ const maxRequestBytes = options.maxRequestBytes ?? exports.GATEWAY_MAX_REQUEST_BYTES;
208
+ const now = options.now ?? Date.now;
209
+ const log = options.log ?? defaultLog;
210
+ const masterDigest = digest(keys.masterKey);
211
+ function persist() {
212
+ try {
213
+ (0, keys_1.saveGatewayState)(statePath, state);
214
+ }
215
+ catch (error) {
216
+ (0, runtime_1.emitNervesEvent)({
217
+ level: "warn",
218
+ component: "gateway",
219
+ event: "gateway.state_save_error",
220
+ message: "credit gateway could not save its state",
221
+ meta: { statePath, error: errorMessage(error) },
222
+ });
223
+ }
224
+ }
225
+ /** Returns the skip-until time for a label, or undefined when the key is usable. Expired entries are dropped. */
226
+ function skipUntil(label) {
227
+ const until = state.exhausted[label];
228
+ if (!until)
229
+ return undefined;
230
+ if (Date.parse(until) > now())
231
+ return until;
232
+ delete state.exhausted[label];
233
+ persist();
234
+ return undefined;
235
+ }
236
+ function markExhausted(entry) {
237
+ state.exhausted[entry.label] = new Date(now() + cooldownMs).toISOString();
238
+ persist();
239
+ log(`gateway: ${entry.label} out of credits; moving on`);
240
+ (0, runtime_1.emitNervesEvent)({
241
+ level: "warn",
242
+ component: "gateway",
243
+ event: "gateway.key_exhausted",
244
+ message: "credit gateway key is out of credits",
245
+ meta: { label: entry.label, skipUntil: state.exhausted[entry.label] },
246
+ });
247
+ }
248
+ function logRequest(label, method, pathname, status, startedAt) {
249
+ const ms = now() - startedAt;
250
+ log(`gateway: ${label} ${method} ${pathname} ${status} ${ms}ms`);
251
+ (0, runtime_1.emitNervesEvent)({
252
+ component: "gateway",
253
+ event: "gateway.request",
254
+ message: "credit gateway request",
255
+ meta: { label, method, path: pathname, status, ms },
256
+ });
257
+ }
258
+ async function handle(req, res) {
259
+ const startedAt = now();
260
+ /* v8 ignore start -- Node always supplies url and method for accepted HTTP requests @preserve */
261
+ const requestUrl = req.url ?? "/";
262
+ const method = req.method ?? "GET";
263
+ /* v8 ignore stop */
264
+ const pathname = new URL(requestUrl, "http://gateway.local").pathname;
265
+ if (method === "GET" && pathname === "/health") {
266
+ const body = JSON.stringify({
267
+ ok: true,
268
+ keys: keys.upstream.map((entry) => {
269
+ const until = skipUntil(entry.label);
270
+ return until ? { label: entry.label, available: false, skipUntil: until } : { label: entry.label, available: true };
271
+ }),
272
+ });
273
+ writeBuffered(res, { status: 200, headers: { "content-type": "application/json" }, body: Buffer.from(body) });
274
+ logRequest("-", method, pathname, 200, startedAt);
275
+ return;
276
+ }
277
+ if (!matchesMasterKey(presentedKey(req), masterDigest)) {
278
+ writeJsonError(res, 401, "authentication_error", "invalid x-api-key");
279
+ logRequest("-", method, pathname, 401, startedAt);
280
+ return;
281
+ }
282
+ const target = upstreamUrl(upstreamBase, requestUrl);
283
+ if (target.origin !== upstreamBase.origin) {
284
+ writeJsonError(res, 400, "invalid_request_error", "invalid request path");
285
+ logRequest("-", method, pathname, 400, startedAt);
286
+ return;
287
+ }
288
+ let body;
289
+ try {
290
+ body = await readRequestBody(req, maxRequestBytes);
291
+ }
292
+ catch (error) {
293
+ /* v8 ignore start -- the client went away while sending its body; there is no one to answer @preserve */
294
+ if (!(error instanceof RequestTooLargeError)) {
295
+ res.destroy();
296
+ return;
297
+ }
298
+ /* v8 ignore stop */
299
+ res.setHeader("connection", "close");
300
+ writeJsonError(res, 413, "request_too_large", `request body exceeds ${maxRequestBytes} bytes`);
301
+ logRequest("-", method, pathname, 413, startedAt);
302
+ return;
303
+ }
304
+ let lastCredit;
305
+ let lastCreditLabel = "-";
306
+ for (const entry of keys.upstream) {
307
+ if (skipUntil(entry.label))
308
+ continue;
309
+ let upstream;
310
+ try {
311
+ upstream = await sendUpstream(target, method, forwardedRequestHeaders(req, entry.key, body.length), body);
312
+ }
313
+ catch (error) {
314
+ (0, runtime_1.emitNervesEvent)({
315
+ level: "error",
316
+ component: "gateway",
317
+ event: "gateway.upstream_error",
318
+ message: "credit gateway could not reach the upstream API",
319
+ meta: { label: entry.label, error: errorMessage(error) },
320
+ });
321
+ writeJsonError(res, 502, "api_error", "credit gateway could not reach the upstream API");
322
+ logRequest(entry.label, method, pathname, 502, startedAt);
323
+ return;
324
+ }
325
+ /* v8 ignore next -- Node always sets statusCode on an upstream response @preserve */
326
+ const status = upstream.response.statusCode ?? 502;
327
+ if (CREDIT_CHECK_STATUSES.has(status)) {
328
+ const buffered = {
329
+ status,
330
+ headers: responseHeaders(upstream.response.headers),
331
+ body: await readAll(upstream.response),
332
+ };
333
+ if (isCreditExhaustedBody(buffered.body, upstream.response.headers["content-encoding"])) {
334
+ markExhausted(entry);
335
+ lastCredit = buffered;
336
+ lastCreditLabel = entry.label;
337
+ continue;
338
+ }
339
+ writeBuffered(res, buffered);
340
+ logRequest(entry.label, method, pathname, status, startedAt);
341
+ return;
342
+ }
343
+ res.writeHead(status, responseHeaders(upstream.response.headers));
344
+ res.flushHeaders();
345
+ (0, node_stream_1.pipeline)(upstream.response, res, (error) => {
346
+ if (error)
347
+ upstream.request.destroy();
348
+ logRequest(entry.label, method, pathname, status, startedAt);
349
+ });
350
+ return;
351
+ }
352
+ if (lastCredit) {
353
+ writeBuffered(res, { ...lastCredit, status: 400 });
354
+ }
355
+ else {
356
+ writeJsonError(res, 400, "invalid_request_error", "Your credit balance is too low on every gateway key");
357
+ }
358
+ logRequest(lastCreditLabel, method, pathname, 400, startedAt);
359
+ }
360
+ const server = http.createServer((req, res) => {
361
+ handle(req, res).catch((error) => {
362
+ (0, runtime_1.emitNervesEvent)({
363
+ level: "error",
364
+ component: "gateway",
365
+ event: "gateway.request_error",
366
+ message: "credit gateway request failed",
367
+ meta: { error: errorMessage(error) },
368
+ });
369
+ /* v8 ignore next 4 -- failures after the headers went out are reported by the stream pipeline instead @preserve */
370
+ if (res.headersSent) {
371
+ res.destroy();
372
+ return;
373
+ }
374
+ writeJsonError(res, 502, "api_error", "credit gateway request failed");
375
+ });
376
+ });
377
+ await new Promise((resolve, reject) => {
378
+ server.once("error", reject);
379
+ /* v8 ignore next -- tests always bind port 0; the default is for production @preserve */
380
+ const port = options.port ?? exports.GATEWAY_DEFAULT_PORT;
381
+ server.listen(port, host, () => {
382
+ server.off("error", reject);
383
+ resolve();
384
+ });
385
+ });
386
+ const address = server.address();
387
+ const url = `http://${host}:${address.port}`;
388
+ (0, runtime_1.emitNervesEvent)({
389
+ component: "gateway",
390
+ event: "gateway.server_started",
391
+ message: "credit gateway started",
392
+ meta: { url, upstream: upstreamBase.origin, labels: keys.upstream.map((entry) => entry.label), statePath },
393
+ });
394
+ return {
395
+ server,
396
+ url,
397
+ port: address.port,
398
+ close: () => new Promise((resolve) => {
399
+ server.closeAllConnections();
400
+ server.close(() => {
401
+ (0, runtime_1.emitNervesEvent)({
402
+ component: "gateway",
403
+ event: "gateway.server_stopped",
404
+ message: "credit gateway stopped",
405
+ meta: { url },
406
+ });
407
+ resolve();
408
+ });
409
+ }),
410
+ };
411
+ }
412
+ /* v8 ignore start -- waits for real process signals; tests inject waitForStop @preserve */
413
+ function waitForShutdownSignal() {
414
+ return new Promise((resolve) => {
415
+ process.once("SIGINT", () => resolve());
416
+ process.once("SIGTERM", () => resolve());
417
+ });
418
+ }
419
+ /* v8 ignore stop */
420
+ /** Runs `ouro gateway serve` in the foreground until `waitForStop` resolves. */
421
+ async function runCreditGatewayCommand(command, deps) {
422
+ const handle = await startCreditGateway(creditGatewayOptionsFromCli(command));
423
+ const message = `credit gateway listening on ${handle.url}\nhealth: ${handle.url}/health`;
424
+ deps.writeStdout(message);
425
+ /* v8 ignore next -- production waits for SIGINT/SIGTERM; tests inject waitForStop @preserve */
426
+ await (deps.waitForStop ?? waitForShutdownSignal)();
427
+ await handle.close();
428
+ return message;
429
+ }
@@ -361,6 +361,16 @@ async function collectRuntimeAuthCredentials(input, deps) {
361
361
  }
362
362
  return credentials;
363
363
  }
364
+ if (input.provider === "anthropic" && input.apiKey) {
365
+ (0, runtime_1.emitNervesEvent)({
366
+ component: "daemon",
367
+ event: "daemon.auth_anthropic_api_key",
368
+ message: "storing anthropic API key for runtime auth",
369
+ meta: { agentName: input.agentName, hasBaseUrl: !!input.baseUrl },
370
+ });
371
+ writeAuthProgress(input, "using the supplied anthropic API key...");
372
+ return { apiKey: input.apiKey.trim(), ...(input.baseUrl ? { baseUrl: input.baseUrl } : {}) };
373
+ }
364
374
  if (input.provider === "anthropic") {
365
375
  (0, runtime_1.emitNervesEvent)({
366
376
  component: "daemon",
@@ -414,12 +424,16 @@ async function collectRuntimeAuthCredentials(input, deps) {
414
424
  }
415
425
  return creds;
416
426
  }
427
+ /** True when the record holds `required` or one of the provider's alternative field sets in full. */
428
+ function hasRequiredProviderCredentials(provider, record) {
429
+ const requirements = identity_1.PROVIDER_CREDENTIALS[provider];
430
+ return [requirements.required, ...(requirements.alternativeRequired ?? [])].some((fields) => fields.every((key) => !!record[key]));
431
+ }
417
432
  async function resolveHatchCredentials(input) {
418
433
  const credentials = { ...(input.credentials ?? {}) };
419
434
  // If all required fields are already provided, return as-is
420
435
  const cred = credentials;
421
- const missing = identity_1.PROVIDER_CREDENTIALS[input.provider].required.some((key) => !cred[key]);
422
- if (!missing)
436
+ if (hasRequiredProviderCredentials(input.provider, cred))
423
437
  return credentials;
424
438
  // Try the full auth flow (wraps collectRuntimeAuthCredentials + writes secrets)
425
439
  if (input.runAuthFlow) {
@@ -431,7 +445,7 @@ async function resolveHatchCredentials(input) {
431
445
  });
432
446
  Object.assign(credentials, result.credentials);
433
447
  /* v8 ignore next 3 -- branch: auth flow always fills all required fields in production @preserve */
434
- if (!identity_1.PROVIDER_CREDENTIALS[input.provider].required.some((key) => !credentials[key])) {
448
+ if (hasRequiredProviderCredentials(input.provider, credentials)) {
435
449
  return credentials;
436
450
  }
437
451
  }
@@ -235,7 +235,11 @@ function getMinimaxConfig() {
235
235
  }
236
236
  function getAnthropicConfig() {
237
237
  const raw = readProviderConfig("anthropic");
238
- return { setupToken: typeof raw.setupToken === "string" ? raw.setupToken : "" };
238
+ return {
239
+ setupToken: typeof raw.setupToken === "string" ? raw.setupToken : "",
240
+ ...(typeof raw.apiKey === "string" && raw.apiKey ? { apiKey: raw.apiKey } : {}),
241
+ ...(typeof raw.baseUrl === "string" && raw.baseUrl ? { baseUrl: raw.baseUrl } : {}),
242
+ };
239
243
  }
240
244
  function getOpenAICodexConfig() {
241
245
  const raw = readProviderConfig("openai-codex");
@@ -285,6 +285,7 @@ exports.STEP_BUDGET_EXTENSION = 4;
285
285
  /** Hard ceiling on the per-turn provider-call budget, however much progress is made. */
286
286
  exports.MAX_PROVIDER_ITERATIONS_CEILING = 24;
287
287
  const STEP_BUDGET_REPORT_INSTRUCTION = "the step budget for this turn is used up. do not call any more work tools; none of the tool calls in your last message were executed. call settle now with a short progress report: what is done (with evidence from tool results above), what is still in flight, and what you will do next. say you can continue where you left off when the conversation resumes.";
288
+ const STEP_BUDGET_REST_INSTRUCTION = "the step budget for this turn is used up. do not call any more work tools; none of the tool calls in your last message were executed. call rest now with a short note: what is done, what is still open, and what to pick up next time.";
288
289
  function continuationCallbacks(callbacks, text) {
289
290
  const noop = () => undefined;
290
291
  return {
@@ -1208,6 +1209,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1208
1209
  let stepBudget = exports.MAX_PROVIDER_ITERATIONS;
1209
1210
  let lastIterationMadeProgress = false;
1210
1211
  let stepReportIteration = null;
1212
+ // The one tool a turn may call after its step budget is used up: settle for conversations, rest for private turns.
1213
+ let stepReportTool = "settle";
1214
+ // A pre-dispatch contract check may reject the same call twice in a turn; after that it is advisory, so a check no call can satisfy never traps the turn.
1215
+ const requiredDispatchRejectionCounts = new Map();
1211
1216
  const budgetExhausted = () => {
1212
1217
  if (providerIterations < stepBudget)
1213
1218
  return false;
@@ -1444,7 +1449,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1444
1449
  .filter((name) => candidateToolNames.has(name)));
1445
1450
  const forcingHistoricalEffect = forcedHistoricalToolNames.size > 0;
1446
1451
  const activeTools = stepReportIteration !== null
1447
- ? candidateActiveTools.filter((tool) => tool.function.name === "settle")
1452
+ ? candidateActiveTools.filter((tool) => tool.function.name === stepReportTool)
1448
1453
  : forcingHistoricalEffect
1449
1454
  ? candidateActiveTools.filter((tool) => forcedHistoricalToolNames.has(tool.function.name))
1450
1455
  : candidateActiveTools;
@@ -1605,8 +1610,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1605
1610
  nextAttemptControls = [];
1606
1611
  providerIterations += 1;
1607
1612
  if (stepReportIteration !== null) {
1608
- // The progress-report call is the last one: it must settle, or the turn errors.
1609
- const settledReport = result.toolCalls.length === 1 && result.toolCalls[0].name === "settle";
1613
+ // The progress-report call is the last one: it must settle (or rest), or the turn errors.
1614
+ const settledReport = result.toolCalls.length === 1 && result.toolCalls[0].name === stepReportTool;
1610
1615
  if (providerIterations > stepReportIteration || (result.toolCalls.length > 0 && !settledReport)) {
1611
1616
  throw new Error(`provider iteration limit exhausted at response ${providerIterations} before the progress report settled`);
1612
1617
  }
@@ -1676,20 +1681,22 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1676
1681
  if (thinkingItems.length > 0) {
1677
1682
  msg._thinking_blocks = thinkingItems;
1678
1683
  }
1679
- const settlingNow = result.toolCalls.length === 1 && result.toolCalls[0].name === "settle";
1684
+ const settlingNow = result.toolCalls.length === 1 && (result.toolCalls[0].name === "settle" || result.toolCalls[0].name === "rest");
1680
1685
  if (stepReportIteration === null && result.toolCalls.length > 0 && !settlingNow && budgetExhausted()) {
1681
- if (!candidateToolNames.has("settle")) {
1686
+ const wrapUpTool = candidateToolNames.has("settle") ? "settle" : candidateToolNames.has("rest") ? "rest" : null;
1687
+ if (!wrapUpTool) {
1682
1688
  throw new Error(`provider iteration limit exhausted at response ${stepBudget} before tool execution`);
1683
1689
  }
1684
- // Do not error into the reply: give the model one settle-only call to report progress.
1690
+ // Do not error into the reply: give the model one settle-only (or rest-only) call to report progress.
1685
1691
  streamCallbackBuffer?.discard();
1686
1692
  stepReportIteration = providerIterations + 1;
1693
+ stepReportTool = wrapUpTool;
1687
1694
  rejectAttempt(msg, [
1688
1695
  ...result.toolCalls.map((tc) => ({ role: "tool", tool_call_id: tc.id, content: "not executed: the step budget for this turn is used up." })),
1689
- { role: "user", content: STEP_BUDGET_REPORT_INSTRUCTION },
1696
+ { role: "user", content: wrapUpTool === "settle" ? STEP_BUDGET_REPORT_INSTRUCTION : STEP_BUDGET_REST_INSTRUCTION },
1690
1697
  ]);
1691
1698
  callbacks.onClearText?.();
1692
- (0, runtime_1.emitNervesEvent)({ level: "warn", component: "engine", event: "engine.step_budget_exhausted", message: "step budget used up; requesting a settle-only progress report", meta: { stepBudget, providerIterations } });
1699
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "engine", event: "engine.step_budget_exhausted", message: `step budget used up; requesting a ${wrapUpTool}-only progress report`, meta: { stepBudget, providerIterations } });
1693
1700
  continue;
1694
1701
  }
1695
1702
  if (result.toolCalls.length > 0)
@@ -2166,14 +2173,16 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2166
2173
  const restArgs = validatedCallArguments.get(result.toolCalls[0]);
2167
2174
  callbacks.onToolStart("rest", restArgs);
2168
2175
  // Attention queue gate: reject rest if items remain
2176
+ // A budget wrap-up rest is final: held work and fresh messages wait for the next turn.
2177
+ const wrappingUp = stepReportIteration !== null;
2169
2178
  const attentionQueue = augmentedToolContext?.delegatedOrigins;
2170
- if (attentionQueue && attentionQueue.length > 0) {
2179
+ if (!wrappingUp && attentionQueue && attentionQueue.length > 0) {
2171
2180
  callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs, toolSelection), false);
2172
2181
  const gateMessage = "current held-work frame still has unsurfaced items — return each listed item with surface(delegationId=...) before you rest. Older transcript claims are historical; only the current held-work frame is the gate.";
2173
2182
  rejectToolBatch(msg, [gateMessage]);
2174
2183
  continue;
2175
2184
  }
2176
- if (hasFreshPendingWork(options) && !freshWorkGateFired) {
2185
+ if (!wrappingUp && hasFreshPendingWork(options) && !freshWorkGateFired) {
2177
2186
  freshWorkGateFired = true;
2178
2187
  callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs, toolSelection), false);
2179
2188
  const gateMessage = "fresh work arrived for me this turn — inspect the pending messages above and take the next concrete action before you rest.";
@@ -2206,8 +2215,20 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2206
2215
  for (const entry of validCalls) {
2207
2216
  const requiredArgs = entry.validated.arguments;
2208
2217
  const rejection = options?.requiredToolCalls?.validateToolCallBeforeDispatch?.(entry.call.name, requiredArgs);
2209
- if (rejection)
2210
- requiredDispatchRejections.set(entry.call.id, { name: entry.call.name, args: requiredArgs, message: rejection });
2218
+ if (!rejection)
2219
+ continue;
2220
+ const rejectionKey = `${entry.call.name}\0${rejection}`;
2221
+ const priorRejections = requiredDispatchRejectionCounts.get(rejectionKey) ?? 0;
2222
+ if (priorRejections >= 2) {
2223
+ (0, runtime_1.emitNervesEvent)({
2224
+ level: "warn", component: "engine", event: "engine.required_tool_dispatch_advisory",
2225
+ message: "required tool dependency rejected twice this turn; letting the call through",
2226
+ meta: { toolName: entry.call.name },
2227
+ });
2228
+ continue;
2229
+ }
2230
+ requiredDispatchRejectionCounts.set(rejectionKey, priorRejections + 1);
2231
+ requiredDispatchRejections.set(entry.call.id, { name: entry.call.name, args: requiredArgs, message: rejection });
2211
2232
  }
2212
2233
  if (requiredDispatchRejections.size > 0) {
2213
2234
  streamCallbackBuffer?.discard();
@@ -5648,8 +5648,24 @@ async function executeProviderRefresh(command, deps) {
5648
5648
  deps.writeStdout(message);
5649
5649
  return message;
5650
5650
  }
5651
+ /** Reads an API key from a file (or /dev/stdin) so it never appears in argv. */
5652
+ function readApiKeyFile(filePath) {
5653
+ let raw;
5654
+ try {
5655
+ raw = fs.readFileSync(filePath, "utf8");
5656
+ }
5657
+ catch (error) {
5658
+ /* v8 ignore next -- fs.readFileSync only throws Error objects @preserve */
5659
+ throw new Error(`could not read --api-key-file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
5660
+ }
5661
+ const key = raw.trim();
5662
+ if (!key)
5663
+ throw new Error(`--api-key-file ${filePath} is empty.`);
5664
+ return key;
5665
+ }
5651
5666
  async function executeAuthRun(command, deps) {
5652
5667
  const provider = command.provider ?? (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot).config.humanFacing.provider;
5668
+ const apiKey = command.apiKeyFile ? readApiKeyFile(command.apiKeyFile) : undefined;
5653
5669
  /* v8 ignore next -- tests always inject runAuthFlow; default is for production @preserve */
5654
5670
  const authRunner = deps.runAuthFlow ?? (await Promise.resolve().then(() => __importStar(require("../auth/auth-flow")))).runRuntimeAuthFlow;
5655
5671
  const progress = createHumanCommandProgress(deps, "auth");
@@ -5662,6 +5678,8 @@ async function executeAuthRun(command, deps) {
5662
5678
  promptInput: deps.promptInput,
5663
5679
  promptSecret: deps.promptSecret,
5664
5680
  onProgress: (message) => progress.updateDetail(message),
5681
+ ...(apiKey ? { apiKey } : {}),
5682
+ ...(command.baseUrl ? { baseUrl: command.baseUrl } : {}),
5665
5683
  });
5666
5684
  }
5667
5685
  catch (error) {
@@ -8231,6 +8249,13 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
8231
8249
  if (command.kind === "a2a.card" || command.kind === "a2a.onboard" || command.kind === "a2a.serve" || command.kind === "a2a.escalation" || command.kind === "a2a.delegatedCommands") {
8232
8250
  return executeA2ACommand(command, deps);
8233
8251
  }
8252
+ if (command.kind === "gateway.serve") {
8253
+ const { runCreditGatewayCommand } = await Promise.resolve().then(() => __importStar(require("../../gateway/server")));
8254
+ return runCreditGatewayCommand(command, {
8255
+ writeStdout: deps.writeStdout,
8256
+ ...(deps.waitForShutdownSignal ? { waitForStop: deps.waitForShutdownSignal } : {}),
8257
+ });
8258
+ }
8234
8259
  // ── provider commands (local, no daemon socket needed) ──
8235
8260
  if (command.kind === "provider.use") {
8236
8261
  return executeProviderUse(command, deps);