@ola-893/agentpay-cli 0.1.0 → 0.2.0
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 +37 -22
- package/artifacts/agentpay_vault.wasm +0 -0
- package/artifacts/manifest.json +6 -0
- package/package.json +4 -4
- package/src/agent.mjs +49 -0
- package/src/cli.mjs +156 -105
- package/src/config.mjs +8 -14
- package/src/install.mjs +2 -2
- package/src/mcp.mjs +15 -6
- package/src/settings.mjs +101 -0
- package/src/setup.mjs +73 -0
- package/src/ui.mjs +4 -0
- package/ui/assets/{index-B-PcdIha.js → index-DBHTJ1PQ.js} +62 -46
- package/ui/index.html +1 -1
package/src/settings.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { resolve, join } from 'node:path';
|
|
2
|
+
import { mkdir, open, unlink } from 'node:fs/promises';
|
|
3
|
+
import { liveProfile } from './config.mjs';
|
|
4
|
+
import { atomicWrite, readOptional } from './files.mjs';
|
|
5
|
+
import { readConfig, readKey } from '@ola-893/agentpay-core/internal-config';
|
|
6
|
+
import { runtimePolicy, serviceConfig } from '@ola-893/agentpay-core/internal-runtime-policy';
|
|
7
|
+
import { inspectNearProfile, NearAgentPayClient } from '@ola-893/agentpay-core/near';
|
|
8
|
+
|
|
9
|
+
export function usdc(value) {
|
|
10
|
+
if (typeof value !== 'string' || !/^(0|[1-9][0-9]{0,24})(\.[0-9]{1,6})?$/.test(value)) throw new Error('Use a nonnegative USDC amount with at most six decimal places.');
|
|
11
|
+
const [whole, fraction = ''] = value.split('.');
|
|
12
|
+
return (BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, '0'))).toString();
|
|
13
|
+
}
|
|
14
|
+
export async function settings(project) {
|
|
15
|
+
const profile = await liveProfile(project);
|
|
16
|
+
return { profile, config: await readConfig(profile.configPath) };
|
|
17
|
+
}
|
|
18
|
+
export async function showSettings(project) {
|
|
19
|
+
const { profile, config } = await settings(project);
|
|
20
|
+
const { client, summary } = await inspectNearProfile(profile.configPath, profile.stateRoot);
|
|
21
|
+
return { network: config.network, vaultId: config.vaultId, agentId: config.agentId, recipientId: config.recipientId, paymentsEnabled: profile.paymentsEnabled,
|
|
22
|
+
financialPolicy: (await client.getVault()).policy, financialUnits: 'raw USDC (1 USDC = 1000000)', runtimePolicy: config.runtimePolicy,
|
|
23
|
+
service: { url: config.serviceUrl, ...config.service }, inference: profile.inference || null, ...summary };
|
|
24
|
+
}
|
|
25
|
+
export async function setRuntime(project, patch) {
|
|
26
|
+
if (!Object.keys(patch).length) throw new Error('Specify at least one runtime guardrail. See --help.');
|
|
27
|
+
const { profile, config } = await settings(project);
|
|
28
|
+
config.runtimePolicy = runtimePolicy({ ...config.runtimePolicy, ...patch });
|
|
29
|
+
await atomicWrite(profile.configPath, JSON.stringify(config, null, 2));
|
|
30
|
+
return { runtimePolicy: config.runtimePolicy, enforcement: 'local payment worker; thresholds reload on the next checkpoint', restartRequired: 'Restart running agents/MCP servers after changing reuse, polling or exit behavior.' };
|
|
31
|
+
}
|
|
32
|
+
export async function setService(project, patch) {
|
|
33
|
+
const { profile, config } = await settings(project);
|
|
34
|
+
const { summary, client } = await inspectNearProfile(profile.configPath, profile.stateRoot);
|
|
35
|
+
if (summary.activeSession || summary.pendingIntent || summary.executorBusyOrInterrupted) throw new Error('CLOSE_OR_RECONCILE_STREAM_BEFORE_SERVICE_CHANGE');
|
|
36
|
+
const { url, recipient, ...paths } = patch;
|
|
37
|
+
const next = { ...config, serviceUrl: url || config.serviceUrl, recipientId: recipient || config.recipientId, service: serviceConfig({ ...config.service, ...paths }) };
|
|
38
|
+
const temp = profile.configPath + '.candidate.json';
|
|
39
|
+
await atomicWrite(temp, JSON.stringify(next));
|
|
40
|
+
let validated;
|
|
41
|
+
try { validated = await readConfig(temp); } finally { await unlink(temp); }
|
|
42
|
+
if (!(await client.getVault()).policy.recipients.includes(validated.recipientId)) throw new Error('RECIPIENT_NOT_IN_ONCHAIN_POLICY');
|
|
43
|
+
await atomicWrite(profile.configPath, JSON.stringify(validated, null, 2));
|
|
44
|
+
return { service: { url: validated.serviceUrl, recipient: validated.recipientId, ...validated.service }, restartRequired: 'Restart running agents/MCP servers after service changes.' };
|
|
45
|
+
}
|
|
46
|
+
export function inferenceConfig(input) {
|
|
47
|
+
if (!input || typeof input.model !== 'string' || !input.model.trim() || input.model.length > 256) throw new Error('Provide --model.');
|
|
48
|
+
const url = new URL(input.baseUrl);
|
|
49
|
+
if (url.username || url.password || url.search || url.hash || !(url.protocol === 'https:' || (url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)))) throw new Error('Inference URL must use HTTPS or loopback without embedded credentials.');
|
|
50
|
+
if (input.apiKeyEnv && !/^[A-Z][A-Z0-9_]{0,127}$/.test(input.apiKeyEnv)) throw new Error('Provide an environment variable NAME via --api-key-env, never the key itself.');
|
|
51
|
+
const maxSteps = input.maxSteps ?? 12;
|
|
52
|
+
if (!Number.isInteger(maxSteps) || maxSteps < 1 || maxSteps > 100) throw new Error('maxSteps must be between 1 and 100.');
|
|
53
|
+
if (input.thinking !== undefined && typeof input.thinking !== 'boolean') throw new Error('thinking must be true or false.');
|
|
54
|
+
return { protocol: 'openai-chat', baseUrl: url.href.replace(/\/$/, ''), model: input.model, ...(input.apiKeyEnv ? { apiKeyEnv: input.apiKeyEnv } : {}), ...(input.thinking !== undefined ? { thinking: input.thinking } : {}), maxSteps };
|
|
55
|
+
}
|
|
56
|
+
export async function setInference(project, input) {
|
|
57
|
+
const profile = await liveProfile(project);
|
|
58
|
+
const inference = inferenceConfig(input);
|
|
59
|
+
const { file, configPath, stateRoot, ...saved } = profile;
|
|
60
|
+
await atomicWrite(file, JSON.stringify({ ...saved, inference }, null, 2));
|
|
61
|
+
return { inference, credentialsStored: false, next: 'agentpay agent run --prompt "Use the configured paid API to answer my question."' };
|
|
62
|
+
}
|
|
63
|
+
export async function setLimits(project, patch, ownerKeyFile) {
|
|
64
|
+
if (!ownerKeyFile) throw new Error('--owner-key is required for an on-chain policy change; it is never saved in the agent profile.');
|
|
65
|
+
if (!Object.keys(patch).length) throw new Error('Specify at least one financial limit.');
|
|
66
|
+
const { profile, config } = await settings(project);
|
|
67
|
+
const { client: reader, stateDir } = await inspectNearProfile(profile.configPath, profile.stateRoot);
|
|
68
|
+
const owner = await readKey(resolve(ownerKeyFile));
|
|
69
|
+
if (owner.accountId !== config.ownerId) throw new Error('OWNER_CREDENTIAL_MISMATCH');
|
|
70
|
+
const client = new NearAgentPayClient({ vaultId: config.vaultId, accountId: owner.accountId, privateKey: owner.privateKey, rpcUrl: config.rpcUrl });
|
|
71
|
+
await mkdir(stateDir, { recursive: true, mode: 0o700 });
|
|
72
|
+
const lock = await open(join(stateDir, 'operation.lock'), 'wx', 0o600);
|
|
73
|
+
try {
|
|
74
|
+
if (await readOptional(join(stateDir, 'pending.json'))) throw new Error('RECONCILIATION_REQUIRED');
|
|
75
|
+
const vault = await reader.getVault();
|
|
76
|
+
if (vault.active_session) throw new Error('CLOSE_STREAM_BEFORE_POLICY_CHANGE');
|
|
77
|
+
const policy = { ...vault.policy, ...patch };
|
|
78
|
+
if (BigInt(policy.budget) < BigInt(vault.spent) || BigInt(policy.max_deposit) <= 0n || BigInt(policy.max_deposit) > BigInt(policy.budget) || BigInt(policy.max_rate) <= 0n || !Number.isInteger(policy.max_duration_seconds) || policy.max_duration_seconds < 1 || policy.max_duration_seconds > 3600 || !policy.recipients.includes(config.recipientId) || policy.recipients.length > 32 || policy.recipients.some(id => !/^[a-z0-9]+(?:[-_.][a-z0-9]+)*$/.test(id) || id.length < 2 || id.length > 64)) throw new Error('INVALID_FINANCIAL_POLICY');
|
|
79
|
+
const pending = join(stateDir, 'policy-pending.json');
|
|
80
|
+
const journal = await open(pending, 'wx', 0o600);
|
|
81
|
+
try { await journal.writeFile(JSON.stringify({ policy, vaultId: config.vaultId, nonceBefore: vault.next_nonce, createdAt: new Date().toISOString() })); await journal.sync(); } finally { await journal.close(); }
|
|
82
|
+
const receipt = await client.execute('configure', { policy });
|
|
83
|
+
await atomicWrite(join(stateDir, `policy-${receipt.hash}.receipt.json`), JSON.stringify(receipt));
|
|
84
|
+
const actual = (await reader.getVault()).policy;
|
|
85
|
+
if (JSON.stringify(actual) !== JSON.stringify(policy)) throw new Error('POLICY_RECONCILIATION_REQUIRED');
|
|
86
|
+
await unlink(pending);
|
|
87
|
+
return { financialPolicy: actual, units: 'raw USDC', transactionHash: receipt.hash, explorerUrl: `https://testnet.nearblocks.io/txns/${receipt.hash}` };
|
|
88
|
+
} finally { await lock.close(); await unlink(join(stateDir, 'operation.lock')); }
|
|
89
|
+
}
|
|
90
|
+
export async function reconcileLimits(project) {
|
|
91
|
+
const { profile } = await settings(project);
|
|
92
|
+
const { client, stateDir } = await inspectNearProfile(profile.configPath, profile.stateRoot);
|
|
93
|
+
const file = join(stateDir, 'policy-pending.json');
|
|
94
|
+
const text = await readOptional(file);
|
|
95
|
+
if (!text) return { resolved: true, reason: 'NO_PENDING_POLICY' };
|
|
96
|
+
const pending = JSON.parse(text), vault = await client.getVault(), actual = vault.policy;
|
|
97
|
+
if (JSON.stringify(actual) !== JSON.stringify(pending.policy) || pending.nonceBefore === undefined || BigInt(vault.next_nonce) <= BigInt(pending.nonceBefore)) return { resolved: false, reason: 'POLICY_OUTCOME_UNCONFIRMED' };
|
|
98
|
+
await atomicWrite(file.replace('.json', '.reconciled.json'), JSON.stringify({ ...pending, verifiedAt: new Date().toISOString() }));
|
|
99
|
+
await unlink(file);
|
|
100
|
+
return { resolved: true, financialPolicy: actual, receiptRecovered: false };
|
|
101
|
+
}
|
package/src/setup.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { mkdir, readFile, stat, open, unlink } from 'node:fs/promises';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { JsonRpcProvider } from 'near-api-js';
|
|
6
|
+
import { configHome, initProject, liveProfile } from './config.mjs';
|
|
7
|
+
import { atomicWrite, readOptional, safeTarget } from './files.mjs';
|
|
8
|
+
import { readKey, readConfig } from '@ola-893/agentpay-core/internal-config';
|
|
9
|
+
import { NearAgentPayClient } from '@ola-893/agentpay-core/near';
|
|
10
|
+
import { deployTestnet } from '@ola-893/agentpay-core/internal-deployment';
|
|
11
|
+
|
|
12
|
+
const setupId = project => createHash('sha256').update(project).digest('hex');
|
|
13
|
+
export const bundledWasm = fileURLToPath(new URL('../artifacts/agentpay_vault.wasm', import.meta.url));
|
|
14
|
+
export const publicRecording = fileURLToPath(new URL('../ui/near/testnet.json', import.meta.url));
|
|
15
|
+
|
|
16
|
+
export async function setupTestnet(project, options) {
|
|
17
|
+
if (!options.ownerId || !options.ownerKeyFile) return {
|
|
18
|
+
status: 'OWNER_SETUP_REQUIRED', network: 'testnet', signedTransactions: 0,
|
|
19
|
+
next: 'agentpay init --network testnet --owner-id YOUR.testnet --owner-key /absolute/path/owner.json',
|
|
20
|
+
steps: ['Create/export a named NEAR testnet owner account using your wallet or near-cli-rs. Keep its Ed25519 credential JSON private (0600).', 'Fund it with at least 6 test NEAR after storage reserve. The setup creates a vault and restricted agent; recipient defaults to the owner for the local fixture.', 'Run the command above for read-only preflight, then add --deploy to submit setup transactions. No deployment.json or Rust build is required.', 'After setup, use the Circle faucet (Near Testnet) for the printed owner address, then agentpay fund --amount 100000 --owner-key /absolute/path/owner.json.', 'Run Configure the service, inference and financial/runtime guardrails, then enable payments and run agentpay agent run --prompt "...".'],
|
|
21
|
+
nearAccounts: 'https://docs.near.org/tools/cli', usdcFaucet: 'https://faucet.circle.com/',
|
|
22
|
+
noSponsor: true,
|
|
23
|
+
};
|
|
24
|
+
const { deploymentPlan, inspectTestnet } = await import('@ola-893/agentpay-core/internal-preflight');
|
|
25
|
+
const key = await readKey(resolve(options.ownerKeyFile));
|
|
26
|
+
if (key.accountId !== options.ownerId) throw new Error('OWNER_CREDENTIAL_MISMATCH');
|
|
27
|
+
const plan = deploymentPlan({ ownerId: options.ownerId, recipientId: options.recipientId || options.ownerId, vaultLabel: options.vaultLabel, agentLabel: options.agentLabel, rpcUrl: options.rpcUrl, serviceUrl: options.serviceUrl });
|
|
28
|
+
const provider = new JsonRpcProvider({ url: plan.rpcUrl });
|
|
29
|
+
const preflight = await inspectTestnet(plan, { provider, readWasm: () => readFile(bundledWasm), ownerPublicKey: key.publicKey });
|
|
30
|
+
if (!options.deploy) return { ...preflight, next: preflight.readyToDeploy ? 'Repeat this init command with --deploy. Setup uses up to 6 test NEAR, installs owner-controlled vault/admin keys, and does not fund USDC or enable payments.' : 'Resolve the reported checks, then repeat init. No transaction was signed.' };
|
|
31
|
+
if (!preflight.readyToDeploy) throw new Error('TESTNET_PREFLIGHT_FAILED: ' + preflight.checks.filter(c => c.status !== 'pass').map(c => `${c.id}: ${c.detail}`).join('; '));
|
|
32
|
+
const home = configHome();
|
|
33
|
+
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
34
|
+
if (((await stat(home)).mode & 0o077) !== 0) throw new Error('AGENTPAY_HOME must have permissions 0700.');
|
|
35
|
+
const directory = await safeTarget(home, join('setups', setupId(project)));
|
|
36
|
+
const configPath = join(directory, 'deployment.json');
|
|
37
|
+
if (await readOptional(await safeTarget(project, '.agentpay/config.json'))) throw new Error('PROJECT_ALREADY_INITIALIZED');
|
|
38
|
+
const result = await deployTestnet({ ...plan, ownerKeyFile: resolve(options.ownerKeyFile), vaultLabel: options.vaultLabel, agentLabel: options.agentLabel, directory, wasmFile: bundledWasm, onProgress: event => process.stderr.write(`NEAR setup ${event.step}: ${event.transactionHash}\n`) });
|
|
39
|
+
await initProject(project, { deployment: configPath });
|
|
40
|
+
return {
|
|
41
|
+
status: 'DEPLOYED', network: 'testnet', ownerId: result.config.ownerId, vaultId: result.config.vaultId, agentId: result.config.agentId, recipientId: result.config.recipientId,
|
|
42
|
+
paymentsEnabled: false, ownerUSDCFundingAddress: result.config.ownerId, usdcFaucet: 'https://faucet.circle.com/',
|
|
43
|
+
transactions: result.record.steps.map(s => ({ step: s.name, hash: s.hash, explorerUrl: `https://testnet.nearblocks.io/txns/${s.hash}` })),
|
|
44
|
+
privateSetupDirectory: directory, next: 'Get Circle test USDC for the owner (Near Testnet). Then agentpay fund --amount 100000 --owner-key /absolute/path/owner.json. Owner/admin setup credentials are kept separately from the runtime profile.',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function fundProject(project, { ownerKeyFile, amount }) {
|
|
49
|
+
if (!/^[1-9][0-9]{0,20}$/.test(amount ?? '') || BigInt(amount) > 100000n) throw new Error('Provide --amount in raw six-decimal USDC units, at most 100000 (0.1 test USDC) per funding call.');
|
|
50
|
+
if (!ownerKeyFile) throw new Error('--owner-key is required only for this explicit funding operation.');
|
|
51
|
+
const profile = await liveProfile(project);
|
|
52
|
+
const config = await readConfig(profile.configPath);
|
|
53
|
+
const owner = await readKey(resolve(ownerKeyFile));
|
|
54
|
+
if (owner.accountId !== config.ownerId) throw new Error('OWNER_CREDENTIAL_MISMATCH');
|
|
55
|
+
const provider = new JsonRpcProvider({ url: config.rpcUrl });
|
|
56
|
+
if ((await provider.viewNodeStatus()).chain_id !== 'testnet') throw new Error('RPC_NETWORK_MISMATCH');
|
|
57
|
+
const accessKey = await provider.viewAccessKey({ accountId: config.ownerId, publicKey: owner.publicKey, blockQuery: { finality: 'final' } });
|
|
58
|
+
if (accessKey.permission !== 'FullAccess') throw new Error('OWNER_FULL_ACCESS_REQUIRED');
|
|
59
|
+
const client = new NearAgentPayClient({ vaultId: config.vaultId, accountId: owner.accountId, privateKey: owner.privateKey, provider });
|
|
60
|
+
const vault = await client.getVault();
|
|
61
|
+
if (vault.owner_id !== owner.accountId || vault.token_id !== config.tokenId) throw new Error('VAULT_IDENTITY_MISMATCH');
|
|
62
|
+
if (BigInt(await client.tokenBalance(owner.accountId, config.tokenId)) < BigInt(amount)) throw new Error(`OWNER_USDC_INSUFFICIENT: request Near Testnet USDC at https://faucet.circle.com/ for ${owner.accountId}`);
|
|
63
|
+
const pending = join(configHome(), 'funding', createHash('sha256').update(config.vaultId).digest('hex') + '.json');
|
|
64
|
+
await mkdir(join(configHome(), 'funding'), { recursive: true, mode: 0o700 });
|
|
65
|
+
let journal;
|
|
66
|
+
try { journal = await open(pending, 'wx', 0o600); } catch (e) { if (e.code === 'EEXIST') throw new Error(`FUNDING_RECONCILIATION_REQUIRED: inspect ${pending}. No transaction was resubmitted.`); throw e; }
|
|
67
|
+
try { await journal.writeFile(JSON.stringify({ network: config.network, vaultId: config.vaultId, ownerId: owner.accountId, amount, createdAt: new Date().toISOString() })); await journal.sync(); } finally { await journal.close(); }
|
|
68
|
+
const receipt = await client.fund(amount);
|
|
69
|
+
await atomicWrite(pending.replace('.json', `-${receipt.hash}.receipt.json`), JSON.stringify(receipt));
|
|
70
|
+
if (receipt.receiptFailures.length || receipt.value !== amount) throw new Error(`FUNDING_NOT_CONFIRMED: ${receipt.hash}; inspect ${pending}.`);
|
|
71
|
+
await unlink(pending);
|
|
72
|
+
return { network: 'testnet', amountRawUSDC: amount, transactionHash: receipt.hash, explorerUrl: `https://testnet.nearblocks.io/txns/${receipt.hash}`, next: 'agentpay config show; configure service/inference and guardrails, then agentpay payments --enable and agentpay agent run --prompt "..."' };
|
|
73
|
+
}
|
package/src/ui.mjs
CHANGED
|
@@ -3,11 +3,15 @@ import { readFile, realpath } from 'node:fs/promises';
|
|
|
3
3
|
import { resolve, extname, sep } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
|
+
import { verifyRun } from '@ola-893/agentpay-core/internal-evidence-audit';
|
|
6
7
|
|
|
7
8
|
const uiRoot = fileURLToPath(new URL('../ui/', import.meta.url));
|
|
8
9
|
const mime = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff2': 'font/woff2', '.ico': 'image/x-icon' };
|
|
9
10
|
export async function startUi({ port = 0, recording = 'testnet' } = {}) {
|
|
10
11
|
if (!['testnet', 'sandbox'].includes(recording)) throw new Error('Choose --recording testnet or sandbox.');
|
|
12
|
+
const captured = JSON.parse(await readFile(resolve(uiRoot, 'near', `${recording}.json`), 'utf8'));
|
|
13
|
+
verifyRun(captured);
|
|
14
|
+
if (captured.network !== recording || captured.simulated) throw new Error('REPLAY_PROVENANCE_MISMATCH');
|
|
11
15
|
const server = createServer(async (req, res) => {
|
|
12
16
|
const send = (status, body, type = 'text/plain') => {
|
|
13
17
|
res.writeHead(status, { 'content-type': type, 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', 'x-frame-options': 'DENY', 'referrer-policy': 'no-referrer' });
|