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.
- package/README.md +52 -23
- package/dist/base-registered-trial.d.ts +6 -2
- package/dist/base-registered-trial.js +18 -2
- package/dist/base-sidecar.d.ts +2 -0
- package/dist/base-sidecar.js +13 -1
- package/dist/cli.js +152 -40
- package/dist/codex-profile.js +2 -1
- package/dist/gateway-trial-status.d.ts +13 -0
- package/dist/gateway-trial-status.js +70 -0
- package/dist/local-x402-agent.d.ts +34 -0
- package/dist/local-x402-agent.js +130 -0
- package/dist/responses-bridge.js +20 -3
- package/dist/sidecar.d.ts +2 -0
- package/dist/sidecar.js +8 -0
- package/dist/zk-credits.js +647 -356
- package/package.json +5 -1
|
@@ -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
|
+
}
|
package/dist/responses-bridge.js
CHANGED
|
@@ -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
|
-
|
|
192
|
-
|
|
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' },
|