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/README.md +74 -42
- package/dist/base-registered-trial.d.ts +33 -0
- package/dist/base-registered-trial.js +168 -0
- package/dist/base-sidecar.d.ts +14 -1
- package/dist/base-sidecar.js +82 -72
- package/dist/cli-runtime.d.ts +1 -0
- package/dist/cli-runtime.js +2 -1
- package/dist/cli.js +418 -16
- package/dist/node-sidecar-lifecycle.d.ts +2 -0
- package/dist/node-sidecar-lifecycle.js +29 -7
- package/dist/responses-bridge.d.ts +14 -0
- package/dist/responses-bridge.js +328 -0
- package/dist/setup-config.d.ts +36 -0
- package/dist/setup-config.js +320 -0
- package/dist/sidecar.js +52 -8
- package/dist/zk-credits.js +1409 -189
- package/package.json +2 -1
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
|
|
105
|
-
POST /v1/chat/completions
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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 =
|
|
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
|
|
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
|
|
487
|
+
witnessProvider,
|
|
142
488
|
prove,
|
|
143
489
|
slotLedger,
|
|
144
490
|
lifecycle: exchangeMetrics.observe,
|
|
145
491
|
});
|
|
146
|
-
const gatewayBaseUrl = process.env.ZK_CREDITS_GATEWAY_URL ||
|
|
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
|
-
|
|
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 {};
|