@deeprelay/mcp 0.0.1 → 0.1.0

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 (77) hide show
  1. package/README.md +188 -1
  2. package/dist/auth.d.ts +52 -0
  3. package/dist/auth.js +122 -0
  4. package/dist/auth.js.map +1 -0
  5. package/dist/errors.d.ts +83 -0
  6. package/dist/errors.js +307 -0
  7. package/dist/errors.js.map +1 -0
  8. package/dist/guardrail/budget.d.ts +95 -0
  9. package/dist/guardrail/budget.js +225 -0
  10. package/dist/guardrail/budget.js.map +1 -0
  11. package/dist/guardrail/elicitation.d.ts +12 -0
  12. package/dist/guardrail/elicitation.js +79 -0
  13. package/dist/guardrail/elicitation.js.map +1 -0
  14. package/dist/guardrail/estimate.d.ts +67 -0
  15. package/dist/guardrail/estimate.js +171 -0
  16. package/dist/guardrail/estimate.js.map +1 -0
  17. package/dist/guardrail/handshake.d.ts +32 -0
  18. package/dist/guardrail/handshake.js +164 -0
  19. package/dist/guardrail/handshake.js.map +1 -0
  20. package/dist/guardrail/types.d.ts +61 -0
  21. package/dist/guardrail/types.js +10 -0
  22. package/dist/guardrail/types.js.map +1 -0
  23. package/dist/index.d.ts +18 -0
  24. package/dist/index.js +61 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/roster.d.ts +36 -0
  27. package/dist/roster.js +130 -0
  28. package/dist/roster.js.map +1 -0
  29. package/dist/tools/crypto-deposit-gate.d.ts +21 -0
  30. package/dist/tools/crypto-deposit-gate.js +91 -0
  31. package/dist/tools/crypto-deposit-gate.js.map +1 -0
  32. package/dist/tools/index.d.ts +16 -0
  33. package/dist/tools/index.js +108 -0
  34. package/dist/tools/index.js.map +1 -0
  35. package/dist/tools/readonly/define.d.ts +32 -0
  36. package/dist/tools/readonly/define.js +64 -0
  37. package/dist/tools/readonly/define.js.map +1 -0
  38. package/dist/tools/readonly/index.d.ts +7 -0
  39. package/dist/tools/readonly/index.js +292 -0
  40. package/dist/tools/readonly/index.js.map +1 -0
  41. package/dist/tools/readonly/video-reads.d.ts +11 -0
  42. package/dist/tools/readonly/video-reads.js +170 -0
  43. package/dist/tools/readonly/video-reads.js.map +1 -0
  44. package/dist/tools/result.d.ts +22 -0
  45. package/dist/tools/result.js +63 -0
  46. package/dist/tools/result.js.map +1 -0
  47. package/dist/tools/spend/define.d.ts +93 -0
  48. package/dist/tools/spend/define.js +286 -0
  49. package/dist/tools/spend/define.js.map +1 -0
  50. package/dist/tools/spend/index.d.ts +11 -0
  51. package/dist/tools/spend/index.js +163 -0
  52. package/dist/tools/spend/index.js.map +1 -0
  53. package/dist/tools/spend/media.d.ts +9 -0
  54. package/dist/tools/spend/media.js +164 -0
  55. package/dist/tools/spend/media.js.map +1 -0
  56. package/dist/tools/spend/pricing.d.ts +17 -0
  57. package/dist/tools/spend/pricing.js +58 -0
  58. package/dist/tools/spend/pricing.js.map +1 -0
  59. package/dist/tools/types.d.ts +45 -0
  60. package/dist/tools/types.js +3 -0
  61. package/dist/tools/types.js.map +1 -0
  62. package/dist/tools/write/define.d.ts +60 -0
  63. package/dist/tools/write/define.js +110 -0
  64. package/dist/tools/write/define.js.map +1 -0
  65. package/dist/tools/write/index.d.ts +12 -0
  66. package/dist/tools/write/index.js +159 -0
  67. package/dist/tools/write/index.js.map +1 -0
  68. package/dist/tools/write/payment-links.d.ts +3 -0
  69. package/dist/tools/write/payment-links.js +161 -0
  70. package/dist/tools/write/payment-links.js.map +1 -0
  71. package/dist/tools/write/spending-limit.d.ts +2 -0
  72. package/dist/tools/write/spending-limit.js +229 -0
  73. package/dist/tools/write/spending-limit.js.map +1 -0
  74. package/dist/version.d.ts +3 -0
  75. package/dist/version.js +36 -0
  76. package/dist/version.js.map +1 -0
  77. package/package.json +39 -3
