@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.
- package/changelog.json +24 -0
- package/deploy/unraid/credit-gateway.md +95 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/mcp/media-mcp.mjs +22 -8
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/gateway/keys.js +117 -0
- package/dist/gateway/server.js +429 -0
- package/dist/heart/auth/auth-flow.js +17 -3
- package/dist/heart/config.js +5 -1
- package/dist/heart/core.js +33 -12
- package/dist/heart/daemon/cli-exec.js +25 -0
- package/dist/heart/daemon/cli-help.js +14 -2
- package/dist/heart/daemon/cli-parse.js +83 -0
- package/dist/heart/external-events/router.js +20 -12
- package/dist/heart/identity.js +1 -1
- package/dist/heart/provider-credentials.js +2 -2
- package/dist/heart/provider-ping.js +2 -1
- package/dist/heart/providers/anthropic.js +65 -7
- package/dist/heart/runtime-credentials.js +2 -1
- package/dist/repertoire/house-sweep.js +10 -8
- package/dist/repertoire/tools-continuity.js +16 -10
- package/dist/senses/private-runtime-worker.js +53 -1
- package/dist/senses/sanctuary-health-acceptance-probe.js +1 -1
- package/dist/senses/sanctuary-health.js +31 -5
- package/dist/senses/telegram-handoff.js +75 -1
- package/dist/senses/telegram.js +35 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -259,8 +259,8 @@ exports.COMMAND_REGISTRY = {
|
|
|
259
259
|
},
|
|
260
260
|
auth: {
|
|
261
261
|
category: "Auth",
|
|
262
|
-
description: "Set up or verify agent credentials",
|
|
263
|
-
usage: "ouro auth [verify] [--agent <name>] [--provider <provider>]",
|
|
262
|
+
description: "Set up or verify agent credentials. For Anthropic API-key mode (for example through a credit gateway), pass --api-key-file <path> (use /dev/stdin to pipe it) and optionally --base-url <url>; the key is never taken from the command line.",
|
|
263
|
+
usage: "ouro auth [verify] [--agent <name>] [--provider <provider>] [--api-key-file <path> [--base-url <url>]]",
|
|
264
264
|
example: "ouro auth",
|
|
265
265
|
subcommands: ["verify"],
|
|
266
266
|
},
|
|
@@ -358,6 +358,13 @@ exports.COMMAND_REGISTRY = {
|
|
|
358
358
|
example: "ouro session list",
|
|
359
359
|
subcommands: ["list"],
|
|
360
360
|
},
|
|
361
|
+
gateway: {
|
|
362
|
+
category: "System",
|
|
363
|
+
description: "Run the credit gateway: one master key for agents, upstream Anthropic API keys spent in a fixed order",
|
|
364
|
+
usage: "ouro gateway serve --keys-dir <dir> [--host <host>] [--port <port>]",
|
|
365
|
+
example: "ouro gateway serve --keys-dir /mnt/user/appdata/credit-gateway/keys",
|
|
366
|
+
subcommands: ["serve"],
|
|
367
|
+
},
|
|
361
368
|
mcp: {
|
|
362
369
|
category: "System",
|
|
363
370
|
description: "Interact with MCP servers",
|
|
@@ -497,6 +504,11 @@ const SUBCOMMAND_HELP = {
|
|
|
497
504
|
usage: "ouro a2a onboard [--agent <name>] (--card-url <url>|--did <did:key> --name <name>) [--trust <level>] [--name <name>]",
|
|
498
505
|
example: "ouro a2a onboard --agent <agent> --card-url https://peer.example/.well-known/agent-card.json --trust friend",
|
|
499
506
|
},
|
|
507
|
+
"gateway serve": {
|
|
508
|
+
description: "Run the credit gateway in the foreground. It accepts the master key from <keys-dir>/master.key and forwards each request to the Anthropic API with the first upstream *.key (by file name) whose credits are not exhausted; a key whose credit balance is too low is skipped for the cooldown (default 60 minutes). GET /health needs no key.",
|
|
509
|
+
usage: "ouro gateway serve --keys-dir <dir> [--host <host>] [--port <port>] [--state <path>] [--upstream <url>] [--cooldown-minutes <n>]",
|
|
510
|
+
example: "ouro gateway serve --keys-dir /mnt/user/appdata/credit-gateway/keys --port 8787",
|
|
511
|
+
},
|
|
500
512
|
"a2a serve": {
|
|
501
513
|
description: "Run this agent's local A2A JSON-RPC sense server",
|
|
502
514
|
usage: "ouro a2a serve [--agent <name>] [--host <host>] [--port <port>] [--base-url <url>] [--path <path>]",
|
|
@@ -254,6 +254,7 @@ function usage() {
|
|
|
254
254
|
" ouro mailbox [--json]",
|
|
255
255
|
" ouro -v|--version",
|
|
256
256
|
" ouro auth [--agent <name>] [--provider <provider>]",
|
|
257
|
+
" ouro auth --agent <name> --provider anthropic --api-key-file <path> [--base-url <url>]",
|
|
257
258
|
" ouro account ensure [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
|
|
258
259
|
" ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|telegram|workbench] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
|
|
259
260
|
" ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>] [--foreground]",
|
|
@@ -303,6 +304,7 @@ function usage() {
|
|
|
303
304
|
" ouro a2a card [--agent <name>] [--base-url <url>] [--json]",
|
|
304
305
|
" ouro a2a onboard [--agent <name>] (--card-url <url>|--did <did:key> --name <name>) [--trust <level>] [--name <name>]",
|
|
305
306
|
" ouro a2a serve [--agent <name>] [--host <host>] [--port <port>] [--base-url <url>] [--path <path>]",
|
|
307
|
+
" ouro gateway serve --keys-dir <dir> [--host <host>] [--port <port>] [--state <path>] [--upstream <url>] [--cooldown-minutes <n>]",
|
|
306
308
|
" ouro a2a identity [--identity-file <path>] [--json]",
|
|
307
309
|
" ouro a2a message --to <card-url> --text <text> [--context <id>] [--delegated] [--identity-file <path>] [--json]",
|
|
308
310
|
" ouro a2a outbox list --to <card-url> [--since <cursor>] [--identity-file <path>] [--json]",
|
|
@@ -834,6 +836,8 @@ function parseAuthCommand(args) {
|
|
|
834
836
|
}
|
|
835
837
|
const { agent, rest } = extractAgentFlag(args);
|
|
836
838
|
let provider;
|
|
839
|
+
let apiKeyFile;
|
|
840
|
+
let baseUrl;
|
|
837
841
|
for (let i = 0; i < rest.length; i += 1) {
|
|
838
842
|
if (rest[i] === "--provider") {
|
|
839
843
|
const value = rest[i + 1];
|
|
@@ -843,11 +847,33 @@ function parseAuthCommand(args) {
|
|
|
843
847
|
i += 1;
|
|
844
848
|
continue;
|
|
845
849
|
}
|
|
850
|
+
// The API key itself is read from a file (or /dev/stdin), never from argv, so it stays out of
|
|
851
|
+
// shell history and process listings.
|
|
852
|
+
if (rest[i] === "--api-key-file" || rest[i] === "--base-url") {
|
|
853
|
+
const value = rest[i + 1];
|
|
854
|
+
if (!value || value.startsWith("--"))
|
|
855
|
+
throw new Error(`${rest[i]} requires a value.`);
|
|
856
|
+
if (rest[i] === "--api-key-file")
|
|
857
|
+
apiKeyFile = value;
|
|
858
|
+
else
|
|
859
|
+
baseUrl = value;
|
|
860
|
+
i += 1;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if ((apiKeyFile || baseUrl) && provider !== "anthropic") {
|
|
865
|
+
throw new Error("--api-key-file and --base-url are only for --provider anthropic (API-key mode).");
|
|
846
866
|
}
|
|
867
|
+
if (baseUrl && !apiKeyFile)
|
|
868
|
+
throw new Error("--base-url requires --api-key-file.");
|
|
869
|
+
if (baseUrl && !/^https?:\/\//.test(baseUrl))
|
|
870
|
+
throw new Error("--base-url must be an http(s) URL.");
|
|
847
871
|
return {
|
|
848
872
|
kind: "auth.run",
|
|
849
873
|
...(agent ? { agent } : {}),
|
|
850
874
|
...(provider ? { provider } : {}),
|
|
875
|
+
...(apiKeyFile ? { apiKeyFile } : {}),
|
|
876
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
851
877
|
};
|
|
852
878
|
}
|
|
853
879
|
function isVaultUnlockStoreKind(value) {
|
|
@@ -1936,6 +1962,61 @@ function parseA2ACommand(args) {
|
|
|
1936
1962
|
}
|
|
1937
1963
|
throw new Error("Usage: ouro a2a card|onboard|serve|message|identity|outbox|escalation|delegated-commands ...");
|
|
1938
1964
|
}
|
|
1965
|
+
const GATEWAY_SERVE_USAGE = "Usage: ouro gateway serve --keys-dir <dir> [--host <host>] [--port <port>] [--state <path>] [--upstream <url>] [--cooldown-minutes <n>]";
|
|
1966
|
+
function parseGatewayCommand(args) {
|
|
1967
|
+
const [sub, ...rest] = args;
|
|
1968
|
+
if (sub !== "serve")
|
|
1969
|
+
throw new Error(GATEWAY_SERVE_USAGE);
|
|
1970
|
+
let keysDir;
|
|
1971
|
+
let host;
|
|
1972
|
+
let port;
|
|
1973
|
+
let statePath;
|
|
1974
|
+
let upstream;
|
|
1975
|
+
let cooldownMinutes;
|
|
1976
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
1977
|
+
const flag = rest[i];
|
|
1978
|
+
const value = rest[i + 1];
|
|
1979
|
+
if (!value || value.startsWith("--"))
|
|
1980
|
+
throw new Error(GATEWAY_SERVE_USAGE);
|
|
1981
|
+
i += 1;
|
|
1982
|
+
if (flag === "--keys-dir")
|
|
1983
|
+
keysDir = value;
|
|
1984
|
+
else if (flag === "--host")
|
|
1985
|
+
host = value;
|
|
1986
|
+
else if (flag === "--state")
|
|
1987
|
+
statePath = value;
|
|
1988
|
+
else if (flag === "--upstream") {
|
|
1989
|
+
if (!/^https?:\/\//.test(value))
|
|
1990
|
+
throw new Error("--upstream must be an http(s) URL");
|
|
1991
|
+
upstream = value;
|
|
1992
|
+
}
|
|
1993
|
+
else if (flag === "--port") {
|
|
1994
|
+
const parsed = Number(value);
|
|
1995
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535)
|
|
1996
|
+
throw new Error("Gateway port must be 0-65535");
|
|
1997
|
+
port = parsed;
|
|
1998
|
+
}
|
|
1999
|
+
else if (flag === "--cooldown-minutes") {
|
|
2000
|
+
const parsed = Number(value);
|
|
2001
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
2002
|
+
throw new Error("--cooldown-minutes must be a positive number");
|
|
2003
|
+
cooldownMinutes = parsed;
|
|
2004
|
+
}
|
|
2005
|
+
else
|
|
2006
|
+
throw new Error(GATEWAY_SERVE_USAGE);
|
|
2007
|
+
}
|
|
2008
|
+
if (!keysDir)
|
|
2009
|
+
throw new Error(GATEWAY_SERVE_USAGE);
|
|
2010
|
+
return {
|
|
2011
|
+
kind: "gateway.serve",
|
|
2012
|
+
keysDir,
|
|
2013
|
+
...(host ? { host } : {}),
|
|
2014
|
+
...(port !== undefined ? { port } : {}),
|
|
2015
|
+
...(statePath ? { statePath } : {}),
|
|
2016
|
+
...(upstream ? { upstream } : {}),
|
|
2017
|
+
...(cooldownMinutes !== undefined ? { cooldownMinutes } : {}),
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
1939
2020
|
function parseConfigCommand(args) {
|
|
1940
2021
|
const { agent, rest: afterAgent } = extractAgentFlag(args);
|
|
1941
2022
|
const { facing, rest: cleaned } = extractFacingFlag(afterAgent);
|
|
@@ -3118,6 +3199,8 @@ function parseOuroCommand(args) {
|
|
|
3118
3199
|
return parseFriendCommand(args.slice(1));
|
|
3119
3200
|
if (head === "a2a")
|
|
3120
3201
|
return parseA2ACommand(args.slice(1));
|
|
3202
|
+
if (head === "gateway")
|
|
3203
|
+
return parseGatewayCommand(args.slice(1));
|
|
3121
3204
|
if (head === "config")
|
|
3122
3205
|
return parseConfigCommand(args.slice(1));
|
|
3123
3206
|
if (head === "mcp")
|
|
@@ -813,8 +813,13 @@ function recordExternalEventInternal(input, options = {}) {
|
|
|
813
813
|
&& existing.recoveryGrant?.generation === existing.generation && existing.failureProvenance !== undefined
|
|
814
814
|
&& Date.parse(now) - Date.parse(existing.failureProvenance.failedAt) >= EXHAUSTED_RECOVERY_REOPEN_MS
|
|
815
815
|
&& Math.floor(Date.parse(now) / 3_600_000) % EXHAUSTED_RECOVERY_SLOTS === exhaustedRecoverySlot(existing);
|
|
816
|
-
|
|
816
|
+
// A recovery is only news to an owner who was told about the incident, or to a turn that asked to watch for it; any other recovery settles here without a model turn.
|
|
817
|
+
const silentRecovery = input.transition === "recovered" && existing !== null
|
|
818
|
+
&& existing.disposition?.decision !== "ask" && existing.disposition?.decision !== "report" && existing.disposition?.nextWake.kind !== "on_recovery";
|
|
819
|
+
const settledRecoveryReopened = existing?.executionState === "handled" && !existing.disposition && existing.transition === "recovered" && input.transition !== "recovered";
|
|
820
|
+
const shouldWake = !quietInitialReceipt && !silentRecovery && (!existing || quietBaselineChanged || settledRecoveryReopened
|
|
817
821
|
|| (existing.disposition ? predicateMatches(existing.disposition, input, revision) : existing.executionState === "dead_letter" && (revision !== existing.observationRevision || exhaustedRecoveryReopens)));
|
|
822
|
+
const settles = quietInitialReceipt || silentRecovery;
|
|
818
823
|
const generation = existing ? existing.generation + (shouldWake ? 1 : 0) : 1;
|
|
819
824
|
const record = {
|
|
820
825
|
schemaVersion: 2,
|
|
@@ -834,17 +839,17 @@ function recordExternalEventInternal(input, options = {}) {
|
|
|
834
839
|
observationRevision: revision,
|
|
835
840
|
observationDigest: digest,
|
|
836
841
|
transition: input.transition ?? (existing ? "unchanged" : "opened"),
|
|
837
|
-
executionState:
|
|
842
|
+
executionState: settles ? "handled" : shouldWake ? "received" : existing.executionState,
|
|
838
843
|
generation,
|
|
839
|
-
attemptCount: shouldWake ||
|
|
840
|
-
claimOwner: shouldWake ||
|
|
841
|
-
claimExpiresAt: shouldWake ||
|
|
842
|
-
nextAttemptAt: shouldWake ||
|
|
843
|
-
lastError: shouldWake ||
|
|
844
|
-
...(!shouldWake && !
|
|
845
|
-
...(!shouldWake && !
|
|
846
|
-
disposition: shouldWake ||
|
|
847
|
-
pendingObservation: shouldWake ||
|
|
844
|
+
attemptCount: shouldWake || settles ? 0 : existing.attemptCount,
|
|
845
|
+
claimOwner: shouldWake || settles ? null : existing.claimOwner,
|
|
846
|
+
claimExpiresAt: shouldWake || settles ? null : existing.claimExpiresAt,
|
|
847
|
+
nextAttemptAt: shouldWake || settles ? null : existing.nextAttemptAt,
|
|
848
|
+
lastError: shouldWake || settles ? null : existing.lastError,
|
|
849
|
+
...(!shouldWake && !settles && existing?.failureProvenance ? { failureProvenance: existing.failureProvenance } : {}),
|
|
850
|
+
...(!shouldWake && !settles && existing?.recoveryGrant ? { recoveryGrant: existing.recoveryGrant } : {}),
|
|
851
|
+
disposition: shouldWake || settles ? null : existing.disposition,
|
|
852
|
+
pendingObservation: shouldWake || settles ? null : existing.pendingObservation,
|
|
848
853
|
dispatchEnabled: options.dispatchEnabled ?? existing?.dispatchEnabled ?? true,
|
|
849
854
|
shouldWake,
|
|
850
855
|
...(persistedRetention ? { retentionSummary: persistedRetention } : {}),
|
|
@@ -1399,7 +1404,10 @@ function settleExternalEventFailureIfOwned(recordPath, input) {
|
|
|
1399
1404
|
|| record.generation !== input.expectedGeneration) {
|
|
1400
1405
|
return { settled: false, record };
|
|
1401
1406
|
}
|
|
1402
|
-
const
|
|
1407
|
+
const next = input.deferUntil
|
|
1408
|
+
? { ...record, executionState: "retry_wait", claimOwner: null, claimExpiresAt: null, nextAttemptAt: input.deferUntil, attemptCount: Math.max(0, record.attemptCount - 1), lastError: input.error.slice(0, 1_000), shouldWake: false }
|
|
1409
|
+
: retryState(record, now, 5, 1_000, input.error, input.failureClass);
|
|
1410
|
+
const settled = commitMutation(recordPath, next, now);
|
|
1403
1411
|
return { settled: true, record: settled };
|
|
1404
1412
|
});
|
|
1405
1413
|
}
|
package/dist/heart/identity.js
CHANGED
|
@@ -69,7 +69,7 @@ var autonomy_budget_2 = require("./autonomy-budget");
|
|
|
69
69
|
Object.defineProperty(exports, "normalizeHabitPaidTurnsPerDay", { enumerable: true, get: function () { return autonomy_budget_2.normalizeHabitPaidTurnsPerDay; } });
|
|
70
70
|
/** Single source of truth for per-provider credential field names, env var mappings, and prompt labels. */
|
|
71
71
|
exports.PROVIDER_CREDENTIALS = {
|
|
72
|
-
anthropic: { required: ["setupToken"], envVars: { ANTHROPIC_API_KEY: "setupToken" }, promptLabels: { setupToken: "Anthropic setup-token" } },
|
|
72
|
+
anthropic: { required: ["setupToken"], alternativeRequired: [["apiKey"]], envVars: { ANTHROPIC_API_KEY: "setupToken" }, promptLabels: { setupToken: "Anthropic setup-token" } },
|
|
73
73
|
"openai-codex": { required: ["oauthAccessToken"], envVars: { OPENAI_API_KEY: "oauthAccessToken" }, promptLabels: { oauthAccessToken: "OpenAI Codex OAuth token" } },
|
|
74
74
|
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" } },
|
|
75
75
|
minimax: { required: ["apiKey"], envVars: { MINIMAX_API_KEY: "apiKey" }, promptLabels: { apiKey: "MiniMax API key" } },
|
|
@@ -59,8 +59,8 @@ const VAULT_ITEM_PREFIX = "providers/";
|
|
|
59
59
|
exports.PROVIDER_CREDENTIAL_POOL_NOT_LOADED_ERROR = "provider credentials have not been loaded from vault";
|
|
60
60
|
const PROVIDER_FIELD_SPLITS = {
|
|
61
61
|
anthropic: {
|
|
62
|
-
credentials: ["setupToken", "refreshToken", "expiresAt"],
|
|
63
|
-
config: [],
|
|
62
|
+
credentials: ["setupToken", "refreshToken", "expiresAt", "apiKey"],
|
|
63
|
+
config: ["baseUrl"],
|
|
64
64
|
},
|
|
65
65
|
"openai-codex": {
|
|
66
66
|
credentials: ["oauthAccessToken", "refreshToken", "expiresAt"],
|
|
@@ -157,7 +157,8 @@ function hasEmptyCredentials(provider, config) {
|
|
|
157
157
|
if (hasManagedIdentity)
|
|
158
158
|
return false;
|
|
159
159
|
}
|
|
160
|
-
|
|
160
|
+
const requirements = identity_1.PROVIDER_CREDENTIALS[provider];
|
|
161
|
+
return ![requirements.required, ...(requirements.alternativeRequired ?? [])].some((fields) => fields.every((key) => !!record[key]));
|
|
161
162
|
}
|
|
162
163
|
function createProviderRuntimeForConfig(provider, config, options = {}) {
|
|
163
164
|
// Use the same provider defaults as auth switch and hatch so verification
|
|
@@ -225,7 +225,16 @@ function mergeAnthropicToolArguments(current, partial) {
|
|
|
225
225
|
}
|
|
226
226
|
return current + partial;
|
|
227
227
|
}
|
|
228
|
+
// An org whose prepaid API credits are used up gets HTTP 400 (or 402) with this phrase. That is a
|
|
229
|
+
// usage limit, not a malformed request, so failover tells the human the right thing.
|
|
230
|
+
const ANTHROPIC_CREDIT_LOW_PHRASE = "credit balance is too low";
|
|
231
|
+
function isAnthropicCreditExhausted(error) {
|
|
232
|
+
const status = error.status;
|
|
233
|
+
return (status === 400 || status === 402) && error.message.toLowerCase().includes(ANTHROPIC_CREDIT_LOW_PHRASE);
|
|
234
|
+
}
|
|
228
235
|
function classifyAnthropicError(error) {
|
|
236
|
+
if (isAnthropicCreditExhausted(error))
|
|
237
|
+
return "usage-limit";
|
|
229
238
|
return (0, error_classification_1.classifyHttpError)(error, {
|
|
230
239
|
isAuthFailure: isAnthropicAuthFailure,
|
|
231
240
|
isServerError: (e) => e.status === 529,
|
|
@@ -236,9 +245,10 @@ function isAnthropicAuthFailure(error) {
|
|
|
236
245
|
return (lower.includes("oauth authentication") ||
|
|
237
246
|
lower.includes("authentication failed") ||
|
|
238
247
|
lower.includes("unauthorized") ||
|
|
239
|
-
lower.includes("invalid api key")
|
|
248
|
+
lower.includes("invalid api key") ||
|
|
249
|
+
lower.includes("invalid x-api-key"));
|
|
240
250
|
}
|
|
241
|
-
async function streamAnthropicMessages(client, model, request) {
|
|
251
|
+
async function streamAnthropicMessages(client, model, request, options = { claudeCodePreamble: true }) {
|
|
242
252
|
const { system, messages } = toAnthropicMessages(request.messages);
|
|
243
253
|
const additionalSystemParts = request.messages
|
|
244
254
|
.filter((message) => message.role === "system")
|
|
@@ -260,13 +270,14 @@ async function streamAnthropicMessages(client, model, request) {
|
|
|
260
270
|
// prompt when using OAuth setup tokens (sk-ant-oat01). Without it, Opus/Sonnet
|
|
261
271
|
// 4.6 requests are rejected with 400. This is the API's validation that the
|
|
262
272
|
// token is being used by a Claude Code client.
|
|
263
|
-
|
|
273
|
+
// API-key mode (a plain API key, for example through a credit gateway) needs no such block.
|
|
274
|
+
const preambleText = options.claudeCodePreamble ? "You are Claude Code, Anthropic's official CLI for Claude." : "";
|
|
264
275
|
if (request.systemPrompt) {
|
|
265
276
|
// Structured SystemPrompt: merge preamble + stable prefix into one cached block,
|
|
266
277
|
// volatile suffix as a separate uncached block.
|
|
267
278
|
const stableBlock = {
|
|
268
279
|
type: "text",
|
|
269
|
-
text: preambleText + "\n\n" + request.systemPrompt.stable,
|
|
280
|
+
text: preambleText ? preambleText + "\n\n" + request.systemPrompt.stable : request.systemPrompt.stable,
|
|
270
281
|
cache_control: { type: "ephemeral" },
|
|
271
282
|
};
|
|
272
283
|
params.system = [
|
|
@@ -279,9 +290,11 @@ async function streamAnthropicMessages(client, model, request) {
|
|
|
279
290
|
}
|
|
280
291
|
else if (system) {
|
|
281
292
|
// Fallback: no structured prompt, extract from messages (legacy path)
|
|
282
|
-
params.system =
|
|
293
|
+
params.system = preambleText
|
|
294
|
+
? [{ type: "text", text: preambleText }, { type: "text", text: system }]
|
|
295
|
+
: [{ type: "text", text: system }];
|
|
283
296
|
}
|
|
284
|
-
else {
|
|
297
|
+
else if (preambleText) {
|
|
285
298
|
params.system = [{ type: "text", text: preambleText }];
|
|
286
299
|
}
|
|
287
300
|
if (anthropicTools.length > 0)
|
|
@@ -441,6 +454,48 @@ async function streamAnthropicMessages(client, model, request) {
|
|
|
441
454
|
...(settleFinalization ? { settleFinalization } : {}),
|
|
442
455
|
};
|
|
443
456
|
}
|
|
457
|
+
const ANTHROPIC_PING_MODEL = "claude-haiku-4-5-20251001";
|
|
458
|
+
/**
|
|
459
|
+
* API-key mode: a plain Anthropic API key, optionally against another base URL such as a credit
|
|
460
|
+
* gateway. No Claude Code identity headers, no Claude Code system preamble and no token refresh;
|
|
461
|
+
* prompt caching, thinking, tools and streaming work as in setup-token mode.
|
|
462
|
+
*/
|
|
463
|
+
function createAnthropicApiKeyRuntime(model, apiKey, baseUrl) {
|
|
464
|
+
const modelCaps = (0, model_capabilities_1.getModelCapabilities)(model);
|
|
465
|
+
const capabilities = new Set();
|
|
466
|
+
if (modelCaps.reasoningEffort)
|
|
467
|
+
capabilities.add("reasoning-effort");
|
|
468
|
+
// authToken: null keeps the SDK from also sending an ANTHROPIC_AUTH_TOKEN from the environment.
|
|
469
|
+
const client = new sdk_1.default({ apiKey, authToken: null, baseURL: baseUrl, maxRetries: 0 });
|
|
470
|
+
(0, runtime_1.emitNervesEvent)({
|
|
471
|
+
component: "engine",
|
|
472
|
+
event: "engine.anthropic_api_key_mode",
|
|
473
|
+
message: "anthropic provider using API-key mode",
|
|
474
|
+
meta: { provider: "anthropic", customBaseUrl: baseUrl !== undefined },
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
id: "anthropic",
|
|
478
|
+
model,
|
|
479
|
+
client,
|
|
480
|
+
capabilities,
|
|
481
|
+
supportedReasoningEfforts: modelCaps.reasoningEffort,
|
|
482
|
+
resetTurnState(_messages) {
|
|
483
|
+
// Anthropic request payload is derived from canonical messages each turn.
|
|
484
|
+
},
|
|
485
|
+
appendToolOutput(_callId, _output) {
|
|
486
|
+
// Anthropic uses canonical messages for tool_result tracking.
|
|
487
|
+
},
|
|
488
|
+
streamTurn(request) {
|
|
489
|
+
return streamAnthropicMessages(client, model, request, { claudeCodePreamble: false });
|
|
490
|
+
},
|
|
491
|
+
async ping(signal) {
|
|
492
|
+
await client.messages.create({ model: ANTHROPIC_PING_MODEL, max_tokens: 1, messages: [{ role: "user", content: "ping" }] }, { signal });
|
|
493
|
+
},
|
|
494
|
+
classifyError(error) {
|
|
495
|
+
return classifyAnthropicError(error);
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
444
499
|
function createAnthropicProviderRuntime(model, anthropicConfig = (0, config_1.getAnthropicConfig)()) {
|
|
445
500
|
(0, runtime_1.emitNervesEvent)({
|
|
446
501
|
component: "engine",
|
|
@@ -448,6 +503,9 @@ function createAnthropicProviderRuntime(model, anthropicConfig = (0, config_1.ge
|
|
|
448
503
|
message: "anthropic provider init",
|
|
449
504
|
meta: { provider: "anthropic" },
|
|
450
505
|
});
|
|
506
|
+
const apiKey = anthropicConfig.apiKey?.trim();
|
|
507
|
+
if (apiKey)
|
|
508
|
+
return createAnthropicApiKeyRuntime(model, apiKey, anthropicConfig.baseUrl?.trim() || undefined);
|
|
451
509
|
if (!anthropicConfig.setupToken) {
|
|
452
510
|
throw new Error(getAnthropicReauthGuidance("provider 'anthropic' is selected but anthropic.setupToken is missing in the agent vault."));
|
|
453
511
|
}
|
|
@@ -509,7 +567,7 @@ function createAnthropicProviderRuntime(model, anthropicConfig = (0, config_1.ge
|
|
|
509
567
|
/* v8 ignore start -- ping: tested via provider-ping.test.ts @preserve */
|
|
510
568
|
async ping(signal) {
|
|
511
569
|
const freshClient = await ensureClient();
|
|
512
|
-
await freshClient.messages.create({ model:
|
|
570
|
+
await freshClient.messages.create({ model: ANTHROPIC_PING_MODEL, max_tokens: 1, messages: [{ role: "user", content: "ping" }] }, { signal, headers: { "anthropic-beta": "claude-code-20250219,oauth-2025-04-20" } });
|
|
513
571
|
},
|
|
514
572
|
/* v8 ignore stop */
|
|
515
573
|
/* v8 ignore next 3 -- delegation: classification logic tested via classifyAnthropicError @preserve */
|
|
@@ -108,7 +108,8 @@ function isProviderCredentialRecord(value) {
|
|
|
108
108
|
const provider = value.provider;
|
|
109
109
|
const canonicalCredentials = (0, provider_credentials_1.splitProviderCredentialFields)(provider, credentials).credentials;
|
|
110
110
|
const canonicalConfig = (0, provider_credentials_1.splitProviderCredentialFields)(provider, config).config;
|
|
111
|
-
|
|
111
|
+
const requirements = identity_1.PROVIDER_CREDENTIALS[provider];
|
|
112
|
+
if (![requirements.required, ...(requirements.alternativeRequired ?? [])].some((fields) => fields.every((field) => (isPresentCredentialValue(canonicalCredentials[field]) || isPresentCredentialValue(canonicalConfig[field])))))
|
|
112
113
|
return false;
|
|
113
114
|
const provenance = value.provenance;
|
|
114
115
|
if (!isRecord(provenance))
|
|
@@ -185,7 +185,7 @@ function summarizeArr(service, queuePayload, missingPayload, historyPayload, now
|
|
|
185
185
|
id, area: "downloads", severity: "warn", refs,
|
|
186
186
|
next: series ? "fix" : "owner",
|
|
187
187
|
summary: `${label} download of ${name} is stuck at ${percent}% (${release}): ${problem}`,
|
|
188
|
-
fingerprint: fingerprintOf(id,
|
|
188
|
+
fingerprint: fingerprintOf(id, "stuck"),
|
|
189
189
|
...(series ? { fix: { tool: "media_fill_missing", args: { service_id: subjectId }, note: "keeps the partial download; replaces it only when a seeded alternative is grabbed" } } : {}),
|
|
190
190
|
});
|
|
191
191
|
}
|
|
@@ -228,7 +228,7 @@ function summarizeArr(service, queuePayload, missingPayload, historyPayload, now
|
|
|
228
228
|
findings.push({
|
|
229
229
|
id, area: "missing", severity: "info", next: "fix",
|
|
230
230
|
summary: series ? `${entry.name}: ${entry.count} monitored episode${entry.count === 1 ? "" : "s"} missing, the oldest aired ${days} days ago` : `${entry.name}: monitored movie still missing after ${days} days`,
|
|
231
|
-
fingerprint: fingerprintOf(id,
|
|
231
|
+
fingerprint: fingerprintOf(id, "missing"),
|
|
232
232
|
fix: series
|
|
233
233
|
? { tool: "media_fill_missing", args: { service_id: subjectId }, note: "searches, grabs a seeded release, imports what finished" }
|
|
234
234
|
: { tool: "media_search_now", args: { kind: "movie", service_id: subjectId }, note: "triggers a Radarr search" },
|
|
@@ -272,7 +272,7 @@ async function sweepHost(sanctuary, policyStates, nowMs, findings) {
|
|
|
272
272
|
const capacity = (id, label, percent) => {
|
|
273
273
|
if (percent === null || percent < exports.CAPACITY_PERCENT)
|
|
274
274
|
return;
|
|
275
|
-
findings.push({ id, area: "disk", severity: percent >= exports.CRITICAL_CAPACITY_PERCENT ? "critical" : "warn", next: "owner", summary: `${label} is ${percent}% full`, fingerprint: fingerprintOf(id,
|
|
275
|
+
findings.push({ id, area: "disk", severity: percent >= exports.CRITICAL_CAPACITY_PERCENT ? "critical" : "warn", next: "owner", summary: `${label} is ${percent}% full`, fingerprint: fingerprintOf(id, percent >= exports.CRITICAL_CAPACITY_PERCENT ? "critical" : "full") });
|
|
276
276
|
};
|
|
277
277
|
capacity("disk:array:capacity", "the array", summary.arrayUsedPercent);
|
|
278
278
|
for (const share of list(storage?.shares).map(dict))
|
|
@@ -285,7 +285,7 @@ async function sweepHost(sanctuary, policyStates, nowMs, findings) {
|
|
|
285
285
|
findings.push({ id: `${id}:smart`, area: "disk", severity: "critical", next: "owner", summary: `${text(disk.name)} reports a failed SMART status`, fingerprint: fingerprintOf(`${id}:smart`, "failed") });
|
|
286
286
|
const temperature = num(disk.temperatureC);
|
|
287
287
|
if (temperature !== null && temperature >= exports.TEMPERATURE_C)
|
|
288
|
-
findings.push({ id: `${id}:temperature`, area: "disk", severity: "warn", next: "owner", summary: `${text(disk.name)} is ${temperature}C`, fingerprint: fingerprintOf(`${id}:temperature`,
|
|
288
|
+
findings.push({ id: `${id}:temperature`, area: "disk", severity: "warn", next: "owner", summary: `${text(disk.name)} is ${temperature}C`, fingerprint: fingerprintOf(`${id}:temperature`, "hot") });
|
|
289
289
|
}
|
|
290
290
|
const parity = dict(disks?.parity);
|
|
291
291
|
summary.parity = text(parity.result) || null;
|
|
@@ -294,7 +294,7 @@ async function sweepHost(sanctuary, policyStates, nowMs, findings) {
|
|
|
294
294
|
if (disks) {
|
|
295
295
|
const result = summary.parity;
|
|
296
296
|
if (result === "failed")
|
|
297
|
-
findings.push({ id: "parity:result", area: "parity", severity: "critical", next: "owner", summary: `the last parity check failed (${num(parity.errors) ?? "unknown"} errors)`, fingerprint: fingerprintOf("parity:result",
|
|
297
|
+
findings.push({ id: "parity:result", area: "parity", severity: "critical", next: "owner", summary: `the last parity check failed (${num(parity.errors) ?? "unknown"} errors)`, fingerprint: fingerprintOf("parity:result", "failed") });
|
|
298
298
|
else if (result !== "in_progress" && (summary.parityAgeDays === null || summary.parityAgeDays >= exports.PARITY_MAX_AGE_DAYS))
|
|
299
299
|
findings.push({ id: "parity:age", area: "parity", severity: "warn", next: "owner", summary: summary.parityAgeDays === null ? "parity check age is unknown" : `the last parity check was ${summary.parityAgeDays} days ago`, fingerprint: fingerprintOf("parity:age", summary.parityAgeDays === null ? "unknown" : "old") });
|
|
300
300
|
}
|
|
@@ -434,10 +434,12 @@ async function runHouseSweep(deps) {
|
|
|
434
434
|
})
|
|
435
435
|
.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] || a.id.localeCompare(b.id));
|
|
436
436
|
if (!deps.replay) {
|
|
437
|
-
// A finding that
|
|
438
|
-
//
|
|
437
|
+
// A finding that has been gone for REMIND_DAYS is forgotten, so it is told again if it comes back after that.
|
|
438
|
+
// A brief disappearance (or an unreadable source) keeps the entry, so a flickering finding never returns as "new".
|
|
439
439
|
const present = new Set(ordered.map((finding) => finding.id));
|
|
440
|
-
const kept = Object.fromEntries(Object.entries(ledger).filter(([id]) => present.has(id)
|
|
440
|
+
const kept = Object.fromEntries(Object.entries(ledger).filter(([id, told]) => present.has(id)
|
|
441
|
+
|| sourcesFor(id).some((source) => sources[source] !== "ok")
|
|
442
|
+
|| nowMs - Date.parse(told.reportedAt) < exports.REMIND_DAYS * DAY_MS));
|
|
441
443
|
if (Object.keys(kept).length !== Object.keys(ledger).length)
|
|
442
444
|
(0, exports.writeLedger)(deps.agentRoot, kept);
|
|
443
445
|
}
|
|
@@ -50,11 +50,12 @@ const externalEventDispositionProperties = {
|
|
|
50
50
|
expectedGeneration: { type: "number", description: "Exact generation shown in the external-event turn" },
|
|
51
51
|
classifiedRevision: { type: "string", description: "Exact observation revision investigated in this turn" },
|
|
52
52
|
classification: { type: "string", enum: ["expected", "needs_attention", "adopted", "snoozed", "dismissed_until_change", "resolved"] },
|
|
53
|
-
stewardPolicyKind: { type: "string", enum: ["current", "none"], description: "Use current with the exact live policy key/version
|
|
53
|
+
stewardPolicyKind: { type: "string", enum: ["current", "none"], description: "Use current with the exact live policy key/version when one applies; otherwise use none. Any event may be closed with none." },
|
|
54
54
|
stewardPolicyKey: { type: "string", description: "Exact current policy key used for this decision" },
|
|
55
55
|
stewardPolicyVersion: { type: "number", description: "Exact current policy version used for this decision" },
|
|
56
56
|
decision: { type: "string", enum: ["silent", "act", "ask", "report"] },
|
|
57
|
-
reason: { type: "string", description: "
|
|
57
|
+
reason: { type: "string", description: "Internal audit note for the decision; never shown to the owner" },
|
|
58
|
+
owner_message: { type: "string", description: "Required for ask or report: the exact short plain message the owner reads, as you would text a friend. No ids, leases, generations, revisions or receipts." },
|
|
58
59
|
nextWake: { type: "string", enum: ["on_change", "on_escalation", "on_recovery", "at"], description: "Use on_change unless you already called await_condition for this exact event and are supplying its awaitId" },
|
|
59
60
|
wakeAt: { type: "string", description: "ISO time required only when nextWake=at" },
|
|
60
61
|
awaitId: { type: "string", description: "Existing await_condition receipt required when nextWake=at; never invent one" },
|
|
@@ -70,6 +71,8 @@ const externalEventDispositionBatchSchema = {
|
|
|
70
71
|
description: "All exact leases from one coalesced external-event turn",
|
|
71
72
|
items: { type: "object", properties: externalEventDispositionProperties, required: externalEventDispositionRequired, additionalProperties: false },
|
|
72
73
|
};
|
|
74
|
+
// The owner reads owner_message verbatim, so internal bookkeeping words never reach them.
|
|
75
|
+
const OWNER_MESSAGE_FORBIDDEN = /\b(generation|lease|revision|receipt|disposition|claimOwner|recordPath)s?\b|[\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff]/iu;
|
|
73
76
|
function prepareExternalEventDisposition(a, ctx) {
|
|
74
77
|
const agentName = (0, identity_1.getAgentName)();
|
|
75
78
|
const agentEventRoot = path.resolve((0, router_1.getExternalEventRoot)(), agentName);
|
|
@@ -99,6 +102,7 @@ function prepareExternalEventDisposition(a, ctx) {
|
|
|
99
102
|
const policyVersion = Number(a.stewardPolicyVersion);
|
|
100
103
|
const policyKind = String(a.stewardPolicyKind ?? "");
|
|
101
104
|
const reason = String(a.reason ?? "").trim();
|
|
105
|
+
const ownerText = String(a.owner_message ?? "").trim();
|
|
102
106
|
if (!reason || (policyKind !== "none" && (!Number.isSafeInteger(policyVersion) || policyVersion < 1))
|
|
103
107
|
|| !["current", "none"].includes(policyKind)
|
|
104
108
|
|| !["expected", "needs_attention", "adopted", "snoozed", "dismissed_until_change", "resolved"].includes(classification)
|
|
@@ -113,8 +117,6 @@ function prepareExternalEventDisposition(a, ctx) {
|
|
|
113
117
|
const policy = (0, steward_policy_1.readStewardPolicy)(agentRoot);
|
|
114
118
|
let stewardPolicy;
|
|
115
119
|
if (policyKind === "none") {
|
|
116
|
-
if (record.transition !== "opened")
|
|
117
|
-
throw new Error("External event no-policy disposition requires a fresh observation");
|
|
118
120
|
stewardPolicy = { kind: "none" };
|
|
119
121
|
}
|
|
120
122
|
else {
|
|
@@ -178,15 +180,19 @@ function prepareExternalEventDisposition(a, ctx) {
|
|
|
178
180
|
finish: async () => {
|
|
179
181
|
let ownerMessage;
|
|
180
182
|
if (decision === "ask" || decision === "report") {
|
|
181
|
-
if (
|
|
183
|
+
if (!ownerText)
|
|
184
|
+
throw new Error("ask and report need owner_message: the short plain text the owner will read");
|
|
185
|
+
if (Buffer.byteLength(ownerText, "utf8") > 1_200)
|
|
182
186
|
throw new Error("External event owner message must be phone-sized");
|
|
187
|
+
if (OWNER_MESSAGE_FORBIDDEN.test(ownerText))
|
|
188
|
+
throw new Error("owner_message must be plain words for the owner, with no internal ids, leases, generations, revisions, receipts or hidden characters");
|
|
183
189
|
if (!ctx?.externalEventEffects)
|
|
184
190
|
throw new Error("External event owner delivery is unavailable");
|
|
185
191
|
// The whole dispatched batch shares one owner message; the delivery port settles later decisions as covered by it.
|
|
186
192
|
const relatedEvents = [turnContext, ...(turnContext.relatedEvents ?? [])]
|
|
187
193
|
.filter((event) => path.resolve(event.recordPath) !== recordPath)
|
|
188
194
|
.map((event) => ({ source: event.source, eventId: event.eventId, generation: event.generation }));
|
|
189
|
-
const delivered = await ctx.externalEventEffects.deliverOwnerDecision({ source: record.source, eventId: record.eventId, generation: record.generation, text:
|
|
195
|
+
const delivered = await ctx.externalEventEffects.deliverOwnerDecision({ source: record.source, eventId: record.eventId, generation: record.generation, text: ownerText, relatedEvents });
|
|
190
196
|
ownerMessage = { status: delivered.status, effectKey: delivered.effectKey };
|
|
191
197
|
}
|
|
192
198
|
const handled = (0, router_1.commitExternalEventDisposition)(recordPath, {
|
|
@@ -220,7 +226,7 @@ exports.continuityToolDefinitions = [
|
|
|
220
226
|
type: "function",
|
|
221
227
|
function: {
|
|
222
228
|
name: "external_event_disposition",
|
|
223
|
-
description: "Classify the exact external-event generation I just investigated. An ask or report is the sole owner-delivery path and sends
|
|
229
|
+
description: "Classify the exact external-event generation I just investigated. An ask or report is the sole owner-delivery path and sends owner_message once for this receipt; reason stays internal. silent and act only record the disposition. In a batch, only the first ask or report sends a message and the later ones are recorded as covered by it, so write that first owner_message to cover every listed incident.",
|
|
224
230
|
parameters: {
|
|
225
231
|
type: "object",
|
|
226
232
|
properties: {
|
|
@@ -396,7 +402,7 @@ exports.continuityToolDefinitions = [
|
|
|
396
402
|
incidentKey: { type: "string", description: "Stable incident key within the source" },
|
|
397
403
|
classifiedRevision: { type: "string", description: "Observation revision classified by the agent" },
|
|
398
404
|
correlationKey: { type: "string", description: "Optional key relating several incidents to one concern" },
|
|
399
|
-
expectedUpdatedAt: { type: "string", description: "
|
|
405
|
+
expectedUpdatedAt: { type: "string", description: "The Care's current updatedAt, copied from the last care_manage result. Required for bind_incident and resolve_incident; optional for update." },
|
|
400
406
|
currentRisk: { type: "string", description: "Current material risk for this Care, or an empty string to clear it" },
|
|
401
407
|
nextCheckAt: { type: "string", description: "Exact ISO time of the next owned check" },
|
|
402
408
|
},
|
|
@@ -442,7 +448,7 @@ exports.continuityToolDefinitions = [
|
|
|
442
448
|
}
|
|
443
449
|
else if (a.action === "bind_incident") {
|
|
444
450
|
if (!a.expectedUpdatedAt)
|
|
445
|
-
throw new Error("Care
|
|
451
|
+
throw new Error("bind_incident requires expectedUpdatedAt: copy the Care's current updatedAt from the last care_manage result");
|
|
446
452
|
result = (0, cares_1.bindCareIncident)(agentRoot, a.id, {
|
|
447
453
|
source: a.source,
|
|
448
454
|
incidentKey: a.incidentKey,
|
|
@@ -452,7 +458,7 @@ exports.continuityToolDefinitions = [
|
|
|
452
458
|
}
|
|
453
459
|
else if (a.action === "resolve_incident") {
|
|
454
460
|
if (!a.expectedUpdatedAt)
|
|
455
|
-
throw new Error("Care
|
|
461
|
+
throw new Error("resolve_incident requires expectedUpdatedAt: copy the Care's current updatedAt from the last care_manage result");
|
|
456
462
|
result = (0, cares_1.resolveCareIncident)(agentRoot, a.id, {
|
|
457
463
|
source: a.source,
|
|
458
464
|
incidentKey: a.incidentKey,
|