zk-credits 0.2.4 → 0.2.7

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,70 @@
1
+ /**
2
+ * Freshness and readiness checks used by the separately registered adapter
3
+ * trial. Keep this gate fail-closed when a gateway omits a named check.
4
+ *
5
+ * SPDX-License-Identifier: AGPL-3.0-or-later
6
+ */
7
+ export const GATEWAY_READINESS_CHECK_NAMES = [
8
+ 'launchControl',
9
+ 'database',
10
+ 'baseRoot',
11
+ 'baseRpc',
12
+ 'verifierAssets',
13
+ 'provider',
14
+ ];
15
+ const MAX_STATUS_AGE_MS = 120_000;
16
+ const MAX_FUTURE_SKEW_MS = 30_000;
17
+ function isRecord(value) {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ }
20
+ export function isGatewayStatusFresh(value, now = Date.now()) {
21
+ if (typeof value !== 'string')
22
+ return false;
23
+ const timestamp = Date.parse(value);
24
+ if (!Number.isFinite(timestamp))
25
+ return false;
26
+ const age = now - timestamp;
27
+ return age <= MAX_STATUS_AGE_MS && age >= -MAX_FUTURE_SKEW_MS;
28
+ }
29
+ export function evaluateGatewayReadiness(status, body, now = Date.now()) {
30
+ const report = isRecord(body) ? body : undefined;
31
+ const checks = Array.isArray(report?.checks) ? report.checks : undefined;
32
+ const byName = new Map();
33
+ const failedChecks = [];
34
+ let checksValid = Boolean(checks);
35
+ for (const check of checks ?? []) {
36
+ if (!isRecord(check) || typeof check.name !== 'string' || byName.has(check.name)) {
37
+ checksValid = false;
38
+ continue;
39
+ }
40
+ byName.set(check.name, check);
41
+ if (!GATEWAY_READINESS_CHECK_NAMES.includes(check.name))
42
+ checksValid = false;
43
+ }
44
+ checksValid = checksValid
45
+ && byName.size === GATEWAY_READINESS_CHECK_NAMES.length
46
+ && GATEWAY_READINESS_CHECK_NAMES.every((name) => byName.has(name) && byName.get(name)?.ok === true);
47
+ for (const name of GATEWAY_READINESS_CHECK_NAMES) {
48
+ if (byName.get(name)?.ok !== true)
49
+ failedChecks.push(name);
50
+ }
51
+ if (!checksValid && failedChecks.length === 0)
52
+ failedChecks.push('checks');
53
+ if (report?.ready !== true)
54
+ failedChecks.push('ready');
55
+ if (report?.launchControl !== 'enabled' && !failedChecks.includes('launchControl')) {
56
+ failedChecks.push('launchControl');
57
+ }
58
+ const timestampFresh = isGatewayStatusFresh(report?.generatedAt, now);
59
+ if (!timestampFresh)
60
+ failedChecks.push('generatedAt');
61
+ return {
62
+ passed: status === 200
63
+ && report?.ready === true
64
+ && report.launchControl === 'enabled'
65
+ && checksValid
66
+ && timestampFresh,
67
+ failedChecks,
68
+ timestampFresh,
69
+ };
70
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Local runtime for an operator's own x402 agent.
3
+ *
4
+ * This loads the same encrypted credential, pinned proving artifacts, public
5
+ * Base witness source, proof coordinator, and local slot ledger used by the
6
+ * loopback sidecar. The returned client binds each actual HTTP method, URL,
7
+ * and body into its proof. A loopback-only authenticated metrics endpoint
8
+ * reports aggregate counters for the operator's evidence bundle.
9
+ *
10
+ * SPDX-License-Identifier: AGPL-3.0-or-later
11
+ */
12
+ import { createBasePrepaidClient } from './base-sidecar.js';
13
+ export interface LocalX402AgentOptions {
14
+ /** Decrypts the browser export in local memory. Never pass this through an environment variable. */
15
+ credentialPassword: string;
16
+ environment?: NodeJS.ProcessEnv;
17
+ homeDirectory?: string;
18
+ stateDirectory?: string;
19
+ port?: number;
20
+ }
21
+ export interface LocalX402Agent {
22
+ /** Request-aware client backed by local proving and the x402 adapter. */
23
+ client: ReturnType<typeof createBasePrepaidClient>['client'];
24
+ /** Local authenticated endpoint used only for aggregate activation metrics. */
25
+ metricsUrl: string;
26
+ close(): Promise<void>;
27
+ }
28
+ /**
29
+ * Starts the local proof engine for a participant's own agent integration.
30
+ * Call `client.fetch(url, init)` for each real request, then close the runtime
31
+ * when the agent exits. The local server serves only loopback and requires its
32
+ * owner-only token for the aggregate `/metrics` response.
33
+ */
34
+ export declare function createLocalX402Agent(options: LocalX402AgentOptions): Promise<LocalX402Agent>;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Local runtime for an operator's own x402 agent.
3
+ *
4
+ * This loads the same encrypted credential, pinned proving artifacts, public
5
+ * Base witness source, proof coordinator, and local slot ledger used by the
6
+ * loopback sidecar. The returned client binds each actual HTTP method, URL,
7
+ * and body into its proof. A loopback-only authenticated metrics endpoint
8
+ * reports aggregate counters for the operator's evidence bundle.
9
+ *
10
+ * SPDX-License-Identifier: AGPL-3.0-or-later
11
+ */
12
+ import { readFile } from 'node:fs/promises';
13
+ import { homedir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { decryptAnyCredentialExport } from '@zk-credits/shared/base';
16
+ import { BASE_SEPOLIA_NETWORK, createZkPrepaidLifecycleMetrics, } from '@zk-credits/x402-zk-prepaid';
17
+ import { createBasePrepaidClient, createFileWitnessProvider, validateBaseCreditWitness, } from './base-sidecar.js';
18
+ import { createBaseEventWitnessProvider } from './base-event-sync.js';
19
+ import { createPinnedBaseProofGenerator } from './proof-coordinator.js';
20
+ import { createBaseProofMetrics } from './proof-metrics.js';
21
+ import { BaseSlotLedger } from './slot-ledger.js';
22
+ import { readSetupConfig, applySetupConfig } from './setup-config.js';
23
+ import { createSidecarServer } from './sidecar.js';
24
+ import { createLoopbackToken, sidecarStatePaths } from './sidecar-config.js';
25
+ import { writeLoopbackToken } from './sidecar-state.js';
26
+ const DEFAULT_GATEWAY_URL = 'https://zk-credits-gateway.onrender.com';
27
+ const DEFAULT_PORT = 3210;
28
+ async function witnessProviderForSource(source, cachePath) {
29
+ if (source.kind === 'file') {
30
+ const value = JSON.parse(await readFile(source.path, 'utf8'));
31
+ return createFileWitnessProvider(value);
32
+ }
33
+ return createBaseEventWitnessProvider({
34
+ rpcUrl: source.rpcUrl,
35
+ contractAddress: source.contractAddress,
36
+ deploymentBlock: source.deploymentBlock === undefined ? undefined : BigInt(source.deploymentBlock),
37
+ confirmations: source.confirmations === undefined ? undefined : BigInt(source.confirmations),
38
+ cachePath,
39
+ });
40
+ }
41
+ async function readCredential(path, password) {
42
+ const parsed = JSON.parse(await readFile(path, 'utf8'));
43
+ if (parsed.format !== 'zk-credits-credential' || (parsed.version !== 1 && parsed.version !== 2)) {
44
+ throw new Error('Credential file is not a supported zk-credits export');
45
+ }
46
+ return decryptAnyCredentialExport(parsed, password);
47
+ }
48
+ /**
49
+ * Starts the local proof engine for a participant's own agent integration.
50
+ * Call `client.fetch(url, init)` for each real request, then close the runtime
51
+ * when the agent exits. The local server serves only loopback and requires its
52
+ * owner-only token for the aggregate `/metrics` response.
53
+ */
54
+ export async function createLocalX402Agent(options) {
55
+ const environment = options.environment ?? process.env;
56
+ const homeDirectory = options.homeDirectory ?? homedir();
57
+ const stateDirectory = options.stateDirectory ?? environment.ZK_CREDITS_HOME ?? join(homeDirectory, '.zk-credits');
58
+ const credentialPath = environment.ZK_CREDITS_CREDENTIAL_PATH;
59
+ if (!credentialPath)
60
+ throw new Error('Set ZK_CREDITS_CREDENTIAL_PATH to the encrypted browser export');
61
+ const savedConfig = await readSetupConfig(join(stateDirectory, 'config.json'));
62
+ if (savedConfig)
63
+ applySetupConfig(environment, savedConfig);
64
+ const credential = await readCredential(credentialPath, options.credentialPassword);
65
+ const artifactDirectory = environment.ZK_CREDITS_ARTIFACT_DIR;
66
+ if (!artifactDirectory)
67
+ throw new Error('Run `zk-credits setup codex` first to install and verify the pinned proving bundle');
68
+ const witnessPath = environment.ZK_CREDITS_WITNESS_PATH;
69
+ const rpcUrl = environment.BASE_RPC_URL;
70
+ const contractAddress = environment.BASE_PRIVATE_CREDIT_BOND_ADDRESS;
71
+ const witnessProvider = witnessPath
72
+ ? await witnessProviderForSource({ kind: 'file', path: witnessPath }, join(stateDirectory, 'base-event-sync.json'))
73
+ : rpcUrl && contractAddress
74
+ ? await witnessProviderForSource({
75
+ kind: 'base-events',
76
+ rpcUrl,
77
+ contractAddress,
78
+ ...(environment.BASE_DEPLOYMENT_BLOCK && /^\d+$/u.test(environment.BASE_DEPLOYMENT_BLOCK)
79
+ ? { deploymentBlock: environment.BASE_DEPLOYMENT_BLOCK }
80
+ : {}),
81
+ ...(environment.BASE_CONFIRMATIONS && /^\d+$/u.test(environment.BASE_CONFIRMATIONS)
82
+ ? { confirmations: environment.BASE_CONFIRMATIONS }
83
+ : {}),
84
+ }, join(stateDirectory, 'base-event-sync.json'))
85
+ : (() => { throw new Error('Local setup is missing its Base witness source; rerun `zk-credits setup codex`'); })();
86
+ const proofMetrics = createBaseProofMetrics();
87
+ const exchangeMetrics = createZkPrepaidLifecycleMetrics();
88
+ const slotLedger = await BaseSlotLedger.open({ path: join(stateDirectory, 'base-slots.json') });
89
+ const prove = await createPinnedBaseProofGenerator({ artifactDirectory, metrics: proofMetrics });
90
+ validateBaseCreditWitness(await witnessProvider.witnessForCredential(credential));
91
+ if (credential.deploymentDomain !== '84532' || credential.expiry <= Math.floor(Date.now() / 1000)) {
92
+ throw new Error(`Credential is not active for ${BASE_SEPOLIA_NETWORK}`);
93
+ }
94
+ const prepaid = createBasePrepaidClient({
95
+ credential,
96
+ witnessProvider,
97
+ prove,
98
+ slotLedger,
99
+ lifecycle: exchangeMetrics.observe,
100
+ });
101
+ const localToken = createLoopbackToken();
102
+ const paths = sidecarStatePaths(stateDirectory);
103
+ const metrics = () => ({
104
+ ...proofMetrics.snapshot(),
105
+ exchange: exchangeMetrics.snapshot(),
106
+ });
107
+ const server = createSidecarServer({
108
+ localToken,
109
+ gatewayBaseUrl: environment.ZK_CREDITS_GATEWAY_URL || DEFAULT_GATEWAY_URL,
110
+ prepaidClient: prepaid.client,
111
+ metrics,
112
+ });
113
+ let baseUrl;
114
+ try {
115
+ const port = options.port ?? Number(environment.ZK_CREDITS_SIDECAR_PORT || DEFAULT_PORT);
116
+ if (!Number.isInteger(port) || port < 0 || port > 65535)
117
+ throw new Error('Local metrics port is invalid');
118
+ baseUrl = await server.listen(port);
119
+ await writeLoopbackToken(paths.tokenPath, localToken);
120
+ }
121
+ catch (error) {
122
+ await server.close();
123
+ throw error;
124
+ }
125
+ return {
126
+ client: prepaid.client,
127
+ metricsUrl: `${baseUrl}/metrics`,
128
+ close: async () => server.close(),
129
+ };
130
+ }
@@ -91,6 +91,21 @@ function responseTools(value) {
91
91
  tools.push({ type: 'function', function: definition });
92
92
  continue;
93
93
  }
94
+ // Codex sends its local freeform apply_patch tool with every Responses
95
+ // request. Chat Completions cannot execute that tool, so validate this
96
+ // known declaration and omit it from the provider request.
97
+ if (tool.type === 'custom') {
98
+ if (!hasOnlyKeys(tool, ['type', 'name', 'description', 'format'])
99
+ || tool.name !== 'apply_patch'
100
+ || (tool.description !== undefined && typeof tool.description !== 'string')
101
+ || !isRecord(tool.format)
102
+ || !hasOnlyKeys(tool.format, ['type', 'syntax', 'definition'])
103
+ || tool.format.type !== 'grammar'
104
+ || tool.format.syntax !== 'lark'
105
+ || typeof tool.format.definition !== 'string')
106
+ return undefined;
107
+ continue;
108
+ }
94
109
  // Codex includes these tool declarations in each Responses request. The
95
110
  // fixed service class has no web-search executor and accepts only direct
96
111
  // function tools, so validate their known shape and drop them.
@@ -188,8 +203,9 @@ export function translateResponsesRequest(body) {
188
203
  }
189
204
  chatRequest.max_completion_tokens = body.max_output_tokens;
190
205
  }
191
- if (body.parallel_tool_calls !== undefined)
192
- chatRequest.parallel_tool_calls = body.parallel_tool_calls;
206
+ // Codex sends this optional hint even when its model catalog disables the
207
+ // capability. The pinned provider does not advertise the parameter under
208
+ // the gateway's require_parameters route, so forwarding it prevents dispatch.
193
209
  for (const key of ['temperature', 'top_p', 'stop', 'seed', 'presence_penalty', 'frequency_penalty', 'logit_bias']) {
194
210
  if (body[key] !== undefined)
195
211
  chatRequest[key] = body[key];
@@ -250,8 +266,9 @@ export function responsesEventStream(chatValue, model) {
250
266
  if (text !== null && text !== undefined && typeof text !== 'string') {
251
267
  throw new Error('Gateway returned an unsupported chat content value');
252
268
  }
253
- if (refusal !== undefined && typeof refusal !== 'string')
269
+ if (refusal !== undefined && refusal !== null && typeof refusal !== 'string') {
254
270
  throw new Error('Gateway returned an invalid refusal value');
271
+ }
255
272
  if (typeof text === 'string' || typeof refusal === 'string') {
256
273
  const itemId = `msg_${randomUUID().replaceAll('-', '')}`;
257
274
  const isRefusal = typeof refusal === 'string';
package/dist/sidecar.d.ts CHANGED
@@ -15,6 +15,8 @@ export interface SidecarOptions {
15
15
  localToken: string;
16
16
  gatewayBaseUrl: string;
17
17
  prepaidClient: PrepaidTransport;
18
+ /** Limits this process to one authenticated, valid spend request for the internal trial. */
19
+ internalTrialOneProof?: boolean;
18
20
  /** Aggregate-only proof and exchange metrics served to the authenticated local operator. */
19
21
  metrics?: () => SidecarMetricsSnapshot;
20
22
  }
package/dist/sidecar.js CHANGED
@@ -112,6 +112,7 @@ async function relayResponsesGatewayResponse(res, upstream, responseModel) {
112
112
  export function createSidecarServer(options) {
113
113
  const gatewayBaseUrl = options.gatewayBaseUrl.replace(/\/$/u, '');
114
114
  let server = null;
115
+ let internalTrialRequestClaimed = false;
115
116
  const inFlight = new Set();
116
117
  const handler = async (req, res) => {
117
118
  const pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname;
@@ -173,6 +174,13 @@ export function createSidecarServer(options) {
173
174
  return;
174
175
  }
175
176
  }
177
+ if (options.internalTrialOneProof) {
178
+ if (internalTrialRequestClaimed) {
179
+ sendJson(res, 409, { error: 'internal_trial_limit_reached' });
180
+ return;
181
+ }
182
+ internalTrialRequestClaimed = true;
183
+ }
176
184
  const upstream = await options.prepaidClient.fetch(`${gatewayBaseUrl}/v1/chat/completions`, {
177
185
  method: 'POST',
178
186
  headers: { 'Content-Type': 'application/json' },