package/dist/errors.js ADDED
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QuoteUnavailableError = exports.CREDENTIAL_KEYS = exports.REDACTED = void 0;
4
+ exports.redactSecrets = redactSecrets;
5
+ exports.sanitizeCredentialFields = sanitizeCredentialFields;
6
+ exports.httpStatusOf = httpStatusOf;
7
+ exports.mapSdkError = mapSdkError;
8
+ exports.readErrorEnvelope = readErrorEnvelope;
9
+ exports.mapSdkErrorAsync = mapSdkErrorAsync;
10
+ // The error and redaction funnel every tool result goes through
11
+ // (Phase 104, D-12: authored fresh against the invariants below).
12
+ //
13
+ // 1. Agent-legible failures. The SDK throws the same ResponseError for every
14
+ // non-2xx. An agent that cannot tell "your key is bad" from "retry later"
15
+ // loops or gives up, so each status becomes one sentence that names the
16
+ // cause and the fix, and names deeprelay as the only party involved.
17
+ // 2. No credential reaches the agent. Response bodies are untrusted text.
18
+ // Only a short summary field is ever lifted out of one, and every string
19
+ // this module returns has been through redactSecrets().
20
+ // 3. No upstream provider name reaches the agent (INFPRIV-06). The server
21
+ // scrubs provider names from every /v1/* error before it leaves the wire,
22
+ // and that scrub is authoritative. This package ships to npm, so it
23
+ // deliberately carries no list of upstream names of its own; nothing here
24
+ // adds a provider name to a sentence.
25
+ //
26
+ // This module logs nothing. On a stdio MCP server stdout is the JSON-RPC
27
+ // channel, and errors travel back to the caller as return values only.
28
+ //
29
+ // It is a leaf: it imports nothing from auth.ts, because auth.ts imports
30
+ // redactSecrets from here. The two env-var names are restated below and the
31
+ // tests assert they agree with auth.ts.
32
+ const sdk_1 = require("@deeprelay/sdk");
33
+ /** What replaces anything credential-shaped. */
34
+ exports.REDACTED = "[REDACTED]";
35
+ const KEY_ENV = "DEEPRELAY_API_KEY";
36
+ const BASE_ENV = "DEEPRELAY_API_BASE";
37
+ /** Named in network messages when no override is set. Mirrors auth.ts PROD_API_BASE. */
38
+ const DEFAULT_BASE_FOR_MESSAGES = "https://api.deeprelay.ai/v1";
39
+ /**
40
+ * deeprelay credentials: `deeprelay_live_…` and `deeprelay_test_…` keys.
41
+ * Narrow on purpose: other `deeprelay_`-prefixed strings (request ids such as
42
+ * `deeprelay_inf_…`, this server's own tool names) are not secrets and support
43
+ * needs them intact. Case-sensitive, so the env-var NAME `DEEPRELAY_API_KEY`
44
+ * in a remedy sentence is not itself redacted.
45
+ */
46
+ const TOKEN_SHAPE = /deeprelay_(live|test)_[A-Za-z0-9_]+/g;
47
+ /**
48
+ * Strip credentials out of arbitrary text. Two passes, because either alone
49
+ * leaves a hole:
50
+ *
51
+ * 1. the configured key's literal value, which catches a key of any shape
52
+ * (a placeholder, another vendor's key pasted by mistake);
53
+ * 2. the deeprelay token shape, which catches a DIFFERENT key echoed back by
54
+ * the API, one this process never held and so cannot match by value.
55
+ */
56
+ function redactSecrets(text, env = process.env) {
57
+ let out = text;
58
+ const key = env[KEY_ENV];
59
+ if (key !== undefined && key.trim() !== "") {
60
+ out = out.split(key).join(exports.REDACTED);
61
+ }
62
+ return out.replace(TOKEN_SHAPE, exports.REDACTED);
63
+ }
64
+ /**
65
+ * Field names whose string VALUES are credential material. Both spellings,
66
+ * because SDK models are camelCase and raw bodies are snake_case. Exact match:
67
+ * a substring rule would also swallow ordinary fields such as `secret_rotated_at`
68
+ * style metadata or URLs the user needs.
69
+ *
70
+ * The webhook create tool reveals the new endpoint's signing secret exactly
71
+ * once through an explicit reveal path in tools/result.ts. Nothing else ever
72
+ * bypasses this set.
73
+ */
74
+ exports.CREDENTIAL_KEYS = new Set([
75
+ "secret",
76
+ "signing_secret",
77
+ "signingSecret",
78
+ "api_key",
79
+ "apiKey",
80
+ "access_token",
81
+ "accessToken",
82
+ "authorization",
83
+ ]);
84
+ /**
85
+ * Return a copy of `value` with every string under a CREDENTIAL_KEYS field
86
+ * replaced by REDACTED, at any depth and inside arrays. The input is never
87
+ * mutated. Non-string credential values (null, absent) are left alone so the
88
+ * agent is not told a secret exists where none does. Class instances such as
89
+ * Date are returned whole rather than flattened.
90
+ */
91
+ function sanitizeCredentialFields(value) {
92
+ if (Array.isArray(value)) {
93
+ return value.map((item) => sanitizeCredentialFields(item));
94
+ }
95
+ if (typeof value !== "object" || value === null) {
96
+ return value;
97
+ }
98
+ const proto = Object.getPrototypeOf(value);
99
+ if (proto !== Object.prototype && proto !== null) {
100
+ return value;
101
+ }
102
+ const out = {};
103
+ for (const [field, item] of Object.entries(value)) {
104
+ out[field] =
105
+ exports.CREDENTIAL_KEYS.has(field) && typeof item === "string"
106
+ ? exports.REDACTED
107
+ : sanitizeCredentialFields(item);
108
+ }
109
+ return out;
110
+ }
111
+ /**
112
+ * A spend or write tool's quote step refusing to price a request. Throwing is
113
+ * what keeps a guardrail from minting a confirmation token for something that
114
+ * has no quote: no quote, no token, no execution. The mapper keeps this
115
+ * error's own wording.
116
+ */
117
+ class QuoteUnavailableError extends Error {
118
+ constructor(message) {
119
+ super(message);
120
+ this.name = "QuoteUnavailableError";
121
+ }
122
+ }
123
+ exports.QuoteUnavailableError = QuoteUnavailableError;
124
+ /**
125
+ * The HTTP status of an SDK error, or undefined for anything that never got a
126
+ * response. Duck-typed as a fallback so a bundling or version skew that breaks
127
+ * instanceof cannot change behaviour. Exported so callers can branch on a
128
+ * status (e.g. a 404 spending limit meaning "none configured") consistently
129
+ * with the mapper.
130
+ */
131
+ function httpStatusOf(err) {
132
+ if (err instanceof sdk_1.ResponseError) {
133
+ return err.response.status;
134
+ }
135
+ const status = err?.response?.status;
136
+ return typeof status === "number" ? status : undefined;
137
+ }
138
+ const MAX_DETAIL_LENGTH = 300;
139
+ function oneLine(text) {
140
+ return text.replace(/\s+/g, " ").trim();
141
+ }
142
+ /** Prepare API-supplied text for embedding: one line, bounded. */
143
+ function cleanDetail(text) {
144
+ if (text === undefined) {
145
+ return undefined;
146
+ }
147
+ const cleaned = oneLine(text);
148
+ if (cleaned === "") {
149
+ return undefined;
150
+ }
151
+ return cleaned.length > MAX_DETAIL_LENGTH ? `${cleaned.slice(0, MAX_DETAIL_LENGTH - 1)}…` : cleaned;
152
+ }
153
+ function withDetails(sentence, details) {
154
+ const cleaned = cleanDetail(details);
155
+ return cleaned === undefined ? sentence : `${sentence} ${cleaned}`;
156
+ }
157
+ /** The base URL named in a network message, with any userinfo stripped. */
158
+ function baseForMessages(env) {
159
+ const raw = env[BASE_ENV]?.trim();
160
+ if (raw === undefined || raw === "") {
161
+ return DEFAULT_BASE_FOR_MESSAGES;
162
+ }
163
+ try {
164
+ const url = new URL(raw);
165
+ url.username = "";
166
+ url.password = "";
167
+ return url.toString().replace(/\/+$/, "");
168
+ }
169
+ catch {
170
+ return `the configured ${BASE_ENV}`;
171
+ }
172
+ }
173
+ function formatZodIssues(err) {
174
+ const issues = err.issues;
175
+ if (!Array.isArray(issues) || issues.length === 0) {
176
+ return err instanceof Error ? err.message : "the arguments did not match the tool's schema";
177
+ }
178
+ return issues
179
+ .map((issue) => {
180
+ const { path, message } = issue;
181
+ const where = Array.isArray(path) && path.length > 0 ? path.map(String).join(".") : "(root)";
182
+ return `${where}: ${typeof message === "string" ? message : "invalid"}`;
183
+ })
184
+ .join("; ");
185
+ }
186
+ function statusSentence(status, context, details, code) {
187
+ switch (status) {
188
+ case 401:
189
+ // Deliberately carries no response detail: a 401 is about the
190
+ // credential, and echoing the body is the likeliest leak path.
191
+ return `deeprelay API key invalid or expired — check the ${KEY_ENV} environment variable in your MCP client configuration (while ${context}).`;
192
+ case 402: {
193
+ const base = `Insufficient balance — add credit at deeprelay.ai, then retry (while ${context}).`;
194
+ const withCode = code === "daily_limit_reached"
195
+ ? `${base} Code: daily_limit_reached — the account's daily spending limit has been reached.`
196
+ : base;
197
+ return withDetails(withCode, details);
198
+ }
199
+ case 403:
200
+ return withDetails(`Not permitted — this API key lacks the scope required for ${context}, or the account lacks the org-admin role.`, details);
201
+ case 404:
202
+ return withDetails(`Not found — ${context} referred to something that does not exist or is already gone.`, details);
203
+ case 409:
204
+ return withDetails(`Conflict — ${context} collided with the current state of the resource.`, details);
205
+ case 422:
206
+ return withDetails(`Invalid request — the deeprelay API rejected ${context}.`, details);
207
+ case 429:
208
+ return `Rate limited — wait a moment and retry (while ${context}). Do not retry in a tight loop.`;
209
+ default:
210
+ break;
211
+ }
212
+ if (status >= 500) {
213
+ return `deeprelay API error (${status}) while ${context} — retry later; if it persists check https://deeprelay.ai/status.`;
214
+ }
215
+ if (status >= 400) {
216
+ return withDetails(`Request rejected by the deeprelay API (HTTP ${status}) while ${context}.`, details);
217
+ }
218
+ return `Unexpected response from the deeprelay API (HTTP ${status}) while ${context}.`;
219
+ }
220
+ function noStatusSentence(err, context, env, details) {
221
+ const name = err instanceof Error ? err.name : undefined;
222
+ switch (name) {
223
+ case "ZodError":
224
+ return `Invalid arguments while ${context}: ${cleanDetail(formatZodIssues(err)) ?? "see the tool schema"}`;
225
+ case "StartupConfigError":
226
+ case "QuoteUnavailableError":
227
+ // Already a diagnosis in its own words; keep it.
228
+ return `${cleanDetail(err.message) ?? "Request refused."} (while ${context})`;
229
+ case "RequiredError": {
230
+ const field = err.field;
231
+ return `Missing required parameter${typeof field === "string" ? ` "${field}"` : ""} while ${context}.`;
232
+ }
233
+ case "FetchError":
234
+ case "AbortError":
235
+ case "TimeoutError":
236
+ return networkSentence(context, env);
237
+ default:
238
+ break;
239
+ }
240
+ if (name === "TypeError" && /fetch/i.test(err.message)) {
241
+ return networkSentence(context, env);
242
+ }
243
+ const raw = err instanceof Error ? err.message : String(err);
244
+ return withDetails(`Unexpected error while ${context}.`, details ?? raw);
245
+ }
246
+ function networkSentence(context, env) {
247
+ return `Could not reach the deeprelay API at ${baseForMessages(env)} while ${context} — check ${BASE_ENV} and your network.`;
248
+ }
249
+ /**
250
+ * Turn any thrown value into an agent-legible Error. Synchronous so the
251
+ * mapping is directly testable; tool handlers normally call mapSdkErrorAsync,
252
+ * which reads the response body first.
253
+ *
254
+ * `context` names what was being attempted ("listing models"), so a failure
255
+ * several calls deep stays attributable.
256
+ */
257
+ function mapSdkError(err, context, options = {}) {
258
+ const env = options.env ?? process.env;
259
+ const status = httpStatusOf(err);
260
+ const sentence = status === undefined
261
+ ? noStatusSentence(err, context, env, options.details)
262
+ : statusSentence(status, context, options.details, options.code);
263
+ return new Error(redactSecrets(oneLine(sentence), env));
264
+ }
265
+ /**
266
+ * Read `{ code, message }` out of an SDK error's response body. Accepts both
267
+ * shapes the API returns: the OpenAI envelope `{ error: { code, message } }`
268
+ * and problem+json `{ code, detail }`. Fails closed: anything that is not
269
+ * recognisably one of those yields an empty envelope, never the raw body.
270
+ * Reads a clone, so the caller's Response is left unconsumed.
271
+ */
272
+ async function readErrorEnvelope(err) {
273
+ const response = err?.response;
274
+ if (response === undefined || response === null || typeof response.clone !== "function") {
275
+ return {};
276
+ }
277
+ let body;
278
+ try {
279
+ body = JSON.parse(await response.clone().text());
280
+ }
281
+ catch {
282
+ return {};
283
+ }
284
+ if (typeof body !== "object" || body === null) {
285
+ return {};
286
+ }
287
+ const record = body;
288
+ const nested = record.error;
289
+ const source = typeof nested === "object" && nested !== null ? nested : record;
290
+ const code = typeof source.code === "string" && source.code !== "" ? source.code : undefined;
291
+ const messageField = source === record ? (record.detail ?? record.message) : source.message;
292
+ const message = typeof messageField === "string" && messageField.trim() !== "" ? messageField : undefined;
293
+ return { code, message };
294
+ }
295
+ /**
296
+ * The funnel tool handlers call: read the body once, then map. A 401 never
297
+ * uses the body (see statusSentence).
298
+ */
299
+ async function mapSdkErrorAsync(err, context, env = process.env) {
300
+ const status = httpStatusOf(err);
301
+ if (status === undefined || status === 401) {
302
+ return mapSdkError(err, context, { env });
303
+ }
304
+ const envelope = await readErrorEnvelope(err);
305
+ return mapSdkError(err, context, { env, details: envelope.message, code: envelope.code });
306
+ }
307
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ import type { BudgetDecision } from "./types.js";
2
+ /** Micro-cents (1e-6 cent) per cent. Mirrors catalog.MicroCentsPerCent. */
3
+ export declare const MICRO_CENTS_PER_CENT = 1000000;
4
+ /** Default session budget: $5.00 (D-08). */
5
+ export declare const SESSION_BUDGET_DEFAULT_CENTS = 500;
6
+ /** Upper bound for a user-set session budget: $100.00. */
7
+ export declare const SESSION_BUDGET_MAX_CENTS = 10000;
8
+ /** Default per-call ceiling: $0.50 (D-08). */
9
+ export declare const PER_CALL_CEILING_CENTS = 50;
10
+ export declare const PER_CALL_CEILING_MICRO_CENTS: number;
11
+ /**
12
+ * Round one invocation's raw cost UP to whole cents, expressed in micro-cents.
13
+ *
14
+ * The server bills each invocation as ceil(microcents / 1e6) cents
15
+ * (costCentsFor / db.CentsFromMicroCentsCeil). The rounding happens per call and
16
+ * always upward, so its error grows with CALL COUNT. Aggregating first and
17
+ * rounding once would under-count a session of sub-cent calls by up to 100x
18
+ * (the D-82-A regression). Every estimate and every actual goes through this
19
+ * function one invocation at a time.
20
+ *
21
+ * A non-positive or non-finite raw cost bills nothing, which mirrors the
22
+ * server's `if sum <= 0 { return 0 }`.
23
+ */
24
+ export declare function billedMicroCents(rawMicroCents: number): number;
25
+ export interface BudgetStatus {
26
+ approved: boolean;
27
+ limitCents: number;
28
+ ceilingMicroCents: number;
29
+ settledMicroCents: number;
30
+ pendingMicroCents: number;
31
+ /** limit - settled - pending, floored at 0. */
32
+ remainingMicroCents: number;
33
+ }
34
+ export declare function budgetStatus(): BudgetStatus;
35
+ /**
36
+ * Approve a session budget. This is called only after the user confirms the
37
+ * budget handshake. The amount is clamped to [1, SESSION_BUDGET_MAX_CENTS].
38
+ * A fresh approval opens a fresh window: settled spend resets to 0.
39
+ * Outstanding reservations stay, because those calls may still bill.
40
+ * The per-call ceiling can also be set here. It is clamped the same way.
41
+ */
42
+ export declare function approveBudget(cents: number, opts?: {
43
+ ceilingCents?: number;
44
+ }): BudgetStatus;
45
+ /**
46
+ * Decide whether a spend call with this estimate may run.
47
+ *
48
+ * Order (a safety property, D-08):
49
+ * 1. over_ceiling estimate above the per-call ceiling, even when unapproved
50
+ * 2. needs_budget_handshake no budget approved yet
51
+ * 3. needs_budget_handshake settled actuals have reached the limit (the wall)
52
+ * 4. needs_budget_handshake estimate exceeds remaining headroom (including pending)
53
+ * 5. ok
54
+ */
55
+ export declare function checkCall(estimateMicroCents: number, opts?: {
56
+ ceilingMicroCents?: number;
57
+ }): BudgetDecision;
58
+ /** Reserve an estimate before the network call. It reduces headroom until settled or released. */
59
+ export declare function recordEstimate(callId: string, microCents: number): void;
60
+ /** Settle a call at its actual cost. Removes the reservation and adds the billed actual to settled spend. */
61
+ export declare function reconcileActual(callId: string, actualMicroCents: number): void;
62
+ /** Drop a reservation for a call that was never billed (error, cancel, failure). */
63
+ export declare function releaseEstimate(callId: string): void;
64
+ /**
65
+ * Hold a video job's floor estimate under callId and map the video id to it,
66
+ * so a later read (deeprelay_videos_get / _list) can settle it.
67
+ */
68
+ export declare function registerPendingVideo(videoId: string, callId: string, floorMicroCents: number): void;
69
+ export type VideoStatus = "queued" | "in_progress" | "completed" | "failed" | "cancelled" | "expired";
70
+ /**
71
+ * Settle a video reservation from an observed job status.
72
+ * completed -> reconcile at costCents (or the reserved floor if costCents is absent)
73
+ * failed | cancelled | expired -> release. These are never billed.
74
+ * queued | in_progress (or anything else) -> no change
75
+ */
76
+ export declare function settleVideo(videoId: string, outcome: {
77
+ status: VideoStatus | string;
78
+ costCents?: number;
79
+ }): {
80
+ settled: boolean;
81
+ microCents?: number;
82
+ };
83
+ /** One outstanding video reservation, keyed by the video id a read can settle. */
84
+ export interface PendingVideoReservation {
85
+ videoId: string;
86
+ microCents: number;
87
+ }
88
+ /**
89
+ * The video reservations still waiting on a terminal status. Reported by
90
+ * deeprelay_auth_status and used by the video reads to tell "still pending"
91
+ * apart from "never tracked by this session".
92
+ */
93
+ export declare function pendingVideoReservations(): PendingVideoReservation[];
94
+ /** Test-only: restore the fresh-process state. */
95
+ export declare function resetBudgetStateForTests(): void;
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PER_CALL_CEILING_MICRO_CENTS = exports.PER_CALL_CEILING_CENTS = exports.SESSION_BUDGET_MAX_CENTS = exports.SESSION_BUDGET_DEFAULT_CENTS = exports.MICRO_CENTS_PER_CENT = void 0;
4
+ exports.billedMicroCents = billedMicroCents;
5
+ exports.budgetStatus = budgetStatus;
6
+ exports.approveBudget = approveBudget;
7
+ exports.checkCall = checkCall;
8
+ exports.recordEstimate = recordEstimate;
9
+ exports.reconcileActual = reconcileActual;
10
+ exports.releaseEstimate = releaseEstimate;
11
+ exports.registerPendingVideo = registerPendingVideo;
12
+ exports.settleVideo = settleVideo;
13
+ exports.pendingVideoReservations = pendingVideoReservations;
14
+ exports.resetBudgetStateForTests = resetBudgetStateForTests;
15
+ /** Micro-cents (1e-6 cent) per cent. Mirrors catalog.MicroCentsPerCent. */
16
+ exports.MICRO_CENTS_PER_CENT = 1_000_000;
17
+ /** Default session budget: $5.00 (D-08). */
18
+ exports.SESSION_BUDGET_DEFAULT_CENTS = 500;
19
+ /** Upper bound for a user-set session budget: $100.00. */
20
+ exports.SESSION_BUDGET_MAX_CENTS = 10000;
21
+ /** Default per-call ceiling: $0.50 (D-08). */
22
+ exports.PER_CALL_CEILING_CENTS = 50;
23
+ exports.PER_CALL_CEILING_MICRO_CENTS = exports.PER_CALL_CEILING_CENTS * exports.MICRO_CENTS_PER_CENT;
24
+ /**
25
+ * Round one invocation's raw cost UP to whole cents, expressed in micro-cents.
26
+ *
27
+ * The server bills each invocation as ceil(microcents / 1e6) cents
28
+ * (costCentsFor / db.CentsFromMicroCentsCeil). The rounding happens per call and
29
+ * always upward, so its error grows with CALL COUNT. Aggregating first and
30
+ * rounding once would under-count a session of sub-cent calls by up to 100x
31
+ * (the D-82-A regression). Every estimate and every actual goes through this
32
+ * function one invocation at a time.
33
+ *
34
+ * A non-positive or non-finite raw cost bills nothing, which mirrors the
35
+ * server's `if sum <= 0 { return 0 }`.
36
+ */
37
+ function billedMicroCents(rawMicroCents) {
38
+ if (!Number.isFinite(rawMicroCents) || rawMicroCents <= 0) {
39
+ return 0;
40
+ }
41
+ return Math.ceil(rawMicroCents / exports.MICRO_CENTS_PER_CENT) * exports.MICRO_CENTS_PER_CENT;
42
+ }
43
+ function freshState() {
44
+ return {
45
+ approved: false,
46
+ limitCents: exports.SESSION_BUDGET_DEFAULT_CENTS,
47
+ ceilingMicroCents: exports.PER_CALL_CEILING_MICRO_CENTS,
48
+ settledMicroCents: 0,
49
+ pending: new Map(),
50
+ pendingVideos: new Map(),
51
+ };
52
+ }
53
+ let state = freshState();
54
+ function nonNegative(microCents) {
55
+ return Number.isFinite(microCents) && microCents > 0 ? microCents : 0;
56
+ }
57
+ function clampCents(cents, fallback) {
58
+ if (!Number.isFinite(cents))
59
+ return fallback;
60
+ return Math.min(exports.SESSION_BUDGET_MAX_CENTS, Math.max(1, Math.floor(cents)));
61
+ }
62
+ function dollars(microCents) {
63
+ return `$${(microCents / (exports.MICRO_CENTS_PER_CENT * 100)).toFixed(2)}`;
64
+ }
65
+ function pendingTotal() {
66
+ let sum = 0;
67
+ for (const v of state.pending.values())
68
+ sum += v;
69
+ return sum;
70
+ }
71
+ function budgetStatus() {
72
+ const limitMicroCents = state.limitCents * exports.MICRO_CENTS_PER_CENT;
73
+ const pendingMicroCents = pendingTotal();
74
+ return {
75
+ approved: state.approved,
76
+ limitCents: state.limitCents,
77
+ ceilingMicroCents: state.ceilingMicroCents,
78
+ settledMicroCents: state.settledMicroCents,
79
+ pendingMicroCents,
80
+ remainingMicroCents: Math.max(0, limitMicroCents - state.settledMicroCents - pendingMicroCents),
81
+ };
82
+ }
83
+ /**
84
+ * Approve a session budget. This is called only after the user confirms the
85
+ * budget handshake. The amount is clamped to [1, SESSION_BUDGET_MAX_CENTS].
86
+ * A fresh approval opens a fresh window: settled spend resets to 0.
87
+ * Outstanding reservations stay, because those calls may still bill.
88
+ * The per-call ceiling can also be set here. It is clamped the same way.
89
+ */
90
+ function approveBudget(cents, opts) {
91
+ state.approved = true;
92
+ state.limitCents = clampCents(cents, exports.SESSION_BUDGET_DEFAULT_CENTS);
93
+ state.settledMicroCents = 0;
94
+ if (opts?.ceilingCents !== undefined) {
95
+ state.ceilingMicroCents = clampCents(opts.ceilingCents, exports.PER_CALL_CEILING_CENTS) * exports.MICRO_CENTS_PER_CENT;
96
+ }
97
+ return budgetStatus();
98
+ }
99
+ /**
100
+ * Decide whether a spend call with this estimate may run.
101
+ *
102
+ * Order (a safety property, D-08):
103
+ * 1. over_ceiling estimate above the per-call ceiling, even when unapproved
104
+ * 2. needs_budget_handshake no budget approved yet
105
+ * 3. needs_budget_handshake settled actuals have reached the limit (the wall)
106
+ * 4. needs_budget_handshake estimate exceeds remaining headroom (including pending)
107
+ * 5. ok
108
+ */
109
+ function checkCall(estimateMicroCents, opts) {
110
+ const ceiling = opts?.ceilingMicroCents ?? state.ceilingMicroCents;
111
+ if (!Number.isFinite(estimateMicroCents)) {
112
+ return {
113
+ verdict: "over_ceiling",
114
+ reason: "This call could not be priced, so it must be confirmed on its own.",
115
+ };
116
+ }
117
+ const estimate = nonNegative(estimateMicroCents);
118
+ if (estimate > ceiling) {
119
+ return {
120
+ verdict: "over_ceiling",
121
+ reason: `This call is estimated at ${dollars(estimate)}, above the ${dollars(ceiling)} per-call ceiling. It must be confirmed on its own.`,
122
+ };
123
+ }
124
+ if (!state.approved) {
125
+ return {
126
+ verdict: "needs_budget_handshake",
127
+ reason: `No session budget is approved yet. Approve one (default ${dollars(exports.SESSION_BUDGET_DEFAULT_CENTS * exports.MICRO_CENTS_PER_CENT)}) before running spend tools.`,
128
+ };
129
+ }
130
+ const status = budgetStatus();
131
+ const limit = state.limitCents * exports.MICRO_CENTS_PER_CENT;
132
+ if (state.settledMicroCents >= limit) {
133
+ return {
134
+ verdict: "needs_budget_handshake",
135
+ reason: `The ${dollars(limit)} session budget is used up (${dollars(state.settledMicroCents)} settled). Approve a new budget to continue.`,
136
+ };
137
+ }
138
+ if (estimate > status.remainingMicroCents) {
139
+ return {
140
+ verdict: "needs_budget_handshake",
141
+ reason: `This call is estimated at ${dollars(estimate)}, but only ${dollars(status.remainingMicroCents)} of the ${dollars(limit)} session budget remains. Approve a new budget to continue.`,
142
+ };
143
+ }
144
+ return {
145
+ verdict: "ok",
146
+ reason: `Estimated ${dollars(estimate)}, within the remaining ${dollars(status.remainingMicroCents)} of the ${dollars(limit)} session budget.`,
147
+ };
148
+ }
149
+ /** Reserve an estimate before the network call. It reduces headroom until settled or released. */
150
+ function recordEstimate(callId, microCents) {
151
+ state.pending.set(callId, nonNegative(microCents));
152
+ }
153
+ /** Settle a call at its actual cost. Removes the reservation and adds the billed actual to settled spend. */
154
+ function reconcileActual(callId, actualMicroCents) {
155
+ state.pending.delete(callId);
156
+ dropVideoLinks(callId);
157
+ state.settledMicroCents += billedMicroCents(actualMicroCents);
158
+ }
159
+ /** Drop a reservation for a call that was never billed (error, cancel, failure). */
160
+ function releaseEstimate(callId) {
161
+ state.pending.delete(callId);
162
+ dropVideoLinks(callId);
163
+ }
164
+ function dropVideoLinks(callId) {
165
+ for (const [videoId, linked] of state.pendingVideos) {
166
+ if (linked === callId)
167
+ state.pendingVideos.delete(videoId);
168
+ }
169
+ }
170
+ /**
171
+ * Hold a video job's floor estimate under callId and map the video id to it,
172
+ * so a later read (deeprelay_videos_get / _list) can settle it.
173
+ */
174
+ function registerPendingVideo(videoId, callId, floorMicroCents) {
175
+ state.pending.set(callId, nonNegative(floorMicroCents));
176
+ state.pendingVideos.set(videoId, callId);
177
+ }
178
+ /**
179
+ * Settle a video reservation from an observed job status.
180
+ * completed -> reconcile at costCents (or the reserved floor if costCents is absent)
181
+ * failed | cancelled | expired -> release. These are never billed.
182
+ * queued | in_progress (or anything else) -> no change
183
+ */
184
+ function settleVideo(videoId, outcome) {
185
+ const callId = state.pendingVideos.get(videoId);
186
+ if (callId === undefined) {
187
+ return { settled: false };
188
+ }
189
+ switch (outcome.status) {
190
+ case "completed": {
191
+ const reserved = state.pending.get(callId) ?? 0;
192
+ const cost = outcome.costCents;
193
+ const actual = typeof cost === "number" && Number.isFinite(cost) && cost >= 0
194
+ ? cost * exports.MICRO_CENTS_PER_CENT
195
+ : reserved;
196
+ const billed = billedMicroCents(actual);
197
+ reconcileActual(callId, billed);
198
+ return { settled: true, microCents: billed };
199
+ }
200
+ case "failed":
201
+ case "cancelled":
202
+ case "expired":
203
+ releaseEstimate(callId);
204
+ return { settled: true, microCents: 0 };
205
+ default:
206
+ return { settled: false };
207
+ }
208
+ }
209
+ /**
210
+ * The video reservations still waiting on a terminal status. Reported by
211
+ * deeprelay_auth_status and used by the video reads to tell "still pending"
212
+ * apart from "never tracked by this session".
213
+ */
214
+ function pendingVideoReservations() {
215
+ const out = [];
216
+ for (const [videoId, callId] of state.pendingVideos) {
217
+ out.push({ videoId, microCents: state.pending.get(callId) ?? 0 });
218
+ }
219
+ return out;
220
+ }
221
+ /** Test-only: restore the fresh-process state. */
222
+ function resetBudgetStateForTests() {
223
+ state = freshState();
224
+ }
225
+ //# sourceMappingURL=budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.js","sourceRoot":"","sources":["../../src/guardrail/budget.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { ElicitationHost, QuoteResult } from "./types.js";
2
+ export type ElicitationAnswer = "confirmed" | "declined" | "unavailable";
3
+ /** True only when the connected client advertised the elicitation capability. */
4
+ export declare function clientSupportsElicitation(host?: ElicitationHost): boolean;
5
+ /** The text the user reads: summary, price (if any), expiry, then the question. */
6
+ export declare function buildPrompt(quote: QuoteResult, question: string): string;
7
+ /**
8
+ * Ask the user to confirm a quote. Executes nothing. Returns "unavailable" when
9
+ * the client cannot be asked or the ask fails, "confirmed" only for an explicit
10
+ * accept with `confirm === true`, and "declined" for everything else.
11
+ */
12
+ export declare function elicitConfirmation(host: ElicitationHost | undefined, quote: QuoteResult, question: string): Promise<ElicitationAnswer>;