@spendguard/sdk 0.5.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.
@@ -0,0 +1,1164 @@
1
+ import '@grpc/grpc-js';
2
+ import { Tracer } from '@opentelemetry/api';
3
+ import { E as Error$1, T as Timestamp, C as CommitSessionDeltaRequest$1, R as ReleaseSessionRequest$1, a as ReserveSessionRequest$1 } from './adapter-D9T3yEEw.js';
4
+
5
+ /** Default p99-ceiling for `requestDecision` round-trip; design §4.2. */
6
+ declare const DEFAULT_DECISION_TIMEOUT_MS: 250;
7
+ /** Default handshake deadline; design §4.2. */
8
+ declare const DEFAULT_HANDSHAKE_TIMEOUT_MS: 2000;
9
+ /** Default deadline for `confirmPublishOutcome` / `release`; design §4.2. */
10
+ declare const DEFAULT_PUBLISH_TIMEOUT_MS: 150;
11
+ /** Default deadline for `emitTraceEvents` ack loop; design §4.2. */
12
+ declare const DEFAULT_TRACE_TIMEOUT_MS: 500;
13
+ /** Default capability the adapter advertises (L3_POLICY_HOOK = 0x40); design §4.2. */
14
+ declare const DEFAULT_CAPABILITY_LEVEL: 64;
15
+ /** Default protocol version (design §4.2); only `1` is wire-supported in v0.1.x. */
16
+ declare const DEFAULT_PROTOCOL_VERSION: 1;
17
+ /**
18
+ * Lightweight span record passed to the `onSpan` observer hook. The hook
19
+ * receives one record per RPC the client makes. Used by adapters that want
20
+ * to integrate with their own tracing pipeline without pulling
21
+ * `@opentelemetry/api`.
22
+ */
23
+ interface SpanRecord {
24
+ /** RPC span name, e.g. `spendguard.reserve`. */
25
+ name: string;
26
+ /** Span start time in milliseconds since epoch. */
27
+ startTimeMs: number;
28
+ /** Span wall-clock duration in milliseconds. */
29
+ durationMs: number;
30
+ /** Span attributes (snake_case keys per OTel semantic conventions). */
31
+ attributes: Readonly<Record<string, string | number | boolean | undefined>>;
32
+ /** Set when the span completed with an error. */
33
+ error?: Error;
34
+ }
35
+ /**
36
+ * Constructor options for `SpendGuardClient`. This shape is part of the
37
+ * LOCKED public surface — design.md §4.2. Adapters in D04 / D06 / D08 / D29
38
+ * build directly against these fields; renames / removals require a v0.minor
39
+ * bump and a coordinated update of every adapter spec.
40
+ *
41
+ * Per design.md §5.2:
42
+ * - Explicit fields override env fallback.
43
+ * - Required (no default + no env) fields throw `SpendGuardConfigError` at
44
+ * constructor time.
45
+ *
46
+ * Two SLICE 3 deviations from design.md §4.2 worth noting:
47
+ * - `socketPath` is *optional* on this interface even though design §4.2
48
+ * marks it `required in v0.1.x`. The reason: design §5.2 requires env
49
+ * fallback to fill it, which only the constructor can do. The validator
50
+ * enforces presence post-merge.
51
+ * - `tenantId` is similarly optional here, required post-merge.
52
+ */
53
+ interface SpendGuardClientConfig {
54
+ /** UDS path the sidecar listens on. Env fallback: `SPENDGUARD_SOCKET_PATH` then `SPENDGUARD_SIDECAR_UDS`. */
55
+ socketPath?: string;
56
+ /** Tenant id assertion the sidecar verifies at handshake time. Env fallback: `SPENDGUARD_TENANT_ID`. */
57
+ tenantId?: string;
58
+ /** Runtime kind tag e.g. `"langchain-js"` / `"vercel-ai-sdk"`; default `""`. */
59
+ runtimeKind?: string;
60
+ /** Optional runtime version tag (caller's own SDK version). */
61
+ runtimeVersion?: string;
62
+ /** Override the SDK version reported on the wire. Defaults to the package's `VERSION`. */
63
+ sdkVersion?: string;
64
+ /** Adapter↔sidecar protocol version; only `1` is wire-supported in v0.1.x. */
65
+ protocolVersion?: number;
66
+ /** Capability the adapter advertises; default `0x40` (L3_POLICY_HOOK). */
67
+ capabilityLevel?: number;
68
+ /** Stable workload-instance id; env fallback: `SPENDGUARD_WORKLOAD_INSTANCE_ID`. */
69
+ workloadInstanceId?: string;
70
+ /** Per-decision RPC deadline in ms; default 250; env fallback: `SPENDGUARD_DECISION_TIMEOUT_MS`. */
71
+ decisionTimeoutMs?: number;
72
+ /** Handshake RPC deadline in ms; default 2_000; env fallback: `SPENDGUARD_HANDSHAKE_TIMEOUT_MS`. */
73
+ handshakeTimeoutMs?: number;
74
+ /** Publish-side RPC deadline in ms; default 150. */
75
+ publishTimeoutMs?: number;
76
+ /** Trace-event RPC deadline in ms; default 500. */
77
+ traceTimeoutMs?: number;
78
+ /**
79
+ * Observer hook invoked once per RPC. Mutually exclusive with `otelTracer`
80
+ * (constructor throws `SpendGuardConfigError` when both are set).
81
+ * Wired in SLICE 8.
82
+ */
83
+ onSpan?: (span: SpanRecord) => void;
84
+ /**
85
+ * OTel Tracer. When provided, the client emits spans through it instead of
86
+ * `onSpan`. Mutually exclusive with `onSpan`.
87
+ * `@opentelemetry/api` is a `peerDependenciesMeta.optional` dep — adapters
88
+ * that never enable OTel pay zero dep cost. Wired in SLICE 8.
89
+ */
90
+ otelTracer?: Tracer;
91
+ /**
92
+ * Forward-reserved transport selector. v0.1.x supports only `"uds-grpc"`;
93
+ * the literal type carries the slot for a future ASP HTTP gateway transport
94
+ * (design §9 decision 10) without a v0.minor bump.
95
+ */
96
+ runtime?: "uds-grpc";
97
+ /**
98
+ * Test-only short-circuit: when set, every RPC method returns a no-op
99
+ * outcome without contacting the sidecar. **For tests only** — production
100
+ * users who set this and forget have silently lost enforcement. See
101
+ * design.md §5.1 `SPENDGUARD_DISABLE` for the env fallback. Wired in SLICE 4.
102
+ */
103
+ disabled?: boolean;
104
+ /**
105
+ * Default `run_projection` policy when callers do not pass one. Slice-doc
106
+ * addition; env fallback: `SPENDGUARD_RUN_PROJECTION_DEFAULT`. SLICE 4
107
+ * wires consumption: when set to a non-empty value, the resolved policy
108
+ * is folded into the `DecisionRequest.inputs.runtime_metadata` under the
109
+ * `run_projection_policy` key so the sidecar's projector observes the
110
+ * default per design.md §4.2 R2 amendment.
111
+ */
112
+ runProjectionDefault?: RunProjectionPolicy;
113
+ /**
114
+ * In-process idempotency cache. When set, `reserve()` consults this cache
115
+ * before issuing the sidecar RPC: a hit short-circuits to the cached
116
+ * `DecisionOutcome`. SLICE 8 wires the consumption; see
117
+ * `InMemoryIdempotencyCache` / `NoopIdempotencyCache` from
118
+ * `@spendguard/sdk/cache` for ready-made impls. When `undefined`,
119
+ * `reserve()` issues every RPC unconditionally (the sidecar is still the
120
+ * correctness gate via its own idempotency-key dedup).
121
+ */
122
+ idempotencyCache?: IdempotencyCache;
123
+ }
124
+ /**
125
+ * Default `run_projection` policy. Forward-extensible string-literal union per
126
+ * design.md §4.2 R2 amendment + COV_S05_03 R2 commitment MJ-1.
127
+ *
128
+ * v0.1.x ships:
129
+ * - `""` — leave policy unset; rely on contract-bundle default.
130
+ * - `"STRICT_CEILING"` — ASP Draft-01 strict-ceiling policy.
131
+ * - `"ELASTIC"` — ASP Draft-01 elastic policy.
132
+ *
133
+ * The third member `(string & {})` is the standard TS literal-string escape
134
+ * hatch: adapters can pass policy names that land in a future contract DSL
135
+ * bump without forcing a v0.minor on the SDK, while preserving completion
136
+ * suggestions for the two named members above.
137
+ */
138
+ type RunProjectionPolicy = "" | "STRICT_CEILING" | "ELASTIC" | (string & {});
139
+ /**
140
+ * **LOCKED §4.1 spec name** for the constructor options. Identical to
141
+ * `SpendGuardClientConfig` — this alias preserves the symbol that
142
+ * design.md §4.1 line 45 enumerates as part of the consumer-facing barrel
143
+ * import for D04 / D06 / D08 / D29. The slice doc named the file-internal
144
+ * shape `SpendGuardClientConfig`; the spec name `SpendGuardClientOptions`
145
+ * is the cross-deliverable contract. Both resolve to the same shape so
146
+ * that `import { type SpendGuardClientOptions } from "@spendguard/sdk"`
147
+ * (the form every adapter spec uses) compiles unchanged.
148
+ *
149
+ * Added in COV_S05_03 R2 to close the spec-vs-slice-doc rename drift.
150
+ */
151
+ type SpendGuardClientOptions = SpendGuardClientConfig;
152
+ /**
153
+ * Resolved config: every field that the SLICE 3 constructor promises to fill
154
+ * is non-optional here. The runtime validator narrows `SpendGuardClientConfig`
155
+ * to this stricter shape after merging env fallback + defaults.
156
+ */
157
+ interface ResolvedConfig {
158
+ socketPath: string;
159
+ tenantId: string;
160
+ runtimeKind: string;
161
+ runtimeVersion: string;
162
+ sdkVersion: string;
163
+ protocolVersion: number;
164
+ capabilityLevel: number;
165
+ workloadInstanceId: string;
166
+ decisionTimeoutMs: number;
167
+ handshakeTimeoutMs: number;
168
+ publishTimeoutMs: number;
169
+ traceTimeoutMs: number;
170
+ onSpan?: (span: SpanRecord) => void;
171
+ otelTracer?: Tracer;
172
+ idempotencyCache?: IdempotencyCache;
173
+ runtime: "uds-grpc";
174
+ disabled: boolean;
175
+ runProjectionDefault: RunProjectionPolicy;
176
+ }
177
+
178
+ type SessionCommitOutcome = "SUCCESS" | "PROVIDER_ERROR" | "CLIENT_TIMEOUT" | "RUN_ABORTED";
179
+ declare const DEFAULT_MAX_PENDING_SESSION_DELTAS = 64;
180
+ interface ReserveSessionRequest {
181
+ tenantId: string;
182
+ budgetId: string;
183
+ windowInstanceId: string;
184
+ unit: UnitRef;
185
+ pricing: PricingFreeze;
186
+ sessionId: string;
187
+ route: string;
188
+ estimatedAmountAtomic: string;
189
+ ttlSeconds: number;
190
+ idempotencyKey: string;
191
+ }
192
+ interface CommitSessionDeltaRequest {
193
+ sessionReservationId: string;
194
+ streamingCommitId: string;
195
+ amountAtomicDelta: string;
196
+ outcome: SessionCommitOutcome;
197
+ eventTime: Date | number | Timestamp;
198
+ idempotencyKey: string;
199
+ }
200
+ interface ReleaseSessionRequest {
201
+ sessionReservationId: string;
202
+ reasonCode: string;
203
+ eventTime: Date | number | Timestamp;
204
+ idempotencyKey: string;
205
+ }
206
+ type ReserveSessionOutcome = {
207
+ kind: "accepted";
208
+ sessionReservationId: string;
209
+ ledgerTransactionId: string;
210
+ auditSessionEventId: string;
211
+ ttlExpiresAt: Date | null;
212
+ reservedAmountAtomic: string;
213
+ remainingAmountAtomic: string;
214
+ } | {
215
+ kind: "denied";
216
+ auditSessionEventId: string;
217
+ reasonCodes: readonly string[];
218
+ matchedRuleIds: readonly string[];
219
+ error?: Error$1;
220
+ };
221
+ interface CommitSessionDeltaOutcome {
222
+ sessionReservationId: string;
223
+ streamingCommitId: string;
224
+ ledgerTransactionId: string;
225
+ auditSessionEventId: string;
226
+ committedDeltaAtomic: string;
227
+ cumulativeCommittedAtomic: string;
228
+ remainingAmountAtomic: string;
229
+ recordedAt: Date | null;
230
+ }
231
+ interface ReleaseSessionOutcome {
232
+ sessionReservationId: string;
233
+ ledgerTransactionId: string;
234
+ auditSessionEventId: string;
235
+ releasedAmountAtomic: string;
236
+ committedAmountAtomic: string;
237
+ recordedAt: Date | null;
238
+ }
239
+ interface SessionDeltaCommitInput {
240
+ amountAtomicDelta: string;
241
+ outcome: SessionCommitOutcome;
242
+ eventTime: Date | number | Timestamp;
243
+ idempotencyKey?: string;
244
+ }
245
+ interface SessionReleaseInput {
246
+ reasonCode: string;
247
+ eventTime: Date | number | Timestamp;
248
+ idempotencyKey: string;
249
+ }
250
+ interface PendingSessionDelta {
251
+ sequence: number;
252
+ request: CommitSessionDeltaRequest;
253
+ }
254
+ interface SessionReservationHandleOptions {
255
+ sessionReservationId: string;
256
+ nextStreamingCommitSequence?: number;
257
+ maxPendingDeltas?: number;
258
+ pendingDeltas?: readonly PendingSessionDelta[];
259
+ released?: boolean;
260
+ }
261
+ interface SessionReservationHandleSnapshot {
262
+ sessionReservationId: string;
263
+ nextStreamingCommitSequence: number;
264
+ maxPendingDeltas: number;
265
+ released: boolean;
266
+ pendingDeltas: readonly PendingSessionDelta[];
267
+ }
268
+ interface SessionDeltaCommitClient {
269
+ commitSessionDelta(req: CommitSessionDeltaRequest): Promise<CommitSessionDeltaOutcome>;
270
+ }
271
+ interface SessionReleaseClient {
272
+ releaseSession(req: ReleaseSessionRequest): Promise<ReleaseSessionOutcome>;
273
+ }
274
+ declare class SessionReservationHandleError extends Error {
275
+ constructor(message: string);
276
+ }
277
+ declare class SessionPendingDeltaLimitError extends SessionReservationHandleError {
278
+ constructor(maxPendingDeltas: number);
279
+ }
280
+ declare class SessionReservationReleasedError extends SessionReservationHandleError {
281
+ constructor(sessionReservationId: string);
282
+ }
283
+ declare class SessionReservationReplayMismatchError extends SessionReservationHandleError {
284
+ constructor(message: string);
285
+ }
286
+ declare class SessionReservationHandle {
287
+ readonly sessionReservationId: string;
288
+ readonly maxPendingDeltas: number;
289
+ private nextStreamingCommitSequenceValue;
290
+ private pendingDeltaBuffer;
291
+ private releasedValue;
292
+ constructor(options: SessionReservationHandleOptions);
293
+ static fromSnapshot(snapshot: SessionReservationHandleSnapshot): SessionReservationHandle;
294
+ get nextStreamingCommitSequence(): number;
295
+ get released(): boolean;
296
+ get pendingDeltas(): readonly PendingSessionDelta[];
297
+ snapshot(): SessionReservationHandleSnapshot;
298
+ enqueueDelta(input: SessionDeltaCommitInput): PendingSessionDelta;
299
+ commitDelta(client: SessionDeltaCommitClient, input: SessionDeltaCommitInput): Promise<CommitSessionDeltaOutcome>;
300
+ replayPending(client: SessionDeltaCommitClient): Promise<CommitSessionDeltaOutcome[]>;
301
+ release(client: SessionReleaseClient, input: SessionReleaseInput): Promise<ReleaseSessionOutcome>;
302
+ private ackOutcome;
303
+ private assertOpen;
304
+ }
305
+ declare function buildReserveSessionRequest(req: ReserveSessionRequest): ReserveSessionRequest$1;
306
+ declare function buildCommitSessionDeltaRequest(req: CommitSessionDeltaRequest): CommitSessionDeltaRequest$1;
307
+ declare function buildReleaseSessionRequest(req: ReleaseSessionRequest): ReleaseSessionRequest$1;
308
+
309
+ /**
310
+ * Outcome of a successful handshake. SLICE 4 populates this; SLICE 3 only
311
+ * locks the type so `sessionId` / `handshakeOutcome` getters compile.
312
+ */
313
+ interface HandshakeOutcome {
314
+ sessionId: string;
315
+ sidecarVersion: string;
316
+ schemaBundleId: string;
317
+ schemaBundleHash: Uint8Array;
318
+ contractBundleId: string;
319
+ contractBundleHash: Uint8Array;
320
+ capabilityRequired: number;
321
+ /**
322
+ * Key id the sidecar reports for `announcementSignature`. **Pass-through
323
+ * only in v0.1.x — NOT verified by the SDK.** See `announcementSignature`.
324
+ */
325
+ signingKeyId: string;
326
+ /**
327
+ * Sidecar-supplied announcement signature.
328
+ *
329
+ * **PASS-THROUGH ONLY (v0.1.x): these bytes are surfaced verbatim and are
330
+ * NOT cryptographically verified by the SDK.** Trust rests entirely on the
331
+ * local UDS transport: the sidecar enforces the kernel `SO_PEERCRED`
332
+ * peer-credential check, which is the trust anchor under the documented
333
+ * local-socket threat model. The SDK does not (yet) verify
334
+ * `announcementSignature` against a pinned `signingKeyId` / control-plane
335
+ * public key.
336
+ *
337
+ * Pinned-key verification is deferred to the future non-local transport
338
+ * (the forward-reserved `runtime: "fetch"` / TLS path), where the UDS peer-
339
+ * cred anchor no longer applies. Until then, callers MUST NOT treat a
340
+ * non-empty `announcementSignature` as an independent proof of sidecar
341
+ * authenticity. Empty when the sidecar omits it.
342
+ */
343
+ announcementSignature: Uint8Array;
344
+ }
345
+ interface UnitRef {
346
+ unit: string;
347
+ denomination: number;
348
+ /** Canonical-truth UUID of the ledger unit row.
349
+ *
350
+ * When provided, the SDK threads it verbatim onto `BudgetClaim.unit.unit_id`
351
+ * on the wire. When omitted, the SDK sends "" and the ledger reject with
352
+ * `INVALID_REQUEST: claim[N].unit.unit_id empty`.
353
+ *
354
+ * Adapters that issue ledger-backed `client.reserve()` MUST provide
355
+ * `unitId`. Recipe-style integrations (where no ledger reserve happens) MAY
356
+ * omit. The most common operator path is to set this from the
357
+ * `SPENDGUARD_UNIT_ID` env var at adapter construction time.
358
+ *
359
+ * NB: this is the ledger UUID, distinct from the free-form `unit` slug —
360
+ * they are NOT interchangeable. Multiple unit slugs can resolve to the same
361
+ * unit_id when migration aliasing is configured.
362
+ */
363
+ unitId?: string;
364
+ }
365
+ interface BudgetClaim {
366
+ scopeId: string;
367
+ amountAtomic: string;
368
+ unit: UnitRef;
369
+ /** Canonical-truth UUID of the ledger window-instance row.
370
+ *
371
+ * When provided, the SDK threads it verbatim onto
372
+ * `BudgetClaim.window_instance_id` on the wire. When omitted, the SDK sends
373
+ * "" and the ledger rejects with
374
+ * `INVALID_REQUEST: claim[N].window_instance_id empty`.
375
+ *
376
+ * Adapters that issue ledger-backed `client.reserve()` MUST provide
377
+ * `windowInstanceId`. Recipe-style integrations (where no ledger reserve
378
+ * happens) MAY omit. The most common operator path is to set this from the
379
+ * `SPENDGUARD_WINDOW_INSTANCE_ID` env var at adapter construction time.
380
+ *
381
+ * Mirrors the HARDEN_D05_UR `UnitRef.unitId` broadening — same disease,
382
+ * same additive backward-compatible cure (HARDEN_D05_WI).
383
+ */
384
+ windowInstanceId?: string;
385
+ }
386
+ interface PricingFreeze {
387
+ pricingVersion: string;
388
+ pricingHash: Uint8Array;
389
+ /** HARDEN_D05_WI — optional FX rate version of the pricing freeze tuple.
390
+ * When omitted the SDK sends "" (pre-HARDEN wire shape). Ledger commit
391
+ * validation compares the FULL tuple against the reservation's freeze, so
392
+ * adapters whose reservations carry a non-empty fx version MUST thread it. */
393
+ fxRateVersion?: string;
394
+ /** HARDEN_D05_WI — optional unit-conversion version (see fxRateVersion). */
395
+ unitConversionVersion?: string;
396
+ }
397
+ interface ClaimEstimate {
398
+ tokenizerTier?: "T1" | "T2" | "T3" | "";
399
+ tokenizerVersionId?: string;
400
+ inputTokens?: number | bigint;
401
+ predictedATokens?: number | bigint;
402
+ predictedBTokens?: number | bigint;
403
+ predictedCTokens?: number | bigint;
404
+ reservedStrategy?: "A" | "B" | "C" | "";
405
+ predictionStrategyUsed?: "A" | "B" | "C" | "";
406
+ predictionPolicyUsed?: string;
407
+ predictionConfidence?: number;
408
+ predictionSampleSize?: number | bigint;
409
+ coldStartLayerUsed?: "L1" | "L2" | "L3" | "L4" | "";
410
+ classifierVersion?: string;
411
+ fingerprintVersion?: string;
412
+ promptClassFingerprint?: string;
413
+ runProjectionAtDecisionAtomic?: number | bigint;
414
+ runPredictedRemainingSteps?: number;
415
+ runStepsCompletedSoFar?: number | bigint;
416
+ runCodeTriggered?: string;
417
+ model?: string;
418
+ promptClass?: string;
419
+ }
420
+ interface ReserveRequest {
421
+ trigger: "RUN_PRE" | "AGENT_STEP_PRE" | "LLM_CALL_PRE" | "TOOL_CALL_PRE";
422
+ runId: string;
423
+ stepId: string;
424
+ llmCallId: string;
425
+ toolCallId?: string;
426
+ decisionId: string;
427
+ route: string;
428
+ projectedClaims: BudgetClaim[];
429
+ idempotencyKey: string;
430
+ traceparent?: string;
431
+ tracestate?: string;
432
+ parentRunId?: string;
433
+ budgetGrantJti?: string;
434
+ projectedP50Atomic?: string;
435
+ projectedP90Atomic?: string;
436
+ projectedP95Atomic?: string;
437
+ projectedP99Atomic?: string;
438
+ projectedUnit?: UnitRef;
439
+ promptText?: string;
440
+ decisionContextJson?: Record<string, unknown>;
441
+ claimEstimate?: ClaimEstimate;
442
+ }
443
+ interface DecisionOutcome {
444
+ decisionId: string;
445
+ auditDecisionEventId: string;
446
+ decision: "CONTINUE" | "DEGRADE";
447
+ mutationPatchJson: string;
448
+ effectHash: Uint8Array;
449
+ ledgerTransactionId: string;
450
+ reservationIds: readonly string[];
451
+ ttlExpiresAtSeconds: number;
452
+ reasonCodes: readonly string[];
453
+ matchedRuleIds: readonly string[];
454
+ }
455
+ interface CommitEstimatedRequest {
456
+ runId: string;
457
+ stepId: string;
458
+ llmCallId: string;
459
+ decisionId: string;
460
+ reservationId: string;
461
+ estimatedAmountAtomic: string;
462
+ unit: UnitRef;
463
+ pricing: PricingFreeze;
464
+ providerEventId: string;
465
+ outcome: "SUCCESS" | "PROVIDER_ERROR" | "CLIENT_TIMEOUT" | "RUN_ABORTED";
466
+ actualInputTokens?: number;
467
+ actualOutputTokens?: number;
468
+ deltaBRatio?: number;
469
+ deltaCRatio?: number;
470
+ traceparent?: string;
471
+ tracestate?: string;
472
+ providerResponseMetadata?: string;
473
+ outcomeKind?: "SUCCESS" | "FAILURE";
474
+ /**
475
+ * `int64`-as-string, mirroring SLICE 4 `actualInputTokens` semantics for
476
+ * the multi-event path. When `outcomeKind` is set and this is provided,
477
+ * the outcome event carries the value verbatim; when omitted on the
478
+ * outcome event, the SDK reuses `actualInputTokens` from this request
479
+ * (avoids a double-spec for callers that already populate the SLICE 4
480
+ * field).
481
+ */
482
+ actualInputTokensWire?: string;
483
+ /** `int64`-as-string companion of `actualOutputTokens` (see above). */
484
+ actualOutputTokensWire?: string;
485
+ /**
486
+ * Free-form error message threaded onto the outcome event's
487
+ * `providerResponseMetadata` JSON envelope as `{"error_message": "..."}`.
488
+ * Only consulted when `outcomeKind === "FAILURE"`; ignored on SUCCESS.
489
+ */
490
+ actualErrorMessage?: string;
491
+ }
492
+ interface ReleaseRequest {
493
+ reservationId: string;
494
+ idempotencyKey: string;
495
+ reasonCodes?: readonly string[];
496
+ workloadInstanceId?: string;
497
+ tenantId?: string;
498
+ }
499
+ interface ReleaseOutcome {
500
+ /**
501
+ * Audit-chain signature for the release event, as reported by the sidecar.
502
+ *
503
+ * **PASS-THROUGH ONLY (v0.1.x): NOT cryptographically verified by the SDK.**
504
+ * Like `HandshakeOutcome.announcementSignature`, trust rests on the local
505
+ * UDS `SO_PEERCRED` peer-credential anchor; these are unvalidated bytes the
506
+ * adapter may forward but MUST NOT treat as independent proof. Pinned-key
507
+ * verification is deferred to the future non-local (TLS) transport. Empty
508
+ * when the sidecar omits it.
509
+ */
510
+ auditEventSignature: Uint8Array;
511
+ ledgerTransactionId: string;
512
+ releasedReservationIds: readonly string[];
513
+ }
514
+ interface QueryBudgetRequest {
515
+ scopeId: string;
516
+ asOfSeconds?: number;
517
+ }
518
+ interface QueryBudgetResult {
519
+ availableAtomic: string;
520
+ reservedAtomic: string;
521
+ committedAtomic: string;
522
+ unit: UnitRef;
523
+ asOfSeconds: number;
524
+ }
525
+ interface PublishOutcomeRequest {
526
+ decisionId: string;
527
+ effectHash: Uint8Array;
528
+ outcome: "APPLIED" | "APPLIED_NOOP" | "APPLY_FAILED" | "APPROVAL_GRANTED" | "APPROVAL_DENIED" | "APPROVAL_TIMED_OUT";
529
+ adapterError?: string;
530
+ }
531
+ interface ApplyFailedRequest {
532
+ decisionId: string;
533
+ effectHash: Uint8Array;
534
+ adapterError: string;
535
+ }
536
+ interface ResumeAfterApprovalRequest {
537
+ approvalId: string;
538
+ tenantId: string;
539
+ decisionId: string;
540
+ workloadInstanceId?: string;
541
+ }
542
+ /** Alias for `CommitEstimatedRequest` exposed for the lower-level entry point. */
543
+ type EmitLlmCallPostRequest = CommitEstimatedRequest;
544
+ /**
545
+ * Async gRPC client for the SpendGuard sidecar over a Unix Domain Socket.
546
+ *
547
+ * SLICE 3 wired lifecycle + UDS transport; SLICE 4 wires handshake / reserve /
548
+ * commitEstimated bodies; SLICE 5 wires release / queryBudget. Adapters (D04 /
549
+ * D06 / D08 / D29) build against the LOCKED §4.2 surface and treat this class
550
+ * as their primary integration point.
551
+ *
552
+ * Usage:
553
+ *
554
+ * await using client = SpendGuardClient.fromEnv();
555
+ * await client.connect();
556
+ * const handshake = await client.handshake();
557
+ * const decision = await client.reserve({ ... });
558
+ * await client.commitEstimated({ ... });
559
+ * // `await using` runs `[Symbol.asyncDispose]` here → graceful close
560
+ *
561
+ * @example Test-only short-circuit (per design.md §5.1)
562
+ *
563
+ * // SPENDGUARD_DISABLE=1 in env, or:
564
+ * const client = new SpendGuardClient({
565
+ * socketPath: "/dev/null",
566
+ * tenantId: "test",
567
+ * disabled: true,
568
+ * });
569
+ * // Every RPC returns a no-op outcome; no UDS contact. **TESTS ONLY** —
570
+ * // a forgotten production setting silently loses enforcement.
571
+ */
572
+ declare class SpendGuardClient implements AsyncDisposable {
573
+ /** Frozen, merged + validated configuration. */
574
+ private readonly cfg;
575
+ /** Active gRPC transport, or `null` before `connect()` / after `close()`. */
576
+ private transport;
577
+ /** Active SidecarAdapter gRPC client; mirrors `transport` lifetime. */
578
+ private adapterClient;
579
+ /** Cached handshake outcome; first `handshake()` populates, subsequent reads reuse. */
580
+ private handshakeResult;
581
+ /**
582
+ * Coalesces concurrent `handshake()` callers into a single in-flight RPC.
583
+ * Mirrors Python `self._handshake_lock` in `client.py`. Stays non-null only
584
+ * while a handshake RPC is pending; cleared after success or failure so a
585
+ * post-failure retry can re-enter.
586
+ */
587
+ private handshakeInFlight;
588
+ /**
589
+ * Construct a client. Per design.md §5.2: explicit options win over env
590
+ * fallback; required fields without either throw `SpendGuardConfigError`
591
+ * immediately.
592
+ */
593
+ constructor(rawOpts?: SpendGuardClientConfig);
594
+ /**
595
+ * Wrap an RPC body in the configured observability hook(s). Threads BOTH
596
+ * `cfg.otelTracer` and `cfg.onSpan` into `withOtelSpan` so every RPC site
597
+ * gets span emission uniformly — the documented `onSpan` observer (the
598
+ * no-OTel-dep path) is invoked once per RPC, fixing the prior drift where
599
+ * `onSpan` was stored but never called. The two are mutually exclusive at
600
+ * config-validation time, so at most one fires.
601
+ */
602
+ private withSpan;
603
+ /**
604
+ * Convenience factory that reads required config from env vars and falls
605
+ * back to `/var/run/spendguard/adapter.sock` when `SPENDGUARD_SOCKET_PATH`
606
+ * is unset.
607
+ *
608
+ * Env vars consumed:
609
+ * - `SPENDGUARD_SOCKET_PATH` (slice-doc alias) / `SPENDGUARD_SIDECAR_UDS`
610
+ * (design §5.1) — UDS path; defaults to `/var/run/spendguard/adapter.sock`.
611
+ * - `SPENDGUARD_TENANT_ID` — **required**; throws `SpendGuardConfigError`
612
+ * when unset.
613
+ * - `SPENDGUARD_RUN_PROJECTION_DEFAULT` — optional default
614
+ * `run_projection` policy name; SLICE 4 wires consumption.
615
+ * - `SPENDGUARD_WORKLOAD_INSTANCE_ID` / `SPENDGUARD_DECISION_TIMEOUT_MS`
616
+ * / `SPENDGUARD_HANDSHAKE_TIMEOUT_MS` / `SPENDGUARD_DISABLE` — optional
617
+ * per design §5.1.
618
+ *
619
+ * Extra options provided as the `overrides` argument win over env per
620
+ * design.md §5.2.
621
+ *
622
+ * @throws SpendGuardConfigError when `SPENDGUARD_TENANT_ID` is missing.
623
+ */
624
+ static fromEnv(overrides?: SpendGuardClientConfig): SpendGuardClient;
625
+ /**
626
+ * Open the UDS gRPC channel. Idempotent — a second call when already
627
+ * connected is a no-op.
628
+ *
629
+ * Per design.md §6.3 / Python `client.py:240-251`, the `unix:` URI scheme
630
+ * is used and `grpc.default_authority=localhost` is set so the tonic-based
631
+ * sidecar accepts the HTTP/2 `:authority` pseudo-header. Without this
632
+ * channel option, tonic resets every stream with `PROTOCOL_ERROR`.
633
+ *
634
+ * In disabled mode (`SPENDGUARD_DISABLE=1` or `disabled: true`), no
635
+ * transport is opened — the call returns immediately. Subsequent RPCs
636
+ * short-circuit to no-op outcomes in SLICE 4.
637
+ *
638
+ * @throws SpendGuardConnectionError when the underlying transport could
639
+ * not be opened (e.g. malformed socket path).
640
+ */
641
+ connect(): Promise<void>;
642
+ /**
643
+ * Graceful close. Idempotent — calling `close()` twice (or `close()` before
644
+ * `connect()`) does not throw.
645
+ *
646
+ * On success the transport is dropped; the next `connect()` allocates a new
647
+ * channel. In-flight RPCs may complete with a `CANCELLED` status; SLICE 8
648
+ * adds the grace-period drain semantics that mirror the Python SDK's
649
+ * `await ch.close(grace=0.5)` path.
650
+ */
651
+ close(): Promise<void>;
652
+ /**
653
+ * ESM 2024 `await using` hook. Equivalent to `await this.close()`.
654
+ *
655
+ * Usage:
656
+ *
657
+ * await using client = new SpendGuardClient({ ... });
658
+ * // ... use client ...
659
+ * // [Symbol.asyncDispose] runs here automatically.
660
+ */
661
+ [Symbol.asyncDispose](): Promise<void>;
662
+ /** The tenant id this client asserted at construction. Stable for the client's lifetime. */
663
+ get tenantId(): string;
664
+ /**
665
+ * The negotiated session id. Throws `HandshakeError` until `handshake()`
666
+ * has completed (SLICE 4 wires the handshake; until then the getter is
667
+ * effectively unusable, which is intentional — adapters should call
668
+ * `handshake()` before reading state).
669
+ *
670
+ * @throws HandshakeError before `handshake()` completes.
671
+ */
672
+ get sessionId(): string;
673
+ /** Full handshake outcome. Throws `HandshakeError` until `handshake()` completes. */
674
+ get handshakeOutcome(): HandshakeOutcome;
675
+ /** Whether the client is currently connected to the sidecar. */
676
+ get isConnected(): boolean;
677
+ /** Frozen view of the resolved configuration. Useful for tests + debugging. */
678
+ get config(): Readonly<ResolvedConfig>;
679
+ /**
680
+ * Mandatory initial handshake. Idempotent — a second call returns the cached
681
+ * outcome without re-issuing the RPC (design.md §4.5). Concurrent callers
682
+ * are coalesced into the same in-flight RPC via `handshakeInFlight`.
683
+ *
684
+ * Disabled mode (`SPENDGUARD_DISABLE=1` / `disabled: true`) short-circuits
685
+ * to a synthetic `HandshakeOutcome` so unit tests can run without a real
686
+ * sidecar (`makeDisabledHandshake`).
687
+ *
688
+ * @throws HandshakeError on protocol-version mismatch or insufficient
689
+ * capability advertisement.
690
+ * @throws SidecarUnavailable on UNAVAILABLE / DEADLINE_EXCEEDED / CANCELLED.
691
+ * @throws SpendGuardError on any other gRPC failure surface.
692
+ */
693
+ handshake(opts?: {
694
+ workloadInstanceId?: string;
695
+ }): Promise<HandshakeOutcome>;
696
+ /**
697
+ * Internal: issue the real Handshake RPC and map the response.
698
+ *
699
+ * Splits out from `handshake()` so the idempotency guard there stays
700
+ * obviously correct: `doHandshake` never reads `handshakeResult` itself;
701
+ * it only writes it on success.
702
+ */
703
+ private doHandshake;
704
+ /**
705
+ * Run a `*.pre` decision boundary through the sidecar. Equivalent to the
706
+ * Python SDK's `request_decision` (design.md §4.7).
707
+ *
708
+ * The wire shape is built in `buildDecisionRequest()` and consumes:
709
+ * - the cached handshake `sessionId` (auto-handshakes on first use),
710
+ * - the caller-supplied `idempotencyKey` (REQUIRED — see design §6.5),
711
+ * - `runProjectionDefault` from config when the caller did not pass
712
+ * one in `decisionContextJson.run_projection_policy` (closes MJ-1).
713
+ *
714
+ * The response is mapped through `mapDecisionResponse()`: CONTINUE / DEGRADE
715
+ * return a `DecisionOutcome`; STOP / STOP_RUN_PROJECTION / SKIP /
716
+ * REQUIRE_APPROVAL raise the matching typed exception so adapters can route
717
+ * on `instanceof DecisionDenied` (and its subclasses) per review-standards §5.
718
+ *
719
+ * @throws DecisionStopped on STOP / STOP_RUN_PROJECTION.
720
+ * @throws DecisionSkipped on SKIP.
721
+ * @throws ApprovalRequired on REQUIRE_APPROVAL — `await err.resume(client)`
722
+ * surfaces the operator decision.
723
+ * @throws DecisionDenied on an unknown decision enum.
724
+ * @throws SidecarUnavailable on UNAVAILABLE / DEADLINE_EXCEEDED / CANCELLED.
725
+ * @throws SpendGuardError on any other gRPC failure surface.
726
+ */
727
+ reserve(req: ReserveRequest): Promise<DecisionOutcome>;
728
+ /**
729
+ * Alias for `reserve()` — identical function reference (review-standards §1.5
730
+ * P0 BLOCKER). The Python SDK exposes the symbol as `request_decision`; the
731
+ * TS surface keeps `reserve` as the canonical name AND exposes
732
+ * `requestDecision` so cross-language docs work without surprise.
733
+ *
734
+ * Implemented as an instance-field initializer that reads `this.reserve`
735
+ * during construction. The dot-lookup on `this.reserve` (inside the field
736
+ * initializer, before any instance shadow exists) resolves to the prototype
737
+ * method `SpendGuardClient.prototype.reserve`. Assigning it to the field
738
+ * makes `client.requestDecision === client.reserve` Boolean-true at runtime
739
+ * (both resolve to the same prototype function reference).
740
+ *
741
+ * NOTE on `.bind(this)`: implementation.md §4 line 581 sketches the field
742
+ * as `this.reserve.bind(this)`. The literal `bind` would produce a NEW
743
+ * function object and break the §1.5 identity gate; the constraint cited
744
+ * by the slice doc (review-standards §1.5 P0 BLOCKER) wins, so we drop
745
+ * `.bind(this)`. Callers always invoke as `client.requestDecision(req)`
746
+ * (method-call form) which preserves `this` via JS dispatch semantics —
747
+ * the bind was over-specification for the Pythonic detached-method
748
+ * pattern, which the TS SDK does not advertise.
749
+ *
750
+ * NOTE: do NOT add a JSDoc `@throws` block here — TypeScript erases JSDoc
751
+ * from runtime fields and the identity invariant is the primary contract
752
+ * this declaration enforces.
753
+ */
754
+ readonly requestDecision: SpendGuardClient["reserve"];
755
+ /**
756
+ * Commit an estimated LLM-call outcome. Equivalent to the Python SDK's
757
+ * `emit_llm_call_post` with `estimated_amount_atomic` (design.md §4.8).
758
+ *
759
+ * Single-event LlmCallPostPayload over the EmitTraceEvents duplex stream:
760
+ * the client opens a fresh stream per commit, sends one event, awaits one
761
+ * ack, and closes (Python parity — `emit_llm_call_post` at client.py:818).
762
+ * SLICE 5+ may switch to a long-lived stream for production latency, but
763
+ * the per-event setup cost is acceptable in v0.1.x.
764
+ *
765
+ * Ack semantics: the sidecar emits exactly one `TraceEventAck` per inbound
766
+ * event in this POC. Status != ACCEPTED surfaces as `SpendGuardError`
767
+ * (Codex round-2 P1.1 from Python parity — silent failure here would mask
768
+ * a commit-lifecycle bug).
769
+ *
770
+ * Mutually exclusive with the deferred provider-report path: this method
771
+ * always sends `estimated_amount_atomic`; the `provider_reported_amount_atomic`
772
+ * wire field stays empty. Adapters needing the provider-report path use the
773
+ * lower-level `emitLlmCallPost` (SLICE 7+).
774
+ *
775
+ * **SLICE 5 multi-event extension.** When `req.outcomeKind` is set, the
776
+ * client emits TWO events on the same bidi stream — the original
777
+ * LLM_CALL_POST event first, then a second LLM_CALL_POST-kind event whose
778
+ * `outcome` field reflects `outcomeKind` (SUCCESS → SUCCESS,
779
+ * FAILURE → PROVIDER_ERROR) and whose `providerResponseMetadata` carries a
780
+ * `{"error_message": ...}` envelope when `actualErrorMessage` is supplied.
781
+ * Both events are acked individually; if either ack is non-ACCEPTED the
782
+ * method raises `SpendGuardError`. See the JSDoc on
783
+ * `CommitEstimatedRequest.outcomeKind` for the LLM_CALL_OUTCOME proto-kind
784
+ * deviation note (Declared Deviation #1 in SLICE 5).
785
+ *
786
+ * When `req.outcomeKind` is ABSENT, behaviour is identical to SLICE 4 —
787
+ * a single event is sent, a single ack is drained.
788
+ *
789
+ * @throws SidecarUnavailable on UNAVAILABLE / DEADLINE_EXCEEDED / CANCELLED.
790
+ * @throws SpendGuardError on rejected ack or any other gRPC failure surface.
791
+ */
792
+ commitEstimated(req: CommitEstimatedRequest): Promise<void>;
793
+ /**
794
+ * Explicit release of a held reservation. Matches Agent Spend Protocol
795
+ * Draft-01 §4 one-to-one (the proto wire's
796
+ * `ReleaseReservationRequest` carries the canonical ASP fields at tags 1-3
797
+ * and SpendGuard extensions at tag 100+).
798
+ *
799
+ * Behaviour:
800
+ * - Disabled-mode short-circuit returns a synthetic
801
+ * `makeDisabledReleaseOutcome(req)` — no UDS contact.
802
+ * - Pre-handshake call throws `HandshakeError` via the `sessionId` getter
803
+ * gate (the request envelope requires the negotiated session id).
804
+ * - Wire envelope built by `buildReleaseRequest(req, sessionId)`.
805
+ * - Response mapped by `mapReleaseResponse(res, decisionIdHint)`.
806
+ * - Errors mapped centrally through `mapGrpcStatusToError`, with the
807
+ * `release`-specific NOT_FOUND override (reservation lookup misses
808
+ * surface as a plain `SpendGuardError("reservation not found")` so
809
+ * adapters can distinguish "no such reservation" from the rich
810
+ * FAILED_PRECONDITION cluster).
811
+ *
812
+ * @throws HandshakeError before `handshake()` completes.
813
+ * @throws SidecarUnavailable on UNAVAILABLE / DEADLINE_EXCEEDED / CANCELLED.
814
+ * @throws MutationApplyFailed on FAILED_PRECONDITION + IDEMPOTENCY_CONFLICT
815
+ * or BUDGET_EXCEEDED — or an unknown FAILED_PRECONDITION reason (the
816
+ * conservative default; never bare `SpendGuardError` for this cluster).
817
+ * @throws ApprovalBundleHotReloadedError on FAILED_PRECONDITION +
818
+ * BUNDLE_HOT_RELOADED.
819
+ * @throws SpendGuardError on NOT_FOUND ("reservation not found") + any
820
+ * other unmapped gRPC failure.
821
+ */
822
+ release(req: ReleaseRequest): Promise<ReleaseOutcome>;
823
+ /**
824
+ * Reserve a session-scoped hold for a realtime voice session (D41 SR-V3).
825
+ *
826
+ * The public request shape mirrors `buildReserveSessionRequest`. When
827
+ * `req.sessionId` is empty, the SDK fills it from the completed sidecar
828
+ * handshake so adapter code can bind the session reservation to the active
829
+ * UDS session without duplicating handshake plumbing.
830
+ *
831
+ * @throws HandshakeError before `handshake()` completes.
832
+ * @throws SidecarUnavailable on UNAVAILABLE / DEADLINE_EXCEEDED / CANCELLED.
833
+ * @throws SpendGuardError on proto error outcome or unmapped gRPC failure.
834
+ */
835
+ reserveSession(req: ReserveSessionRequest): Promise<ReserveSessionOutcome>;
836
+ /**
837
+ * Commit one positive streaming spend delta against a session reservation.
838
+ *
839
+ * @throws HandshakeError before `handshake()` completes.
840
+ * @throws SidecarUnavailable on UNAVAILABLE / DEADLINE_EXCEEDED / CANCELLED.
841
+ * @throws SpendGuardError on proto error outcome or unmapped gRPC failure.
842
+ */
843
+ commitSessionDelta(req: CommitSessionDeltaRequest): Promise<CommitSessionDeltaOutcome>;
844
+ /**
845
+ * Release the uncommitted remainder of a session reservation.
846
+ *
847
+ * @throws HandshakeError before `handshake()` completes.
848
+ * @throws SidecarUnavailable on UNAVAILABLE / DEADLINE_EXCEEDED / CANCELLED.
849
+ * @throws SpendGuardError on proto error outcome or unmapped gRPC failure.
850
+ */
851
+ releaseSession(req: ReleaseSessionRequest): Promise<ReleaseSessionOutcome>;
852
+ /**
853
+ * Read-only budget snapshot. Locked decision #4 of design.md §9: in v0.1.x
854
+ * the substrate ships the method signature but the sidecar wire is NOT yet
855
+ * implemented. SLICE 5 wires the §9.4 placeholder body — adapters call this
856
+ * method, catch the explicit `SpendGuardError`, and surface a clear "feature
857
+ * not yet available" upstream rather than a stray NOT_FOUND from a missing
858
+ * RPC route.
859
+ *
860
+ * Disabled-mode short-circuit returns a synthetic
861
+ * `makeDisabledQueryBudgetResult(req)` so unit tests can program against
862
+ * the method without a sidecar.
863
+ *
864
+ * @throws HandshakeError before `handshake()` completes.
865
+ * @throws SpendGuardError carrying the tracking-issue URL otherwise.
866
+ */
867
+ queryBudget(req: QueryBudgetRequest): Promise<QueryBudgetResult>;
868
+ /**
869
+ * Confirm `publish_effect` outcome. SLICE 7 wires body.
870
+ * @throws SpendGuardError until SLICE 7 wires the body.
871
+ */
872
+ confirmPublishOutcome(req: PublishOutcomeRequest): Promise<string>;
873
+ /**
874
+ * Resume after a human approver acted on a `REQUIRE_APPROVAL` decision.
875
+ * SLICE 7 wires the body; references this method from `ApprovalRequired.resume`.
876
+ * @throws SpendGuardError until SLICE 7 wires the body.
877
+ */
878
+ resumeAfterApproval(req: ResumeAfterApprovalRequest): Promise<DecisionOutcome>;
879
+ /**
880
+ * Safe-ack the `APPLY_FAILED` publish outcome — swallows transport errors
881
+ * so the caller's original exception is never shadowed. SLICE 7 wires body.
882
+ * @throws SpendGuardError until SLICE 7 wires the body.
883
+ */
884
+ safeConfirmApplyFailed(req: ApplyFailedRequest): Promise<void>;
885
+ /**
886
+ * Lower-level entry point that `commitEstimated()` wraps. Provided so
887
+ * adapters that need the raw trace-event surface have access. SLICE 7
888
+ * wires body (the provider-report path).
889
+ * @throws SpendGuardError until SLICE 7 wires the body.
890
+ */
891
+ emitLlmCallPost(req: EmitLlmCallPostRequest): Promise<void>;
892
+ /**
893
+ * Build the gRPC channel credentials. Always insecure over UDS — the kernel
894
+ * `SO_PEERCRED` check on the sidecar side is the trust anchor (Sidecar
895
+ * Architecture §5). TLS over a Unix socket adds overhead with no security
896
+ * benefit when the connection is implicitly local.
897
+ *
898
+ * Carved into its own method so a future slice can override under a
899
+ * `runtime` flag once HTTP-gateway transport is added — `runtime: "fetch"`
900
+ * would override to use TLS credentials. v0.1.x only supports `"uds-grpc"`.
901
+ */
902
+ private buildChannelCredentials;
903
+ /**
904
+ * Translate the public `ReserveRequest` (camelCase, TS-idiomatic) into the
905
+ * snake_case-on-wire `DecisionRequest` proto. Per implementation.md §4:
906
+ *
907
+ * 1. SessionId from the cached handshake (caller already gated above).
908
+ * 2. Trigger enum mapping via `triggerEnumOf()`.
909
+ * 3. W3C `traceparent` → `TraceContext` via `buildTraceContext()` (matches
910
+ * Python `_build_trace_context`).
911
+ * 4. `runtimeMetadata` carries the prompt hash (when caller supplied
912
+ * `promptText`) and any `decisionContextJson` keys. The
913
+ * `run_projection_policy` slot is filled from the caller's
914
+ * `decisionContextJson.run_projection_policy` if present, otherwise
915
+ * from `cfg.runProjectionDefault` when non-empty. **This is the
916
+ * SLICE 4 consumption of MJ-1** — SLICE 3 stored the field on the
917
+ * config; this method wires it onto the wire.
918
+ * 5. `plannedStepsHint` is `plan.plannedCalls + plan.plannedTools` when
919
+ * a `withRunPlan` scope is active (SLICE 7 R2), otherwise the proto3
920
+ * default `0`.
921
+ *
922
+ * `runtime_metadata` is encoded as a hand-built `google.protobuf.Struct`
923
+ * payload because the SDK does not yet ship `computePromptHash` (SLICE 6).
924
+ * Until then this method ALWAYS sends an empty Struct body when no caller
925
+ * decoration is requested — matching Python `runtime_metadata = None` which
926
+ * is wire-equivalent to "field absent" under proto3 message optionality.
927
+ */
928
+ private buildDecisionRequest;
929
+ /**
930
+ * Build the `google.protobuf.Struct` payload that lands in
931
+ * `DecisionRequest.inputs.runtime_metadata`. Returns `undefined` when there
932
+ * is nothing to send (proto3 message optionality — wire equivalent to
933
+ * "field absent").
934
+ *
935
+ * Two slots are populated here:
936
+ * - `decision_context_json.*` keys from the caller (verbatim).
937
+ * - `run_projection_policy` from the caller (if present in
938
+ * `decisionContextJson`) OR `cfg.runProjectionDefault` (when set and
939
+ * non-empty). The caller's value wins; the default only fills in when
940
+ * the caller did not provide one — matches design.md §4.2 R2 semantics.
941
+ *
942
+ * SLICE 6 R1 closure of SLICE 4 M-3: when `req.promptText` is set,
943
+ * `computePromptHash(req.promptText, this.cfg.tenantId)` populates
944
+ * `runtime_metadata.prompt_hash` as a stringValue. Mirrors Python parity
945
+ * at `sdk/python/.../client.py` (the `prompt_hash` field is the rules
946
+ * dedup key per Cost Advisor P0.5 §5.1). The caller may pre-set
947
+ * `decisionContextJson.prompt_hash` to override (e.g. when the prompt is
948
+ * tokenised upstream and the hash is computed there); the caller-supplied
949
+ * value wins.
950
+ */
951
+ private buildRuntimeMetadataStruct;
952
+ /**
953
+ * Build the single LLM_CALL_POST trace event for `commitEstimated()`.
954
+ * Mirrors Python `emit_llm_call_post` at client.py:818 with the difference
955
+ * that `provider_reported_amount_atomic` is always empty here (the
956
+ * provider-report path lives in SLICE 5+'s `emitLlmCallPost`).
957
+ */
958
+ private buildLlmCallPostEvent;
959
+ /**
960
+ * Build the SLICE 5 multi-event "outcome" companion event for
961
+ * `commitEstimated()` when `req.outcomeKind` is set.
962
+ *
963
+ * The event reuses `TraceEvent_EventKind.LLM_CALL_POST` (per Declared
964
+ * Deviation #1 — `LLM_CALL_OUTCOME` does not exist as a proto enum value
965
+ * in `sidecar_adapter/v1/adapter.proto` yet).
966
+ *
967
+ * TODO(GH-issue-TBD): proto bump for `LLM_CALL_OUTCOME` +
968
+ * sidecar `x-spendguard-reason-code` trailer extension. Both deferred to
969
+ * the cross-component slice that touches `proto/` and
970
+ * `services/sidecar/`. Track at
971
+ * https://github.com/m24927605/agentic-spendguard/issues/TBD-proto-bump-llm-call-outcome
972
+ * (R2 follow-up; not in scope for D05 SLICE 5).
973
+ *
974
+ * The `outcome` field on the inner `LlmCallPostPayload` carries the
975
+ * semantic:
976
+ *
977
+ * - `outcomeKind === "SUCCESS"` → `LlmCallPostPayload_Outcome.SUCCESS`
978
+ * - `outcomeKind === "FAILURE"` → `LlmCallPostPayload_Outcome.PROVIDER_ERROR`
979
+ *
980
+ * Actuals (`actualInputTokens` / `actualOutputTokens`) prefer the SLICE 5
981
+ * `*Wire` fields when supplied (int64-as-string form), falling back to
982
+ * the SLICE 4 numeric `actualInputTokens` / `actualOutputTokens` shape so
983
+ * adapters do not need to double-specify. `actualErrorMessage` is threaded
984
+ * onto `TraceEvent.providerResponseMetadata` as a JSON envelope
985
+ * `{"error_message": "..."}` only when `outcomeKind === "FAILURE"`.
986
+ *
987
+ * `estimatedAmountAtomic` is intentionally set to an empty string on the
988
+ * outcome event — the first event already booked the commit; the outcome
989
+ * event only carries observation, not commit semantics. The sidecar will
990
+ * surface a rejection ack if it requires the field (`mock-sidecar`'s tests
991
+ * lock this in).
992
+ *
993
+ * @private
994
+ */
995
+ private buildLlmCallOutcomeEvent;
996
+ }
997
+ /**
998
+ * Stable, in-process hash of a `ReserveRequest`'s identity for binding
999
+ * idempotency-cache entries to the request body.
1000
+ *
1001
+ * Why: `cfg.idempotencyCache` is keyed on the caller-supplied
1002
+ * `idempotencyKey`. A cache HIT short-circuits the UDS round trip entirely, so
1003
+ * the sidecar (the correctness gate) never re-evaluates. If an adapter reuses
1004
+ * an `idempotencyKey` for a logically DIFFERENT reserve (different claims /
1005
+ * amount / route / window / pricing), a key-only cache would return the prior
1006
+ * CONTINUE — allowing spend against a stale decision the sidecar never saw.
1007
+ * Binding the entry to this hash makes a key-hit-with-different-body fall
1008
+ * through to the sidecar (see `InMemoryIdempotencyCache.get`).
1009
+ *
1010
+ * This hash is a LOCAL latency-optimisation discriminator only — it is NOT a
1011
+ * wire value and needs NO cross-language byte-equivalence. It MUST, however,
1012
+ * be deterministic within a process and cover every field that changes the
1013
+ * ledger/sidecar decision, so the local fall-through decision provably agrees
1014
+ * with the sidecar's accept / IDEMPOTENCY_CONFLICT decision. We therefore hash
1015
+ * the FULL request identity (all decision-affecting fields) via a canonical
1016
+ * JSON encoding with sorted keys.
1017
+ *
1018
+ * `idempotencyKey` is intentionally EXCLUDED from the hashed body (it is the
1019
+ * cache key, not part of the identity being disambiguated under that key).
1020
+ *
1021
+ * Exported for the cache-collision regression test; not part of the LOCKED
1022
+ * public surface (no index.ts re-export).
1023
+ */
1024
+ declare function computeReserveBodyHash(req: ReserveRequest): string;
1025
+ /**
1026
+ * Lockable contract: `DomainError::Display` prefix → canonical reason code.
1027
+ *
1028
+ * Sourced from `services/sidecar/src/domain/error.rs` `#[error(...)]`
1029
+ * attributes (lines 26-45) — these strings ARE the public Status-message
1030
+ * format the sidecar emits via `Status::failed_precondition(self.to_string())`.
1031
+ * Match is case-insensitive prefix on the bare `RpcError.message`
1032
+ * (anchored at the start; the `: <detail>` tail varies per call).
1033
+ *
1034
+ * Forward-compat: the bracket-tagged `[BUNDLE_HOT_RELOADED]` form comes from
1035
+ * `services/sidecar/src/server/adapter_uds.rs:1379` (resume path) and is
1036
+ * included so dispatch already works when a future cross-component slice
1037
+ * unifies the release/resume Status surface.
1038
+ *
1039
+ * Ordered ARRAY (not a map): longest / most-specific prefixes FIRST so a
1040
+ * shorter prefix doesn't accidentally swallow a longer one.
1041
+ */
1042
+ declare const REASON_CODE_PREFIXES: ReadonlyArray<readonly [string, string]>;
1043
+ /**
1044
+ * Context passed to `mapGrpcStatusToError`. The `rpc` field is folded into
1045
+ * the typed-error message; `releaseNotFoundAsPlain` opt-in flips NOT_FOUND
1046
+ * into a plain `SpendGuardError("reservation not found")` for the
1047
+ * `release()` call path (the only RPC that needs the override — `reserve` /
1048
+ * `handshake` / `commitEstimated` never legitimately surface NOT_FOUND).
1049
+ */
1050
+ interface MapGrpcStatusContext {
1051
+ rpc: string;
1052
+ releaseNotFoundAsPlain?: boolean;
1053
+ }
1054
+
1055
+ /**
1056
+ * Idempotency cache contract. Adapters consume via
1057
+ * `SpendGuardClientConfig.idempotencyCache?: IdempotencyCache`.
1058
+ *
1059
+ * - `get(key, bodyHash?)` MUST return the cached outcome for `key` when it is
1060
+ * fresh (TTL-window unexpired) AND — when both the caller and the stored
1061
+ * entry carry a `bodyHash` — the hashes match; else `undefined`.
1062
+ * Implementations MAY treat the read as a recency signal (LRU
1063
+ * move-to-front); MUST NOT bump the TTL.
1064
+ * - `set(key, outcome, ttlMs?, bodyHash?)` stores the outcome. Optional `ttlMs`
1065
+ * overrides the cache's default TTL for this entry. Optional `bodyHash` binds
1066
+ * the entry to the request identity that produced `outcome` so a later
1067
+ * `get(key, otherHash)` with a colliding key but a different body falls
1068
+ * through (returns `undefined`) instead of returning a stale decision.
1069
+ * Implementations MAY evict in amortised O(1) to honor a `maxEntries` cap.
1070
+ * - `clear()` resets the cache to empty.
1071
+ * - `size` is a snapshot getter; readers MAY observe a non-monotonic value
1072
+ * across concurrent sets (no atomicity guarantee).
1073
+ *
1074
+ * The `bodyHash` parameters are OPTIONAL and additive: callers that omit them
1075
+ * get key-only semantics (unchanged from v0.1.0). `SpendGuardClient.reserve()`
1076
+ * always supplies a `bodyHash` so a reused `idempotencyKey` for a logically
1077
+ * different request never short-circuits the authoritative sidecar gate.
1078
+ */
1079
+ interface IdempotencyCache {
1080
+ get(key: string, bodyHash?: string): DecisionOutcome | undefined;
1081
+ set(key: string, outcome: DecisionOutcome, ttlMs?: number, bodyHash?: string): void;
1082
+ clear(): void;
1083
+ readonly size: number;
1084
+ }
1085
+ /**
1086
+ * Default cache cap. design.md §3 line 35 LOCKS this at "default 1024
1087
+ * entries". The number is a balance:
1088
+ * - 1024 entries × ~512 B/entry (DecisionOutcome with reservation IDs +
1089
+ * reason codes) → ~512 KiB ceiling. Fits comfortably in adapter
1090
+ * working-set budgets.
1091
+ * - 1024 is large enough to absorb a steady-state burst of in-flight
1092
+ * decisions (each adapter request issues at most one `reserve`).
1093
+ * - Override via `new InMemoryIdempotencyCache({ maxEntries: ... })`
1094
+ * when adapters know their working set is larger.
1095
+ */
1096
+ declare const DEFAULT_CACHE_MAX_ENTRIES = 1024;
1097
+ /**
1098
+ * Default TTL. design.md §3 line 35 + implementation.md §10 LOCK this at
1099
+ * "TTL 5 minutes" — long enough to cover a same-process retry storm
1100
+ * (the SLICE 8 retry helper has a 50 ms ceiling per attempt × 2 attempts =
1101
+ * ≤100 ms — the cache TTL must dominate by 3-4 orders of magnitude to be
1102
+ * useful) but short enough that a cache-bypass-via-eviction edge case
1103
+ * doesn't propagate stale outcomes hours after the original reservation
1104
+ * was already released by TTL sweep on the sidecar side.
1105
+ */
1106
+ declare const DEFAULT_CACHE_TTL_MS: number;
1107
+ /** Options for `InMemoryIdempotencyCache`. */
1108
+ interface InMemoryIdempotencyCacheOptions {
1109
+ /** Max entries before LRU eviction. Default 1024. */
1110
+ maxEntries?: number;
1111
+ /** Default per-entry TTL in ms. Default 300_000. Per-call `ttlMs` overrides. */
1112
+ defaultTtlMs?: number;
1113
+ /** Override the clock for tests. Default `Date.now`. */
1114
+ now?: () => number;
1115
+ }
1116
+ /**
1117
+ * In-memory LRU + TTL cache. Backed by a `Map` whose insertion order doubles
1118
+ * as the LRU axis:
1119
+ * - `set` deletes-then-inserts so the entry moves to the most-recently-used
1120
+ * end of the iteration order.
1121
+ * - `get` on a fresh entry also delete-then-inserts (LRU move-to-front).
1122
+ * - Eviction pops the oldest entry via `keys().next().value`.
1123
+ *
1124
+ * TTL is checked on every `get`: an expired entry is treated as a miss AND
1125
+ * is evicted immediately (lazy expiry — no background sweeper).
1126
+ *
1127
+ * Thread-safety: Node.js is single-threaded for user code. Worker_threads
1128
+ * cannot share `Map` references via structured clone, so the cache is
1129
+ * inherently per-thread. Multi-process deployments need an external cache;
1130
+ * see `IdempotencyCache` interface JSDoc.
1131
+ *
1132
+ * @example
1133
+ * const cache = new InMemoryIdempotencyCache({ maxEntries: 2048 });
1134
+ * const client = new SpendGuardClient({ idempotencyCache: cache, ... });
1135
+ */
1136
+ declare class InMemoryIdempotencyCache implements IdempotencyCache {
1137
+ private readonly entries;
1138
+ private readonly maxEntries;
1139
+ private readonly defaultTtlMs;
1140
+ private readonly now;
1141
+ constructor(opts?: InMemoryIdempotencyCacheOptions);
1142
+ get(key: string, bodyHash?: string): DecisionOutcome | undefined;
1143
+ set(key: string, outcome: DecisionOutcome, ttlMs?: number, bodyHash?: string): void;
1144
+ clear(): void;
1145
+ get size(): number;
1146
+ }
1147
+ /**
1148
+ * No-op cache impl. Always misses on `get`; `set` discards. Useful for:
1149
+ * - Tests that want to assert every reserved decision hits the sidecar.
1150
+ * - Disabled-mode short-circuit (when `SPENDGUARD_DISABLE=1`, the client
1151
+ * never reaches the cache path, but `NoopIdempotencyCache` is the
1152
+ * conservative-default if an adapter passes `idempotencyCache: undefined`
1153
+ * and wants explicit no-cache semantics).
1154
+ *
1155
+ * The `size` getter always returns 0; `clear()` is a no-op.
1156
+ */
1157
+ declare class NoopIdempotencyCache implements IdempotencyCache {
1158
+ get(_key: string, _bodyHash?: string): DecisionOutcome | undefined;
1159
+ set(_key: string, _outcome: DecisionOutcome, _ttlMs?: number, _bodyHash?: string): void;
1160
+ clear(): void;
1161
+ get size(): number;
1162
+ }
1163
+
1164
+ export { type UnitRef as $, type ApplyFailedRequest as A, type BudgetClaim as B, type ClaimEstimate as C, DEFAULT_CACHE_MAX_ENTRIES as D, type EmitLlmCallPostRequest as E, type SessionDeltaCommitClient as F, type SessionDeltaCommitInput as G, type HandshakeOutcome as H, type IdempotencyCache as I, SessionPendingDeltaLimitError as J, type SessionReleaseClient as K, type SessionReleaseInput as L, SessionReservationHandle as M, NoopIdempotencyCache as N, SessionReservationHandleError as O, type PendingSessionDelta as P, type QueryBudgetRequest as Q, type RunProjectionPolicy as R, type SessionCommitOutcome as S, type SessionReservationHandleOptions as T, type SessionReservationHandleSnapshot as U, SessionReservationReleasedError as V, SessionReservationReplayMismatchError as W, type SpanRecord as X, SpendGuardClient as Y, type SpendGuardClientConfig as Z, type SpendGuardClientOptions as _, type CommitEstimatedRequest as a, buildCommitSessionDeltaRequest as a0, buildReleaseSessionRequest as a1, buildReserveSessionRequest as a2, type MapGrpcStatusContext as a3, REASON_CODE_PREFIXES as a4, computeReserveBodyHash as a5, type CommitSessionDeltaOutcome as b, type CommitSessionDeltaRequest as c, DEFAULT_CACHE_TTL_MS as d, DEFAULT_CAPABILITY_LEVEL as e, DEFAULT_DECISION_TIMEOUT_MS as f, DEFAULT_HANDSHAKE_TIMEOUT_MS as g, DEFAULT_MAX_PENDING_SESSION_DELTAS as h, DEFAULT_PROTOCOL_VERSION as i, DEFAULT_PUBLISH_TIMEOUT_MS as j, DEFAULT_TRACE_TIMEOUT_MS as k, type DecisionOutcome as l, InMemoryIdempotencyCache as m, type InMemoryIdempotencyCacheOptions as n, type PricingFreeze as o, type PublishOutcomeRequest as p, type QueryBudgetResult as q, type ReleaseOutcome as r, type ReleaseRequest as s, type ReleaseSessionOutcome as t, type ReleaseSessionRequest as u, type ReserveRequest as v, type ReserveSessionOutcome as w, type ReserveSessionRequest as x, type ResolvedConfig as y, type ResumeAfterApprovalRequest as z };