zk-credits 0.2.11 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,6 +32,8 @@ private: anyone with it can spend your credits.
32
32
  | `zk-credits setup codex` | Import a credential and configure Codex |
33
33
  | `zk-credits setup x402` | Prepare an x402 agent |
34
34
  | `zk-credits codex [args...]` | Start Codex with the local sidecar |
35
+ | `zk-credits codex --task-evidence` | Run a coding task and show its aggregate payment evidence on exit |
36
+ | `zk-credits investigate --chain base <transaction-hash>` | Investigate a failed Base transaction using prepaid inference |
35
37
  | `zk-credits cline [args...]` | Start Cline with the local sidecar |
36
38
  | `zk-credits x402-agent` | Run the included x402 agent |
37
39
  | `zk-credits serve` | Run the sidecar in the foreground |
@@ -82,3 +84,47 @@ Your credential stays on your device. Proofs are sent to authorize each spend. T
82
84
  can observe traffic metadata. This does not hide your IP address.
83
85
 
84
86
  Base Sepolia test credits only. The circuit is not independently audited.
87
+
88
+ ### Coding-task evidence (0.2.14 candidate)
89
+
90
+ After setup, `zk-credits codex --task-evidence` launches Codex against a fresh,
91
+ dedicated local sidecar and prints aggregate payment counters when Codex exits.
92
+ The provider and profile are fixed for attribution. The summary contains no
93
+ prompts, bearer credentials, proofs, or payment headers. `exchange_verified`
94
+ confirms a successful agent exit and an observed settled response; inspect the
95
+ code diff and tests separately to judge the coding task. The supported model and
96
+ reasoning defaults are selected automatically, including with older setups:
97
+
98
+ ```sh
99
+ zk-credits codex --task-evidence
100
+ ```
101
+
102
+ Enter your task, review the change and tests, then exit Codex to see the summary.
103
+ Version 0.2.12 includes this command; 0.2.11 lacks it.
104
+ See the [indexer sample](https://github.com/mangekyou-labs/haze-api/tree/main/examples/base-usdc-indexer).
105
+
106
+ ### Transaction investigator (0.2.14 candidate)
107
+
108
+ After closing Codex, run `zk-credits investigate --chain base <transaction-hash>`
109
+ with the same configured credential. It fetches public execution evidence and
110
+ uses the custom x402 `zk-prepaid` adapter for model inference. Base mainnet is
111
+ the investigated chain; credit funding remains on Base Sepolia and consumption
112
+ is offchain. It does not purchase external traces. Keep missing-evidence notices
113
+ visible and distinguish observed errors from model hypotheses.
114
+
115
+ Version 0.2.14 fixes the investigator's packaged payment recipient pin and refuses
116
+ builds with missing or placeholder treasury addresses. Payment requirements must
117
+ still match the reviewed deployment before any proof is authorized.
118
+
119
+ Repeating a transaction hash recovers its existing local task without authorizing
120
+ a new paid call. The report shows the original stopping reason and labels prior
121
+ accounting separately from current invocation counters, including when the saved
122
+ run authorized no credits. Upgrading does not resume a stopped investigation.
123
+ Do not delete task journals or reset
124
+ credential accounting to force a fresh exchange. Unknown payment outcomes must
125
+ be reconciled before another paid task. A saved report must be labeled as a prior
126
+ run, with its own payment summary.
127
+
128
+ `challengeObserved` counts challenges presented for authorization, including
129
+ refused terms. A challenge alone does not imply a proof or consumed credit;
130
+ check confirmed, failed, unknown and retired counters separately.
@@ -33,6 +33,7 @@
33
33
  "deployment": {
34
34
  "chainId": 84532,
35
35
  "bondAddress": "0xcc1909dD30485d6D9a46e3d33cf04C1378b586Ab",
36
+ "treasuryAddress": "0xa8b9b38e748563AF7FbAA276e8a843Da417D7Bc1",
36
37
  "deploymentBlock": "47372040",
37
38
  "deploymentDomain": "84532",
38
39
  "circuitId": "private-credit-spend-bn254-dev",
@@ -0,0 +1,18 @@
1
+ import { type CredentialStore } from './credential-store.js';
2
+ import { type ResolveSetupInputsOptions, type SetupConfig } from './setup-config.js';
3
+ export interface ActivatedCredentialSetupOptions {
4
+ environment?: NodeJS.ProcessEnv;
5
+ homeDirectory?: string;
6
+ stateDirectory?: string;
7
+ credentialStore?: CredentialStore;
8
+ chooseCandidate?: ResolveSetupInputsOptions['chooseCandidate'];
9
+ /** The caller owns hidden terminal input for explicit legacy import. */
10
+ readLegacyPassword?(): Promise<string>;
11
+ }
12
+ /**
13
+ * Imports or loads the credential and prepares validated runtime configuration.
14
+ * Configuration is saved only after all checks pass. Credential import, artifact
15
+ * acquisition and witness synchronization may persist progress on failure so a
16
+ * retry can reuse it. Applies RPC and successful setup paths to the environment.
17
+ */
18
+ export declare function setupActivatedCredential(options?: ActivatedCredentialSetupOptions): Promise<SetupConfig>;
@@ -0,0 +1,64 @@
1
+ /** Interactive credential preparation; runtime loading and launch remain separate. */
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { loadCircuitManifest } from './artifact-bundle.js';
5
+ import { validateBaseCreditWitness } from './base-sidecar.js';
6
+ import { importCredentialPath, nativeCredentialStore, readStoredCredential, } from './credential-store.js';
7
+ import { requireGatewayKnownRoot, resolveGatewayOrigin } from './gateway-root-check.js';
8
+ import { createPinnedBaseProofGenerator } from './proof-coordinator.js';
9
+ import { createBaseProofMetrics } from './proof-metrics.js';
10
+ import { applyRpcConfig, checkBaseSepoliaRpc, resolveBaseSepoliaRpc } from './rpc-config.js';
11
+ import { applySetupConfig, readSetupConfig, resolveSetupInputs, writeSetupConfig, } from './setup-config.js';
12
+ import { witnessProviderForSource } from './sidecar-runtime.js';
13
+ /**
14
+ * Imports or loads the credential and prepares validated runtime configuration.
15
+ * Configuration is saved only after all checks pass. Credential import, artifact
16
+ * acquisition and witness synchronization may persist progress on failure so a
17
+ * retry can reuse it. Applies RPC and successful setup paths to the environment.
18
+ */
19
+ export async function setupActivatedCredential(options = {}) {
20
+ const environment = options.environment ?? process.env;
21
+ const homeDirectory = options.homeDirectory ?? homedir();
22
+ const stateDirectory = options.stateDirectory ?? (environment.ZK_CREDITS_HOME || join(homeDirectory, '.zk-credits'));
23
+ const configPath = join(stateDirectory, 'config.json');
24
+ const credentialStore = options.credentialStore ?? nativeCredentialStore(stateDirectory);
25
+ await applyRpcConfig(environment, stateDirectory);
26
+ const saved = await readSetupConfig(configPath);
27
+ const rpc = resolveBaseSepoliaRpc(environment, saved?.witness.kind === 'base-events' ? saved.witness.rpcUrl : undefined);
28
+ await checkBaseSepoliaRpc(rpc);
29
+ environment.BASE_RPC_URL = rpc;
30
+ const credentialPath = environment.ZK_CREDITS_CREDENTIAL_PATH;
31
+ const legacyPassword = await options.readLegacyPassword?.();
32
+ const credential = credentialPath
33
+ ? await importCredentialPath(credentialPath, credentialStore, legacyPassword)
34
+ : await readStoredCredential(credentialStore);
35
+ const manifest = await loadCircuitManifest();
36
+ const config = await resolveSetupInputs({
37
+ environment,
38
+ homeDirectory,
39
+ stateDirectory,
40
+ configPath,
41
+ manifest,
42
+ validateWitness: async (source) => {
43
+ const provider = await witnessProviderForSource(source, join(stateDirectory, 'base-event-sync.json'));
44
+ const witness = validateBaseCreditWitness(await provider.witnessForCredential(credential));
45
+ if (manifest.deployment)
46
+ await requireGatewayKnownRoot(witness.root, resolveGatewayOrigin(environment));
47
+ },
48
+ chooseCandidate: options.chooseCandidate,
49
+ });
50
+ try {
51
+ await createPinnedBaseProofGenerator({
52
+ artifactDirectory: config.artifactDirectory,
53
+ metrics: createBaseProofMetrics(),
54
+ });
55
+ }
56
+ catch (error) {
57
+ if (error instanceof Error)
58
+ throw new Error(`Pinned proving bundle check failed: ${error.message}`);
59
+ throw new Error('Pinned proving bundle check failed. Reinstall the bundle from your pilot invite.');
60
+ }
61
+ await writeSetupConfig(configPath, config);
62
+ applySetupConfig(environment, config);
63
+ return config;
64
+ }
@@ -13,6 +13,7 @@ import { baseSepolia } from 'viem/chains';
13
13
  import { createPublicClient, http, parseAbi } from 'viem';
14
14
  import { BASE_MEMBERSHIP_TREE_CAPACITY, BN254_FIELD_ORDER, computeCreditLeaf, deriveSparseCreditWitness, } from '@zk-credits/shared/base';
15
15
  import { RPC_GUIDANCE } from './rpc-config.js';
16
+ import { rpcFetch } from './rpc-transport.js';
16
17
  const BUNDLE_FUNDED_ABI = parseAbi([
17
18
  'event BundleFunded(bytes32 indexed commitment, uint8 indexed tierId, uint64 expiry, uint256 leafIndex, uint256 bondAmount, bytes32 root)',
18
19
  ]);
@@ -82,12 +83,15 @@ function serializeLeaves(leaves) {
82
83
  }
83
84
  /** Creates a witness provider that syncs finalized BundleFunded events. */
84
85
  export function createBaseEventWitnessProvider(options) {
85
- const client = options.client ?? createPublicClient({ chain: baseSepolia, transport: http(options.rpcUrl) });
86
+ const client = options.client ?? createPublicClient({ chain: baseSepolia,
87
+ transport: http(options.rpcUrl, { fetchFn: rpcFetch, retryCount: 0, timeout: 30_000 }),
88
+ });
86
89
  const contract = address(options.contractAddress);
87
90
  const confirmations = options.confirmations ?? 3n;
88
91
  const maxBlockRange = options.maxBlockRange ?? 2000n;
89
92
  let cache;
90
93
  let leaves = new Map();
94
+ let pendingSync;
91
95
  async function rpcCall(operation) {
92
96
  try {
93
97
  return await operation;
@@ -150,7 +154,8 @@ export function createBaseEventWitnessProvider(options) {
150
154
  }
151
155
  return {
152
156
  async witnessForCredential(credential) {
153
- await synchronize();
157
+ pendingSync ??= synchronize().finally(() => { pendingSync = undefined; });
158
+ await pendingSync;
154
159
  const commitment = field(credential.commitment, 'credential commitment');
155
160
  const entry = [...leaves.entries()].find(([, value]) => value.commitment === commitment);
156
161
  if (!entry)
@@ -65,6 +65,28 @@ export interface BasePrepaidClientOptions {
65
65
  * a public signal, or a credential identifier.
66
66
  */
67
67
  lifecycle?: ZkPrepaidLifecycleObserver;
68
+ /** Caller-owned durable policy and accounting boundary; failures propagate. */
69
+ authorization?: BaseAuthorizationBridge;
70
+ }
71
+ export interface BaseAuthorizationRequest extends BasePaymentContext {
72
+ attemptId: string;
73
+ body: Uint8Array;
74
+ }
75
+ export type BaseUsageObservation = {
76
+ attemptId: string;
77
+ slot: number;
78
+ } & ({
79
+ type: 'provisional' | 'released' | 'retired';
80
+ } | {
81
+ type: 'settlement';
82
+ outcome: 'confirmed' | 'failed' | 'unknown';
83
+ reservationId?: string;
84
+ });
85
+ export interface BaseAuthorizationBridge {
86
+ /** Persist reservation and validate actual terms before any fresh proof. */
87
+ beforeProof(request: BaseAuthorizationRequest): Promise<void>;
88
+ /** Contains no request content, credential, proof, signature or replay key. */
89
+ observe(event: BaseUsageObservation): Promise<void>;
68
90
  }
69
91
  export interface BasePrepaidClient {
70
92
  client: ZkPrepaidClient;
@@ -80,6 +102,8 @@ export interface BasePaymentContext {
80
102
  export interface BasePreparedPayment {
81
103
  payment: PaymentPayload;
82
104
  responsePrivateKey: string;
105
+ attemptId: string;
106
+ slot: number;
83
107
  }
84
108
  export declare function validateBaseCreditWitness(witness: BaseCreditWitness): BaseCreditWitness;
85
109
  /** Creates the shared local proof factory used by both client transports. */
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * SPDX-License-Identifier: AGPL-3.0-or-later
9
9
  */
10
- import { createDecipheriv, generateKeyPair as generateKeyPairCallback, privateDecrypt, randomBytes } from 'node:crypto';
10
+ import { createDecipheriv, generateKeyPair as generateKeyPairCallback, privateDecrypt, randomBytes, randomUUID } from 'node:crypto';
11
11
  import { promisify } from 'node:util';
12
12
  import { BASE_MEMBERSHIP_TREE_CAPACITY, canonicalizeBaseJson, computeCreditLeaf, computeNullifier, computeShare, computeSlotBlinding, deriveRequestSignal, deriveSparseCreditWitness, secretFromBase64Url, secretToField, } from '@zk-credits/shared/base';
13
13
  import { buildPaymentPayload, createZkPrepaidClient, decodeHeader, PAYMENT_RESPONSE_HEADER, PAYMENT_SIGNATURE_HEADER, } from '@zk-credits/x402-zk-prepaid';
@@ -77,8 +77,13 @@ export function createBasePaymentFactory(options) {
77
77
  const slotLedger = options.slotLedger;
78
78
  if (!slotLedger)
79
79
  throw new Error('A durable slot ledger is required for the Base pilot');
80
+ const attemptId = randomUUID();
81
+ const bytes = new Uint8Array(requestBodyBytes(body));
82
+ await options.authorization?.beforeProof({ attemptId, method, url, body: bytes, requirements: structuredClone(requirements) });
80
83
  const selectedSlot = slotLedger.allocateProvisional();
84
+ let retired = false;
81
85
  try {
86
+ await options.authorization?.observe({ type: 'provisional', attemptId, slot: selectedSlot });
82
87
  const nonce = randomNonce();
83
88
  const responseKeys = await responseKey();
84
89
  const requestSignal = await deriveRequestSignal({
@@ -128,6 +133,8 @@ export function createBasePaymentFactory(options) {
128
133
  throw new Error('Proof public signals do not match the canonical statement');
129
134
  }
130
135
  await slotLedger.commit(selectedSlot);
136
+ retired = true;
137
+ await options.authorization?.observe({ type: 'retired', attemptId, slot: selectedSlot });
131
138
  const payment = buildPaymentPayload({
132
139
  requirements,
133
140
  proof: proofForPayment(proof.proof),
@@ -135,10 +142,13 @@ export function createBasePaymentFactory(options) {
135
142
  nonce,
136
143
  responseKey: responseKeys.publicKey,
137
144
  });
138
- return { payment, responsePrivateKey: responseKeys.privateKey };
145
+ return { payment, responsePrivateKey: responseKeys.privateKey, attemptId, slot: selectedSlot };
139
146
  }
140
147
  catch (error) {
141
- slotLedger.release(selectedSlot);
148
+ if (!retired) {
149
+ slotLedger.release(selectedSlot);
150
+ await options.authorization?.observe({ type: 'released', attemptId, slot: selectedSlot });
151
+ }
142
152
  throw error;
143
153
  }
144
154
  };
@@ -266,9 +276,49 @@ export function createBasePrepaidClient(options) {
266
276
  headers: new Headers(response.headers),
267
277
  });
268
278
  };
279
+ const observeSettlement = async (response, init) => {
280
+ const header = new Headers(init?.headers).get(PAYMENT_SIGNATURE_HEADER);
281
+ if (!header || !options.authorization)
282
+ return;
283
+ let payment;
284
+ try {
285
+ payment = decodeHeader(header);
286
+ }
287
+ catch {
288
+ return;
289
+ }
290
+ const key = pendingByNonce.get(payment.payload.nonce);
291
+ const pending = key ? pendingPayments.get(key) : undefined;
292
+ if (!pending)
293
+ return;
294
+ let outcome = 'unknown';
295
+ let reservationId;
296
+ try {
297
+ const encoded = response.headers.get(PAYMENT_RESPONSE_HEADER);
298
+ const result = encoded ? decodeHeader(encoded) : undefined;
299
+ if (result && result.network === pending.payment.accepted.network && result.transaction === ''
300
+ && (result.reservationId === undefined || (typeof result.reservationId === 'string' && result.reservationId.length > 0 && result.reservationId.length <= 256))
301
+ && typeof result.success === 'boolean' && !('payer' in result)) {
302
+ // A failed response can describe an ambiguous ready/commit failure.
303
+ outcome = result.success ? 'confirmed' : 'unknown';
304
+ reservationId = result.reservationId;
305
+ }
306
+ }
307
+ catch { /* Missing or malformed settlement leaves consumption unknown. */ }
308
+ await options.authorization.observe({ type: 'settlement', attemptId: pending.attemptId, slot: pending.slot, outcome,
309
+ ...(reservationId === undefined ? {} : { reservationId }) });
310
+ };
269
311
  const fetchWithReplay = async (input, init) => {
270
312
  prunePending();
271
- const response = await fetcher(input, init);
313
+ let response;
314
+ try {
315
+ response = await fetcher(input, { ...init, redirect: 'error' });
316
+ }
317
+ catch (error) {
318
+ await observeSettlement(new Response(null, { status: 503 }), init);
319
+ throw error;
320
+ }
321
+ await observeSettlement(response, init);
272
322
  if (response.status !== 409)
273
323
  return trackPaymentResponse(response, init);
274
324
  let replayAvailable = false;
@@ -343,7 +393,7 @@ export function createBasePrepaidClient(options) {
343
393
  const nonce = payment.payload.nonce;
344
394
  const expiresAt = Date.now() + REPLAY_TTL_MS;
345
395
  replayKeys.set(nonce, { privateKey: prepared.responsePrivateKey, expiresAt });
346
- pendingPayments.set(key, { payment, expiresAt });
396
+ pendingPayments.set(key, { payment, expiresAt, attemptId: prepared.attemptId, slot: prepared.slot });
347
397
  pendingByNonce.set(nonce, key);
348
398
  prunePending();
349
399
  return payment;
@@ -358,7 +408,25 @@ export function createBasePrepaidClient(options) {
358
408
  }
359
409
  },
360
410
  });
361
- return { client, committedSlots: () => options.slotLedger?.committedSlots() ?? [] };
411
+ return {
412
+ client: {
413
+ clearRequirements: client.clearRequirements,
414
+ async fetch(input, init) {
415
+ const source = input instanceof Request ? input : undefined;
416
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
417
+ const body = init?.body ?? (source?.body ? new Uint8Array(await source.clone().arrayBuffer()) : undefined);
418
+ const snapshot = body instanceof ArrayBuffer ? new Uint8Array(body.slice(0))
419
+ : body instanceof Uint8Array ? new Uint8Array(body) : body;
420
+ return client.fetch(url, {
421
+ ...(source ? { method: source.method, headers: new Headers(source.headers), signal: source.signal } : {}),
422
+ ...init,
423
+ headers: new Headers(init?.headers ?? source?.headers),
424
+ ...(snapshot === undefined ? {} : { body: snapshot }),
425
+ });
426
+ },
427
+ },
428
+ committedSlots: () => options.slotLedger?.committedSlots() ?? [],
429
+ };
362
430
  }
363
431
  function parsePublicTreeArtifact(value) {
364
432
  if (!value || typeof value !== 'object')