@ola-893/agentpay-cli 0.2.0 → 0.4.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/LICENSE +0 -2
- package/README.md +98 -4
- package/package.json +32 -7
- package/src/agent.mjs +19 -7
- package/src/agents.mjs +106 -0
- package/src/cli.mjs +103 -23
- package/src/copy-activity.mjs +56 -0
- package/src/copy-commands.mjs +220 -0
- package/src/copy-feed.mjs +278 -0
- package/src/copy-intents.mjs +83 -0
- package/src/copy-runtime.mjs +369 -0
- package/src/inference.mjs +69 -0
- package/src/install.mjs +7 -3
- package/src/repository-analysis.mjs +150 -0
- package/src/repository-service.mjs +17 -0
- package/src/settings.mjs +5 -11
- package/src/setup.mjs +3 -3
- package/src/ui.mjs +4 -3
- package/src/vault-money.mjs +69 -0
- package/ui/assets/index-Bl23n-ZK.js +331 -0
- package/ui/assets/{index-D7tPv-yG.css → index-BzAc-LMS.css} +1 -1
- package/ui/index.html +2 -2
- package/ui/near/testnet.json +741 -347
- package/ui/assets/index-DBHTJ1PQ.js +0 -331
package/src/settings.mjs
CHANGED
|
@@ -5,6 +5,8 @@ import { atomicWrite, readOptional } from './files.mjs';
|
|
|
5
5
|
import { readConfig, readKey } from '@ola-893/agentpay-core/internal-config';
|
|
6
6
|
import { runtimePolicy, serviceConfig } from '@ola-893/agentpay-core/internal-runtime-policy';
|
|
7
7
|
import { inspectNearProfile, NearAgentPayClient } from '@ola-893/agentpay-core/near';
|
|
8
|
+
import { inferenceConfig } from './inference.mjs';
|
|
9
|
+
export { inferenceConfig } from './inference.mjs';
|
|
8
10
|
|
|
9
11
|
export function usdc(value) {
|
|
10
12
|
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.');
|
|
@@ -43,22 +45,14 @@ export async function setService(project, patch) {
|
|
|
43
45
|
await atomicWrite(profile.configPath, JSON.stringify(validated, null, 2));
|
|
44
46
|
return { service: { url: validated.serviceUrl, recipient: validated.recipientId, ...validated.service }, restartRequired: 'Restart running agents/MCP servers after service changes.' };
|
|
45
47
|
}
|
|
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
48
|
export async function setInference(project, input) {
|
|
57
49
|
const profile = await liveProfile(project);
|
|
58
50
|
const inference = inferenceConfig(input);
|
|
59
51
|
const { file, configPath, stateRoot, ...saved } = profile;
|
|
60
52
|
await atomicWrite(file, JSON.stringify({ ...saved, inference }, null, 2));
|
|
61
|
-
return { inference, credentialsStored: false,
|
|
53
|
+
return { inference, credentialsStored: false,
|
|
54
|
+
...(inference.provider === 'near-ai' ? { attestationVerified: false, policySignerLocation: 'local host' } : {}),
|
|
55
|
+
next: 'agentpay agent run --prompt "Use the configured paid API to answer my question."' };
|
|
62
56
|
}
|
|
63
57
|
export async function setLimits(project, patch, ownerKeyFile) {
|
|
64
58
|
if (!ownerKeyFile) throw new Error('--owner-key is required for an on-chain policy change; it is never saved in the agent profile.');
|
package/src/setup.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export const publicRecording = fileURLToPath(new URL('../ui/near/testnet.json',
|
|
|
16
16
|
export async function setupTestnet(project, options) {
|
|
17
17
|
if (!options.ownerId || !options.ownerKeyFile) return {
|
|
18
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',
|
|
19
|
+
next: (options.setupCommand ?? 'agentpay init') + ' --network testnet --owner-id YOUR.testnet --owner-key /absolute/path/owner.json',
|
|
20
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
21
|
nearAccounts: 'https://docs.near.org/tools/cli', usdcFaucet: 'https://faucet.circle.com/',
|
|
22
22
|
noSponsor: true,
|
|
@@ -45,8 +45,8 @@ export async function setupTestnet(project, options) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
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.');
|
|
48
|
+
export async function fundProject(project, { ownerKeyFile, amount, decimal = false }) {
|
|
49
|
+
if (!/^[1-9][0-9]{0,20}$/.test(amount ?? '') || (!decimal && BigInt(amount) > 100000n)) throw new Error(decimal ? 'Provide a positive test USDC amount.' : 'Provide --amount in raw six-decimal USDC units, at most 100000 (0.1 test USDC) per funding call.');
|
|
50
50
|
if (!ownerKeyFile) throw new Error('--owner-key is required only for this explicit funding operation.');
|
|
51
51
|
const profile = await liveProfile(project);
|
|
52
52
|
const config = await readConfig(profile.configPath);
|
package/src/ui.mjs
CHANGED
|
@@ -7,11 +7,11 @@ import { verifyRun } from '@ola-893/agentpay-core/internal-evidence-audit';
|
|
|
7
7
|
|
|
8
8
|
const uiRoot = fileURLToPath(new URL('../ui/', import.meta.url));
|
|
9
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' };
|
|
10
|
-
export async function startUi({ port = 0, recording = 'testnet' } = {}) {
|
|
10
|
+
export async function startUi({ port = 0, recording = 'testnet', runFile } = {}) {
|
|
11
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'));
|
|
12
|
+
const captured = JSON.parse(await readFile(runFile ? resolve(runFile) : resolve(uiRoot, 'near', `${recording}.json`), 'utf8'));
|
|
13
13
|
verifyRun(captured);
|
|
14
|
-
if (captured.network !== recording || captured.simulated) throw new Error('REPLAY_PROVENANCE_MISMATCH');
|
|
14
|
+
if ((!runFile && captured.network !== recording) || captured.simulated) throw new Error('REPLAY_PROVENANCE_MISMATCH');
|
|
15
15
|
const server = createServer(async (req, res) => {
|
|
16
16
|
const send = (status, body, type = 'text/plain') => {
|
|
17
17
|
res.writeHead(status, { 'content-type': type, 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', 'x-frame-options': 'DENY', 'referrer-policy': 'no-referrer' });
|
|
@@ -23,6 +23,7 @@ export async function startUi({ port = 0, recording = 'testnet' } = {}) {
|
|
|
23
23
|
const decoded = decodeURIComponent(req.url.split('?')[0]);
|
|
24
24
|
if (decoded.split('/').some(x => x === '..' || x.startsWith('.')) || decoded.includes('\\') || decoded.includes('\0')) return send(403, 'Path denied.');
|
|
25
25
|
if (decoded.startsWith('/api/') || decoded === '/api') return send(404, 'No signing API is exposed by this viewer.');
|
|
26
|
+
if (runFile && ['/near/latest.json', '/near/testnet.json', '/near/sandbox.json'].includes(decoded)) return send(200, JSON.stringify(captured), 'application/json');
|
|
26
27
|
let path = ['/', '/docs'].includes(decoded) ? '/index.html' : decoded;
|
|
27
28
|
if (path === '/near/latest.json') path = `/near/${recording === 'sandbox' ? 'sandbox' : 'testnet'}.json`;
|
|
28
29
|
const file = await realpath(resolve(uiRoot, `.${path}`));
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readdir, unlink } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { actions, baseEncode, encodeTransaction, JsonRpcProvider } from 'near-api-js';
|
|
5
|
+
import { atomicWrite, readOptional } from './files.mjs';
|
|
6
|
+
import { fingerprint } from './agents.mjs';
|
|
7
|
+
|
|
8
|
+
const explorer = hash => `https://testnet.nearblocks.io/txns/${hash}`;
|
|
9
|
+
|
|
10
|
+
async function finalize(file, intent, outcome) {
|
|
11
|
+
const tx = outcome.transaction;
|
|
12
|
+
if (outcome.final_execution_status !== 'FINAL' || tx?.hash !== intent.hash || tx.signer_id !== intent.accountId || tx.receiver_id !== intent.vaultId) throw new Error('VAULT_PAYOUT_OUTCOME_UNCONFIRMED');
|
|
13
|
+
const status = outcome.status;
|
|
14
|
+
const ok = status && typeof status === 'object' && 'SuccessValue' in status &&
|
|
15
|
+
Buffer.from(status.SuccessValue, 'base64').toString() === 'true' &&
|
|
16
|
+
!outcome.receipts_outcome?.some(receipt => receipt.outcome.status?.Failure);
|
|
17
|
+
const result = { resolved: true, paid: Boolean(ok), method: intent.method, transactionHash: intent.hash, explorerUrl: explorer(intent.hash) };
|
|
18
|
+
await atomicWrite(join(file, '..', `receipt-${intent.hash}.json`), JSON.stringify(result));
|
|
19
|
+
await unlink(file);
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function payout(agent, client, method, args = {}) {
|
|
24
|
+
const root = join(agent.path, 'payouts');
|
|
25
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
26
|
+
const file = join(root, fingerprint(`${client.vaultId}:${client.accountId}`) + '.pending');
|
|
27
|
+
const lockFile = file + '.lock';
|
|
28
|
+
const lock = await open(lockFile, 'wx', 0o600).catch(error => {
|
|
29
|
+
if (error.code === 'EEXIST') throw new Error('VAULT_PAYOUT_BUSY');
|
|
30
|
+
throw error;
|
|
31
|
+
});
|
|
32
|
+
try {
|
|
33
|
+
const existing = await readOptional(file);
|
|
34
|
+
if (existing) throw new Error(`VAULT_PAYOUT_RECONCILIATION_REQUIRED: ${explorer(JSON.parse(existing).hash)}`);
|
|
35
|
+
const signed = await client.account.createSignedTransaction({ receiverId: client.vaultId, actions: [actions.functionCall(method, args, 100000000000000n)] });
|
|
36
|
+
const hash = baseEncode(createHash('sha256').update(encodeTransaction(signed.transaction)).digest());
|
|
37
|
+
const intent = { hash, method, accountId: client.accountId, vaultId: client.vaultId, rpcUrl: client.provider.connection.url };
|
|
38
|
+
await atomicWrite(file, JSON.stringify(intent));
|
|
39
|
+
let result;
|
|
40
|
+
try { result = await finalize(file, intent, await client.provider.sendTransactionUntil(signed, 'FINAL')); }
|
|
41
|
+
catch { throw new Error(`VAULT_PAYOUT_RECONCILIATION_REQUIRED: ${explorer(hash)}`); }
|
|
42
|
+
if (!result.paid) throw new Error(`VAULT_PAYOUT_FAILED: ${result.explorerUrl}`);
|
|
43
|
+
return result;
|
|
44
|
+
} finally { await lock.close(); await unlink(lockFile); }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function reconcilePayouts(agent) {
|
|
48
|
+
const root = join(agent.path, 'payouts');
|
|
49
|
+
let files;
|
|
50
|
+
try { files = await readdir(root); } catch (error) { if (error.code === 'ENOENT') return { resolved: true, transactions: [] }; throw error; }
|
|
51
|
+
const transactions = [];
|
|
52
|
+
for (const name of files.filter(item => /^[a-f0-9]{64}\.pending$/.test(item))) {
|
|
53
|
+
const file = join(root, name), lockFile = file + '.lock';
|
|
54
|
+
let lock;
|
|
55
|
+
try { lock = await open(lockFile, 'wx', 0o600); }
|
|
56
|
+
catch (error) { if (error.code !== 'EEXIST') throw error; transactions.push({ resolved: false, reason: 'VAULT_PAYOUT_BUSY' }); continue; }
|
|
57
|
+
try {
|
|
58
|
+
const pending = await readOptional(file);
|
|
59
|
+
if (!pending) continue;
|
|
60
|
+
const intent = JSON.parse(pending);
|
|
61
|
+
const provider = new JsonRpcProvider({ url: intent.rpcUrl });
|
|
62
|
+
try {
|
|
63
|
+
const outcome = await provider.viewTransactionStatus({ txHash: intent.hash, accountId: intent.accountId, waitUntil: 'FINAL' });
|
|
64
|
+
transactions.push(await finalize(file, intent, outcome));
|
|
65
|
+
} catch { transactions.push({ resolved: false, transactionHash: intent.hash, explorerUrl: explorer(intent.hash) }); }
|
|
66
|
+
} finally { await lock.close(); await unlink(lockFile); }
|
|
67
|
+
}
|
|
68
|
+
return { resolved: transactions.every(x => x.resolved), transactions };
|
|
69
|
+
}
|