zk-credits 0.2.0 → 0.2.2

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/dist/cli.js CHANGED
@@ -2,10 +2,11 @@
2
2
  import { homedir } from 'node:os';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { createInterface } from 'node:readline/promises';
5
6
  import { launchCodexProcess } from './codex-launcher.js';
6
7
  import { launchClineProcess } from './cline-launcher.js';
7
8
  import { isCodexProfileInstalled, resolveCodexHome, writeCodexProfile, } from './codex-profile.js';
8
- import { createBasePrepaidClient, createFileWitnessProvider } from './base-sidecar.js';
9
+ import { createBasePrepaidClient, createFileWitnessProvider, validateBaseCreditWitness } from './base-sidecar.js';
9
10
  import { createBaseEventWitnessProvider } from './base-event-sync.js';
10
11
  import { createPinnedBaseProofGenerator } from './proof-coordinator.js';
11
12
  import { createBaseProofMetrics } from './proof-metrics.js';
@@ -16,14 +17,26 @@ import { runCliCommand } from './cli-runtime.js';
16
17
  import { createNodeSidecarLifecycle } from './node-sidecar-lifecycle.js';
17
18
  import { activateSidecarServer } from './server-startup.js';
18
19
  import { createLoopbackToken, sidecarStatePaths } from './sidecar-config.js';
20
+ import { applySetupConfig, readSetupConfig, resolveSetupInputs, writeSetupConfig, } from './setup-config.js';
19
21
  import { ensureSidecarReady } from './sidecar-lifecycle.js';
20
22
  import { readLoopbackToken, writeLoopbackToken } from './sidecar-state.js';
21
23
  import { createSidecarServer } from './sidecar.js';
24
+ import { runBaseRegisteredTrialExchange } from './base-registered-trial.js';
25
+ import { loadCircuitManifest } from './artifact-bundle.js';
26
+ import { BASE_SEPOLIA_NETWORK } from '@zk-credits/x402-zk-prepaid';
22
27
  const DEFAULT_GATEWAY_URL = 'https://zk-credits-gateway.onrender.com';
23
28
  const DEFAULT_PORT = 3210;
24
29
  function stateDirectory() {
25
30
  return process.env.ZK_CREDITS_HOME || join(homedir(), '.zk-credits');
26
31
  }
32
+ function setupConfigPath(directory = stateDirectory()) {
33
+ return join(directory, 'config.json');
34
+ }
35
+ async function applySavedSetupConfig(environment, directory = stateDirectory()) {
36
+ const config = await readSetupConfig(setupConfigPath(directory));
37
+ if (config)
38
+ applySetupConfig(environment, config);
39
+ }
27
40
  function loopbackBaseUrl() {
28
41
  const port = Number(process.env.ZK_CREDITS_SIDECAR_PORT || DEFAULT_PORT);
29
42
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
@@ -41,6 +54,58 @@ function readPort(args) {
41
54
  }
42
55
  return parsed;
43
56
  }
57
+ async function chooseSetupCandidate(kind, candidates) {
58
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
59
+ const variable = kind === 'proving bundle' ? 'ZK_CREDITS_ARTIFACT_DIR' : 'ZK_CREDITS_WITNESS_PATH';
60
+ throw new Error('Found more than one valid local ' + kind + '. Rerun in an interactive terminal or set ' + variable + ' explicitly.');
61
+ }
62
+ const input = createInterface({ input: process.stdin, output: process.stdout });
63
+ try {
64
+ console.log('Found multiple valid ' + kind + 's:');
65
+ candidates.forEach((candidate, index) => console.log(' ' + (index + 1) + ') ' + candidate));
66
+ const answer = await input.question('Select a number: ');
67
+ const index = Number(answer);
68
+ if (!Number.isInteger(index) || index < 1 || index > candidates.length) {
69
+ throw new Error('Choose one of the listed local candidates');
70
+ }
71
+ return candidates[index - 1];
72
+ }
73
+ finally {
74
+ input.close();
75
+ }
76
+ }
77
+ async function witnessProviderForSource(source, cachePath) {
78
+ if (source.kind === 'file') {
79
+ let value;
80
+ try {
81
+ value = JSON.parse(await readFile(source.path, 'utf8'));
82
+ }
83
+ catch {
84
+ throw new Error('The Base witness file could not be read as JSON');
85
+ }
86
+ return createFileWitnessProvider(value);
87
+ }
88
+ return createBaseEventWitnessProvider({
89
+ rpcUrl: source.rpcUrl,
90
+ contractAddress: source.contractAddress,
91
+ deploymentBlock: source.deploymentBlock === undefined ? undefined : BigInt(source.deploymentBlock),
92
+ confirmations: source.confirmations === undefined ? undefined : BigInt(source.confirmations),
93
+ cachePath,
94
+ });
95
+ }
96
+ /** Reads a password written to the private stdin pipe by the foreground CLI. */
97
+ async function readCredentialPasswordFromStdin() {
98
+ const chunks = [];
99
+ let byteLength = 0;
100
+ for await (const chunk of process.stdin) {
101
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
102
+ byteLength += bytes.length;
103
+ if (byteLength > 4096)
104
+ throw new Error('Credential password input is too long');
105
+ chunks.push(bytes);
106
+ }
107
+ return Buffer.concat(chunks).toString('utf8');
108
+ }
44
109
  /** Reads a credential backup password from a TTY without terminal echo or shell history. */
45
110
  async function readHiddenValue(prompt) {
46
111
  if (!process.stdin.isTTY || !process.stdin.setRawMode) {
@@ -54,6 +119,7 @@ async function readHiddenValue(prompt) {
54
119
  const done = (error) => {
55
120
  process.stdin.off('data', onData);
56
121
  process.stdin.setRawMode(false);
122
+ process.stdin.pause();
57
123
  process.stdout.write('\n');
58
124
  if (error)
59
125
  reject(error);
@@ -98,30 +164,34 @@ function printHelp() {
98
164
  zk-credits codex [codex arguments...]
99
165
  zk-credits status
100
166
  zk-credits serve [--port <port>]
167
+ zk-credits trial-registered-adapter
101
168
  eval "$(zk-credits env)"
102
169
 
103
170
  "zk-credits cline" and the Codex companion configure and launch coding agents
104
- against the x402 zk-prepaid loopback sidecar. The pilot serves non-streaming
105
- POST /v1/chat/completions only.
106
- Set ZK_CREDITS_CREDENTIAL_PATH, ZK_CREDITS_CREDENTIAL_PASSWORD, and
107
- ZK_CREDITS_ARTIFACT_DIR for a headless process. The encrypted export is
108
- decrypted only in local memory.`);
171
+ against the x402 zk-prepaid loopback sidecar. The gateway spend path is
172
+ non-streaming POST /v1/chat/completions; Codex Responses requests pass through
173
+ the sidecar's bounded local compatibility bridge.
174
+ "zk-credits trial-registered-adapter" runs one confirmed, billable internal
175
+ trial request through the directly registered official x402 client adapter.
176
+ Set ZK_CREDITS_CREDENTIAL_PATH before setup. The wizard searches Downloads and
177
+ ~/.zk-credits for the pinned bundle and local Base witness, validates both,
178
+ then saves their paths locally. Alternatively, set ZK_CREDITS_ARTIFACT_DIR and
179
+ ZK_CREDITS_WITNESS_PATH explicitly. Setup prompts for the credential password
180
+ without echo; the encrypted export is decrypted only in local memory.`);
109
181
  }
110
- async function serve(args) {
111
- const port = readPort(args);
112
- const statePaths = sidecarStatePaths(stateDirectory());
113
- const localToken = createLoopbackToken();
182
+ /** Loads the same credential, witness, proof, and durable ledger used by serve. */
183
+ async function loadBaseClientRuntime(passwordInput) {
114
184
  const credentialPath = process.env.ZK_CREDITS_CREDENTIAL_PATH;
115
185
  if (!credentialPath)
116
186
  throw new Error('Set ZK_CREDITS_CREDENTIAL_PATH to the encrypted browser export');
117
- const password = process.env.ZK_CREDITS_CREDENTIAL_PASSWORD ?? await readHiddenValue('Credential backup password: ');
187
+ const password = passwordInput ?? await readHiddenValue('Credential backup password: ');
118
188
  const credential = await readEncryptedCredential(credentialPath, password);
119
189
  const witnessPath = process.env.ZK_CREDITS_WITNESS_PATH;
120
190
  const artifactDirectory = process.env.ZK_CREDITS_ARTIFACT_DIR;
121
191
  if (!artifactDirectory) {
122
192
  throw new Error('Set ZK_CREDITS_ARTIFACT_DIR to the directory holding the installed pinned proving bundle');
123
193
  }
124
- const witness = witnessPath
194
+ const witnessProvider = witnessPath
125
195
  ? createFileWitnessProvider(JSON.parse(await readFile(witnessPath, 'utf8')))
126
196
  : process.env.BASE_RPC_URL && process.env.BASE_PRIVATE_CREDIT_BOND_ADDRESS
127
197
  ? createBaseEventWitnessProvider({
@@ -136,14 +206,290 @@ async function serve(args) {
136
206
  const exchangeMetrics = createZkPrepaidLifecycleMetrics();
137
207
  const slotLedger = await BaseSlotLedger.open({ path: join(stateDirectory(), 'base-slots.json') });
138
208
  const prove = await createPinnedBaseProofGenerator({ artifactDirectory, metrics: proofMetrics });
209
+ validateBaseCreditWitness(await witnessProvider.witnessForCredential(credential));
210
+ return { credential, witnessProvider, slotLedger, proofMetrics, exchangeMetrics, prove };
211
+ }
212
+ const GATEWAY_COUNTER_NAMES = [
213
+ 'challenge_issued',
214
+ 'proof_valid',
215
+ 'proof_invalid',
216
+ 'reservation_new',
217
+ 'reservation_existing',
218
+ 'claim_committed',
219
+ 'claim_cancelled',
220
+ 'claim_replayed',
221
+ 'claim_conflict',
222
+ 'dispatch_ok',
223
+ 'dispatch_error',
224
+ 'dispatch_timeout',
225
+ 'cap_exhausted_utc_day',
226
+ 'cap_exhausted_rolling_30d',
227
+ 'paused_rejected',
228
+ 'request_rejected',
229
+ ];
230
+ const GATEWAY_CLAIM_COUNT_NAMES = ['reserved', 'ready', 'committed', 'cancelled'];
231
+ const GATEWAY_READINESS_CHECK_NAMES = new Set(['launchControl', 'database', 'baseRoot', 'baseRpc', 'verifierAssets', 'provider']);
232
+ function isRecord(value) {
233
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
234
+ }
235
+ function safeCount(value) {
236
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
237
+ }
238
+ function gatewayOrigin() {
239
+ const origin = new URL(process.env.ZK_CREDITS_GATEWAY_URL || DEFAULT_GATEWAY_URL);
240
+ if (origin.protocol !== 'https:' || origin.username || origin.password || origin.search || origin.hash || origin.pathname !== '/') {
241
+ throw new Error('The internal trial requires a plain HTTPS gateway origin');
242
+ }
243
+ return origin;
244
+ }
245
+ async function readGatewayJson(url, headers) {
246
+ try {
247
+ const response = await fetch(url, { headers, signal: AbortSignal.timeout(10_000) });
248
+ let body;
249
+ try {
250
+ body = await response.json();
251
+ }
252
+ catch {
253
+ body = undefined;
254
+ }
255
+ return { status: response.status, body };
256
+ }
257
+ catch {
258
+ return { status: null };
259
+ }
260
+ }
261
+ async function gatewayReadiness(origin) {
262
+ const response = await readGatewayJson(new URL('/ready', origin), { accept: 'application/json' });
263
+ const report = isRecord(response.body) ? response.body : undefined;
264
+ const checks = Array.isArray(report?.checks) ? report.checks : [];
265
+ const failedChecks = checks.flatMap((check) => (isRecord(check) && check.ok === true
266
+ ? []
267
+ : [isRecord(check) && typeof check.name === 'string' && GATEWAY_READINESS_CHECK_NAMES.has(check.name) ? check.name : 'unknown']));
268
+ const hasLaunchCheck = checks.some((check) => isRecord(check) && check.name === 'launchControl' && check.ok === true);
269
+ return {
270
+ status: response.status,
271
+ passed: response.status === 200 && report?.ready === true && failedChecks.length === 0 && hasLaunchCheck,
272
+ failedChecks,
273
+ };
274
+ }
275
+ async function gatewayTrialSnapshot(origin, token) {
276
+ const response = await readGatewayJson(new URL('/v1/admin/status', origin), {
277
+ accept: 'application/json',
278
+ authorization: `Bearer ${token}`,
279
+ });
280
+ const body = isRecord(response.body) ? response.body : undefined;
281
+ const launch = isRecord(body?.launchControl) ? body.launchControl : undefined;
282
+ const base = isRecord(body?.base) ? body.base : undefined;
283
+ const spend = isRecord(body?.spend) ? body.spend : undefined;
284
+ const rawMetrics = isRecord(body?.metrics) ? body.metrics : undefined;
285
+ const rawClaims = isRecord(body?.claims) ? body.claims : undefined;
286
+ const metrics = {};
287
+ const claims = {};
288
+ let countersValid = Boolean(rawMetrics && rawClaims);
289
+ for (const name of GATEWAY_COUNTER_NAMES) {
290
+ const count = safeCount(rawMetrics?.[name]);
291
+ if (count === undefined)
292
+ countersValid = false;
293
+ else
294
+ metrics[name] = count;
295
+ }
296
+ for (const name of GATEWAY_CLAIM_COUNT_NAMES) {
297
+ const count = safeCount(rawClaims?.[name]);
298
+ if (count !== undefined)
299
+ claims[name] = count;
300
+ else
301
+ countersValid = false;
302
+ }
303
+ const headroom = (value) => typeof value === 'string' && /^\d+$/u.test(value) && BigInt(value) > 0n;
304
+ const currentRoot = typeof base?.currentRoot === 'string' && base.currentRoot.length > 0 ? base.currentRoot : undefined;
305
+ const knownRoot = currentRoot !== undefined;
306
+ const lag = typeof base?.lagBlocks === 'string' && /^\d+$/u.test(base.lagBlocks) ? BigInt(base.lagBlocks) : undefined;
307
+ const snapshot = countersValid && currentRoot ? { metrics, claims, currentRoot } : undefined;
308
+ return {
309
+ status: response.status,
310
+ ...(snapshot ? { snapshot } : {}),
311
+ gatePassed: response.status === 200
312
+ && body?.network === BASE_SEPOLIA_NETWORK
313
+ && launch?.state === 'enabled'
314
+ && knownRoot
315
+ && safeCount(base?.knownRootCount) !== undefined
316
+ && (safeCount(base?.knownRootCount) ?? 0) > 0
317
+ && lag !== undefined
318
+ && lag <= 300n
319
+ && headroom(spend?.dailyHeadroomMicroUsd)
320
+ && headroom(spend?.rollingHeadroomMicroUsd)
321
+ && countersValid,
322
+ };
323
+ }
324
+ async function confirmDirectTrial() {
325
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
326
+ throw new Error('the internal trial requires a local interactive terminal');
327
+ const input = createInterface({ input: process.stdin, output: process.stdout });
328
+ try {
329
+ const answer = await input.question('Confirm local recovery completed, this slot ledger was not copied to another client, one credit remains, and you approve one billable internal exchange. Type RUN ONE INTERNAL EXCHANGE: ');
330
+ return answer.trim() === 'RUN ONE INTERNAL EXCHANGE';
331
+ }
332
+ finally {
333
+ input.close();
334
+ }
335
+ }
336
+ function gatewayCounterDelta(before, after) {
337
+ return {
338
+ metrics: Object.fromEntries(GATEWAY_COUNTER_NAMES.map((name) => [name, after.metrics[name] - before.metrics[name]])),
339
+ claims: Object.fromEntries(GATEWAY_CLAIM_COUNT_NAMES.map((name) => [name, after.claims[name] - before.claims[name]])),
340
+ };
341
+ }
342
+ async function trialRegisteredAdapter() {
343
+ if (!await confirmDirectTrial()) {
344
+ console.log(JSON.stringify({ path: 'registered-adapter', status: 'not_run' }));
345
+ return;
346
+ }
347
+ try {
348
+ const runtime = await loadBaseClientRuntime();
349
+ if (runtime.credential.tierId !== 0
350
+ || runtime.credential.deploymentDomain !== '84532'
351
+ || runtime.credential.expiry <= Math.floor(Date.now() / 1000)) {
352
+ console.log(JSON.stringify({ path: 'registered-adapter', status: 'stopped', failurePhase: 'credential_gate' }));
353
+ return;
354
+ }
355
+ const startingSlots = runtime.slotLedger.snapshot();
356
+ if (startingSlots.committed >= startingSlots.capacity) {
357
+ console.log(JSON.stringify({
358
+ path: 'registered-adapter',
359
+ status: 'stopped',
360
+ failurePhase: 'local_credit_gate',
361
+ localCredits: { capacity: startingSlots.capacity, committed: startingSlots.committed, available: 0 },
362
+ }));
363
+ return;
364
+ }
365
+ const gateway = gatewayOrigin();
366
+ const adminToken = process.env.BILLING_INTERNAL_TOKEN || await readHiddenValue('Gateway admin token: ');
367
+ if (!adminToken.trim()) {
368
+ console.log(JSON.stringify({ path: 'registered-adapter', status: 'stopped', failurePhase: 'admin_auth' }));
369
+ return;
370
+ }
371
+ const lifecycle = createNodeSidecarLifecycle({
372
+ loopbackBaseUrl: loopbackBaseUrl(),
373
+ stateDirectory: stateDirectory(),
374
+ tokenPath: sidecarStatePaths(stateDirectory()).tokenPath,
375
+ logPath: sidecarStatePaths(stateDirectory()).logPath,
376
+ cliEntryPath: process.argv[1] ?? '',
377
+ });
378
+ const codexHome = resolveCodexHome(process.env, homedir());
379
+ const [sidecarRunning, codexProfileInstalled] = await Promise.all([
380
+ lifecycle.isHealthy(),
381
+ isCodexProfileInstalled(codexHome),
382
+ ]);
383
+ const launcherStatus = { codexProfileInstalled, sidecarRunning };
384
+ if (sidecarRunning) {
385
+ console.log(JSON.stringify({ path: 'registered-adapter', status: 'stopped', failurePhase: 'sidecar_running', launcherStatus }));
386
+ return;
387
+ }
388
+ const witness = await runtime.witnessProvider.witnessForCredential(runtime.credential);
389
+ const readiness = await gatewayReadiness(gateway);
390
+ if (!readiness.passed) {
391
+ console.log(JSON.stringify({ path: 'registered-adapter', status: 'stopped', failurePhase: 'readiness', launcherStatus, readiness }));
392
+ return;
393
+ }
394
+ const adminBefore = await gatewayTrialSnapshot(gateway, adminToken);
395
+ if (!adminBefore.gatePassed || !adminBefore.snapshot) {
396
+ console.log(JSON.stringify({ path: 'registered-adapter', status: 'stopped', failurePhase: 'admin_status', launcherStatus, readiness, adminStatus: adminBefore.status }));
397
+ return;
398
+ }
399
+ if (witness.root !== adminBefore.snapshot.currentRoot) {
400
+ console.log(JSON.stringify({ path: 'registered-adapter', status: 'stopped', failurePhase: 'root_mismatch', launcherStatus, readiness, adminStatus: adminBefore.status }));
401
+ return;
402
+ }
403
+ const baselineSlots = runtime.slotLedger.snapshot();
404
+ const baselineProof = runtime.proofMetrics.snapshot();
405
+ const baselineLifecycle = runtime.exchangeMetrics.snapshot();
406
+ const requestUrl = new URL('/v1/chat/completions', gateway).toString();
407
+ const requestBody = JSON.stringify({
408
+ model: 'deepseek/deepseek-v4-flash',
409
+ messages: [{ role: 'user', content: 'Reply with exactly: internal trial complete.' }],
410
+ stream: false,
411
+ });
412
+ const exchange = await runBaseRegisteredTrialExchange({
413
+ url: requestUrl,
414
+ body: requestBody,
415
+ headers: { 'content-type': 'application/json' },
416
+ credential: runtime.credential,
417
+ witnessProvider: runtime.witnessProvider,
418
+ prove: runtime.prove,
419
+ slotLedger: runtime.slotLedger,
420
+ lifecycle: runtime.exchangeMetrics.observe,
421
+ });
422
+ const adminAfter = await gatewayTrialSnapshot(gateway, adminToken);
423
+ const proofAfter = runtime.proofMetrics.snapshot();
424
+ const lifecycleAfter = runtime.exchangeMetrics.snapshot();
425
+ const slotAfter = runtime.slotLedger.snapshot();
426
+ const localDeltas = {
427
+ proofAttempts: proofAfter.attempts - baselineProof.attempts,
428
+ proofSuccesses: proofAfter.successes - baselineProof.successes,
429
+ proofFailures: proofAfter.failures - baselineProof.failures,
430
+ challengesReceived: lifecycleAfter.challengesReceived - baselineLifecycle.challengesReceived,
431
+ paymentsPrepared: lifecycleAfter.paymentsPrepared - baselineLifecycle.paymentsPrepared,
432
+ settlementsConfirmed: lifecycleAfter.settlementsConfirmed - baselineLifecycle.settlementsConfirmed,
433
+ exchangeSuccesses: lifecycleAfter.exchangeSuccesses - baselineLifecycle.exchangeSuccesses,
434
+ exchangeFailures: lifecycleAfter.failures - baselineLifecycle.failures,
435
+ committedSlots: slotAfter.committed - baselineSlots.committed,
436
+ };
437
+ const counterDelta = adminAfter.snapshot ? gatewayCounterDelta(adminBefore.snapshot, adminAfter.snapshot) : undefined;
438
+ const expectedGatewayCounters = ['challenge_issued', 'proof_valid', 'reservation_new', 'claim_committed', 'dispatch_ok'];
439
+ const gatewayCountersMatch = Boolean(counterDelta)
440
+ && expectedGatewayCounters.every((name) => counterDelta.metrics[name] === 1)
441
+ && Object.entries(counterDelta?.metrics ?? {}).every(([name, value]) => expectedGatewayCounters.includes(name) || value === 0)
442
+ && (counterDelta?.claims.committed === 1)
443
+ && Object.entries(counterDelta?.claims ?? {}).every(([name, value]) => name === 'committed' || value === 0);
444
+ const failurePhase = exchange.failurePhase
445
+ ?? (!exchange.responseSucceeded ? 'response_not_ok' : undefined)
446
+ ?? (adminAfter.status !== 200 || !adminAfter.snapshot ? 'postflight_admin_status' : undefined)
447
+ ?? (!gatewayCountersMatch ? 'gateway_counter_delta' : undefined);
448
+ console.log(JSON.stringify({
449
+ path: 'registered-adapter',
450
+ readiness: { status: readiness.status, passed: readiness.passed, failedChecks: readiness.failedChecks },
451
+ launcherStatus,
452
+ adminStatus: { before: adminBefore.status, after: adminAfter.status },
453
+ localCredits: {
454
+ capacity: baselineSlots.capacity,
455
+ committedBefore: baselineSlots.committed,
456
+ committedAfter: slotAfter.committed,
457
+ availableBefore: baselineSlots.capacity - baselineSlots.committed,
458
+ availableAfter: slotAfter.capacity - slotAfter.committed,
459
+ },
460
+ exchange: {
461
+ challengeStatus: exchange.challengeStatus,
462
+ challengeReceived: exchange.challengeReceived,
463
+ proofPrepared: exchange.proofPrepared,
464
+ paymentSignatureSent: exchange.paymentSignatureSent,
465
+ paymentStatus: exchange.paymentStatus,
466
+ paymentResponseConfirmed: exchange.paymentResponseConfirmed,
467
+ responseStatus: exchange.responseStatus,
468
+ responseSucceeded: exchange.responseSucceeded,
469
+ committedSlotDelta: exchange.committedSlotDelta,
470
+ },
471
+ localCounterDelta: localDeltas,
472
+ gatewayCounterDelta: counterDelta ?? null,
473
+ failurePhase,
474
+ }));
475
+ }
476
+ catch {
477
+ console.log(JSON.stringify({ path: 'registered-adapter', status: 'stopped', failurePhase: 'local_setup' }));
478
+ }
479
+ }
480
+ async function serve(args, passwordInput) {
481
+ const port = readPort(args);
482
+ const statePaths = sidecarStatePaths(stateDirectory());
483
+ const localToken = createLoopbackToken();
484
+ const { credential, witnessProvider, proofMetrics, exchangeMetrics, slotLedger, prove } = await loadBaseClientRuntime(passwordInput);
139
485
  const prepaid = createBasePrepaidClient({
140
486
  credential,
141
- witnessProvider: witness,
487
+ witnessProvider,
142
488
  prove,
143
489
  slotLedger,
144
490
  lifecycle: exchangeMetrics.observe,
145
491
  });
146
- const gatewayBaseUrl = process.env.ZK_CREDITS_GATEWAY_URL || 'http://127.0.0.1:3001';
492
+ const gatewayBaseUrl = process.env.ZK_CREDITS_GATEWAY_URL || DEFAULT_GATEWAY_URL;
147
493
  const sidecar = createSidecarServer({
148
494
  localToken,
149
495
  gatewayBaseUrl,
@@ -176,22 +522,41 @@ async function main() {
176
522
  printHelp();
177
523
  return;
178
524
  }
525
+ const sidecarHome = stateDirectory();
526
+ if (args[0] !== 'setup' && args[0] !== 'status' && args[0] !== 'env') {
527
+ await applySavedSetupConfig(process.env, sidecarHome);
528
+ }
179
529
  if (args[0] === 'serve') {
180
- await serve(args.slice(1));
530
+ const passwordFromStdin = args.includes('--credential-password-stdin')
531
+ ? await readCredentialPasswordFromStdin()
532
+ : undefined;
533
+ await serve(args.slice(1).filter((argument) => argument !== '--credential-password-stdin'), passwordFromStdin);
534
+ return;
535
+ }
536
+ if (args[0] === 'trial-registered-adapter') {
537
+ await trialRegisteredAdapter();
181
538
  return;
182
539
  }
183
- const sidecarHome = stateDirectory();
184
540
  const statePaths = sidecarStatePaths(sidecarHome);
185
541
  const codexHome = resolveCodexHome(process.env, homedir());
186
542
  const cliEntryPath = process.argv[1];
187
543
  if (!cliEntryPath)
188
544
  throw new Error('Unable to resolve the zk-credits executable path');
545
+ let setupCredentialPassword;
189
546
  const lifecycle = createNodeSidecarLifecycle({
190
547
  loopbackBaseUrl: loopbackBaseUrl(),
191
548
  stateDirectory: sidecarHome,
192
549
  tokenPath: statePaths.tokenPath,
193
550
  logPath: statePaths.logPath,
194
551
  cliEntryPath,
552
+ readCredentialPassword: async () => {
553
+ if (setupCredentialPassword !== undefined) {
554
+ const password = setupCredentialPassword;
555
+ setupCredentialPassword = undefined;
556
+ return password;
557
+ }
558
+ return readHiddenValue('Credential backup password: ');
559
+ },
195
560
  });
196
561
  const exitCode = await runCliCommand(args, {
197
562
  loopbackBaseUrl: loopbackBaseUrl(),
@@ -209,6 +574,43 @@ async function main() {
209
574
  return false;
210
575
  }
211
576
  },
577
+ validateSetupPrerequisites: async () => {
578
+ const credentialPath = process.env.ZK_CREDITS_CREDENTIAL_PATH;
579
+ if (!credentialPath?.trim())
580
+ throw new Error('Set ZK_CREDITS_CREDENTIAL_PATH to the encrypted browser export');
581
+ const password = await readHiddenValue('Credential backup password: ');
582
+ const credential = await readEncryptedCredential(credentialPath, password);
583
+ const manifest = await loadCircuitManifest();
584
+ const config = await resolveSetupInputs({
585
+ environment: process.env,
586
+ homeDirectory: homedir(),
587
+ stateDirectory: sidecarHome,
588
+ configPath: setupConfigPath(sidecarHome),
589
+ manifest,
590
+ validateWitness: async (source) => {
591
+ const provider = await witnessProviderForSource(source, join(stateDirectory(), 'base-event-sync.json'));
592
+ validateBaseCreditWitness(await provider.witnessForCredential(credential));
593
+ },
594
+ chooseCandidate: chooseSetupCandidate,
595
+ });
596
+ const artifactDirectory = config.artifactDirectory;
597
+ try {
598
+ await createPinnedBaseProofGenerator({
599
+ artifactDirectory,
600
+ metrics: createBaseProofMetrics(),
601
+ });
602
+ }
603
+ catch (error) {
604
+ if (error instanceof Error)
605
+ throw new Error(`Pinned proving bundle check failed: ${error.message}`);
606
+ throw new Error('Pinned proving bundle check failed. Reinstall the bundle from your pilot invite.');
607
+ }
608
+ await writeSetupConfig(setupConfigPath(sidecarHome), config);
609
+ applySetupConfig(process.env, config);
610
+ setupCredentialPassword = password;
611
+ console.log('Pinned proving bundle and witness validated for this activated credential.');
612
+ console.log('Local setup paths saved to ' + setupConfigPath(sidecarHome));
613
+ },
212
614
  configureCodex: async (model) => {
213
615
  await writeCodexProfile({ codexHome, loopbackBaseUrl: loopbackBaseUrl(), model });
214
616
  },
@@ -4,6 +4,7 @@ export interface DetachedSidecarProcess {
4
4
  args: readonly string[];
5
5
  env: NodeJS.ProcessEnv;
6
6
  logPath: string;
7
+ stdin?: string;
7
8
  }
8
9
  export interface NodeSidecarLifecycleOptions {
9
10
  loopbackBaseUrl: string;
@@ -11,6 +12,7 @@ export interface NodeSidecarLifecycleOptions {
11
12
  tokenPath: string;
12
13
  logPath: string;
13
14
  cliEntryPath: string;
15
+ readCredentialPassword?: () => Promise<string>;
14
16
  }
15
17
  export interface NodeSidecarLifecycleRuntime {
16
18
  fetchHealth(url: string): Promise<Response>;
@@ -2,6 +2,24 @@ import { spawn } from 'node:child_process';
2
2
  import { chmod, mkdir, open } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
4
  import { readLoopbackToken } from './sidecar-state.js';
5
+ const CHILD_ENVIRONMENT_KEYS = [
6
+ 'HOME', 'PATH', 'TMP', 'TEMP', 'TMPDIR', 'LANG', 'LC_ALL', 'TZ',
7
+ 'SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT',
8
+ 'ZK_CREDITS_CREDENTIAL_PATH', 'ZK_CREDITS_ARTIFACT_DIR', 'ZK_CREDITS_WITNESS_PATH',
9
+ 'ZK_CREDITS_GATEWAY_URL', 'BASE_RPC_URL', 'BASE_PRIVATE_CREDIT_BOND_ADDRESS',
10
+ 'BASE_DEPLOYMENT_BLOCK', 'BASE_CONFIRMATIONS',
11
+ ];
12
+ function sidecarEnvironment(stateDirectory, port) {
13
+ const environment = {};
14
+ for (const key of CHILD_ENVIRONMENT_KEYS) {
15
+ const value = process.env[key];
16
+ if (value !== undefined)
17
+ environment[key] = value;
18
+ }
19
+ environment.ZK_CREDITS_HOME = stateDirectory;
20
+ environment.ZK_CREDITS_SIDECAR_PORT = port;
21
+ return environment;
22
+ }
5
23
  async function startDetachedProcess(specification) {
6
24
  await mkdir(dirname(specification.logPath), { recursive: true, mode: 0o700 });
7
25
  const log = await open(specification.logPath, 'a', 0o600);
@@ -10,12 +28,16 @@ async function startDetachedProcess(specification) {
10
28
  const child = spawn(specification.executable, specification.args, {
11
29
  detached: true,
12
30
  env: specification.env,
13
- stdio: ['ignore', log.fd, log.fd],
31
+ stdio: [specification.stdin === undefined ? 'ignore' : 'pipe', log.fd, log.fd],
14
32
  });
15
33
  await new Promise((resolve, reject) => {
16
34
  child.once('spawn', resolve);
17
35
  child.once('error', reject);
18
36
  });
37
+ if (specification.stdin !== undefined && child.stdin) {
38
+ child.stdin.on('error', () => undefined);
39
+ child.stdin.end(specification.stdin);
40
+ }
19
41
  child.unref();
20
42
  }
21
43
  finally {
@@ -48,15 +70,15 @@ export function createNodeSidecarLifecycle(options, runtime = defaultRuntime) {
48
70
  }
49
71
  },
50
72
  async startDetached() {
73
+ if (!options.readCredentialPassword)
74
+ throw new Error('Credential password prompt is unavailable');
75
+ const password = await options.readCredentialPassword();
51
76
  await runtime.startDetachedProcess({
52
77
  executable: process.execPath,
53
- args: [options.cliEntryPath, 'serve', '--port', url.port],
54
- env: {
55
- ...process.env,
56
- ZK_CREDITS_HOME: options.stateDirectory,
57
- ZK_CREDITS_SIDECAR_PORT: url.port,
58
- },
78
+ args: [options.cliEntryPath, 'serve', '--port', url.port, '--credential-password-stdin'],
79
+ env: sidecarEnvironment(options.stateDirectory, url.port),
59
80
  logPath: options.logPath,
81
+ stdin: password,
60
82
  });
61
83
  },
62
84
  readToken: () => readLoopbackToken(options.tokenPath),
@@ -0,0 +1,14 @@
1
+ type JsonRecord = Record<string, unknown>;
2
+ export type ResponsesTranslation = {
3
+ ok: true;
4
+ chatRequest: JsonRecord;
5
+ responseModel: string;
6
+ } | {
7
+ ok: false;
8
+ error: string;
9
+ };
10
+ /** Converts the small Responses surface emitted by Codex into the fixed Chat Completions request. */
11
+ export declare function translateResponsesRequest(body: unknown): ResponsesTranslation;
12
+ /** Builds the Responses SSE event sequence from one committed Chat Completions result. */
13
+ export declare function responsesEventStream(chatValue: unknown, model: string): string;
14
+ export {};