diff --git a/.env.example b/.env.example index dd19c5c..0723552 100644 --- a/.env.example +++ b/.env.example @@ -7,16 +7,10 @@ VECTOR_KOIOS_URL=https://v2.koios.vector.testnet.apexfusion.org/ VECTOR_SUBMIT_URL=https://submit.vector.testnet.apexfusion.org/api/submit/tx VECTOR_EXPLORER_URL=https://vector.testnet.apexscan.org -# Safety Limits (in lovelace, 1 ADA = 1,000,000 lovelace) -# Scope note (non-custodial migration): the keyless build_* families (wallet/tx/contract, -# agent registry) ignore these - spend limits for them are enforced by the LOCAL signer, per -# user. These server-side limits still govern the one remaining custodial family -# (self-improvement) and will be removed when it goes keyless too. -VECTOR_SPEND_LIMIT_PER_TX=100000000 # 100 ADA per transaction -VECTOR_SPEND_LIMIT_DAILY=500000000 # 500 ADA daily limit - -# Audit Log -VECTOR_AUDIT_LOG_PATH=./vector-audit-log.json # Path for persistent audit log (default: ./vector-audit-log.json) +# Non-custodial migration complete: every builder tool is keyless. Spend +# limits and audit logging are owned entirely by the LOCAL signer, per user - +# see packages/signer/README.md. The server holds no key material and enforces +# no spend limits of its own. # Auth - when set, every request must carry `Authorization: Bearer `. # Comma-separated; each entry is `label:token` or a bare token. diff --git a/packages/builder/src/vector/safety.ts b/packages/builder/src/vector/safety.ts deleted file mode 100644 index 179072e..0000000 --- a/packages/builder/src/vector/safety.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { readFileSync, writeFileSync, existsSync } from 'fs'; -import { resolve, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import type { SpendLimits, SpendStatus, AuditEntry } from '@apexfusion/vector-mcp-shared/types'; - -const VECTOR_SPEND_LIMIT_PER_TX = parseInt(process.env.VECTOR_SPEND_LIMIT_PER_TX || '100000000'); // 100 AP3X -const VECTOR_SPEND_LIMIT_DAILY = parseInt(process.env.VECTOR_SPEND_LIMIT_DAILY || '500000000'); // 500 AP3X - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -// From the bundled entrypoint's directory (packages/builder/build), three levels -// up lands on the repo root, which is where .env.example and the README document -// the default audit log path (./vector-audit-log.json) as living. -const projectRoot = resolve(__dirname, '../../..'); - -export class SafetyLayer { - private dailySpent: number = 0; - private dailyResetTime: number; - private auditLog: AuditEntry[] = []; - private limits: SpendLimits; - private auditLogPath: string; - - constructor() { - this.limits = { - perTransaction: VECTOR_SPEND_LIMIT_PER_TX, - daily: VECTOR_SPEND_LIMIT_DAILY, - }; - // Reset daily limit at midnight UTC - this.dailyResetTime = this.getNextMidnightUTC(); - - // Audit log file persistence - this.auditLogPath = process.env.VECTOR_AUDIT_LOG_PATH - ? resolve(process.env.VECTOR_AUDIT_LOG_PATH) - : resolve(projectRoot, 'vector-audit-log.json'); - - this.loadAuditLog(); - } - - private getNextMidnightUTC(): number { - const now = new Date(); - const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1)); - return tomorrow.getTime(); - } - - private checkAndResetDaily(): void { - if (Date.now() >= this.dailyResetTime) { - this.dailySpent = 0; - this.dailyResetTime = this.getNextMidnightUTC(); - } - } - - private loadAuditLog(): void { - try { - if (existsSync(this.auditLogPath)) { - const data = readFileSync(this.auditLogPath, 'utf-8'); - const parsed = JSON.parse(data); - if (Array.isArray(parsed)) { - this.auditLog = parsed; - // Recalculate dailySpent from today's entries (survives server restart) - this.recalculateDailySpent(); - } - } - } catch (err) { - console.error(`Warning: Could not load audit log from ${this.auditLogPath}:`, err); - this.auditLog = []; - } - } - - private recalculateDailySpent(): void { - const todayStart = new Date(); - todayStart.setUTCHours(0, 0, 0, 0); - const todayStartMs = todayStart.getTime(); - - this.dailySpent = 0; - for (const entry of this.auditLog) { - const entryTime = new Date(entry.timestamp).getTime(); - if (entryTime >= todayStartMs) { - this.dailySpent += entry.amountLovelace; - } - } - } - - private persistAuditLog(): void { - try { - writeFileSync(this.auditLogPath, JSON.stringify(this.auditLog, null, 2)); - } catch (err) { - console.error(`Warning: Could not persist audit log to ${this.auditLogPath}:`, err); - } - } - - checkTransaction(amountLovelace: number): { allowed: boolean; reason?: string } { - this.checkAndResetDaily(); - - if (amountLovelace > this.limits.perTransaction) { - return { - allowed: false, - reason: `Transaction amount ${(amountLovelace / 1_000_000).toFixed(6)} AP3X exceeds per-transaction limit of ${(this.limits.perTransaction / 1_000_000).toFixed(6)} AP3X`, - }; - } - - if (this.dailySpent + amountLovelace > this.limits.daily) { - const remaining = this.limits.daily - this.dailySpent; - return { - allowed: false, - reason: `Transaction would exceed daily spend limit. Daily limit: ${(this.limits.daily / 1_000_000).toFixed(6)} AP3X, already spent: ${(this.dailySpent / 1_000_000).toFixed(6)} AP3X, remaining: ${(remaining / 1_000_000).toFixed(6)} AP3X`, - }; - } - - return { allowed: true }; - } - - recordTransaction(txHash: string, amountLovelace: number, recipient: string): void { - this.checkAndResetDaily(); - this.dailySpent += amountLovelace; - - this.auditLog.push({ - timestamp: new Date().toISOString(), - txHash, - amountLovelace, - recipient, - action: 'send', - }); - - this.persistAuditLog(); - } - - getSpendStatus(): SpendStatus { - this.checkAndResetDaily(); - return { - perTransactionLimit: this.limits.perTransaction, - dailyLimit: this.limits.daily, - dailySpent: this.dailySpent, - dailyRemaining: Math.max(0, this.limits.daily - this.dailySpent), - resetTime: new Date(this.dailyResetTime).toISOString(), - }; - } - - getAuditLog(): AuditEntry[] { - return [...this.auditLog]; - } -} - -// Singleton instance -export const safetyLayer = new SafetyLayer(); diff --git a/packages/builder/src/vector/self-improvement.ts b/packages/builder/src/vector/self-improvement.ts index 889e793..c87bef1 100644 --- a/packages/builder/src/vector/self-improvement.ts +++ b/packages/builder/src/vector/self-improvement.ts @@ -1,276 +1,48 @@ -// @ts-nocheck -// Self-Improvement Module tools: submit proposal, critique, endorse, browse, analyze metrics -// Game 6 of the Vector game theory ecosystem - +// Self-improvement family: 2 read-only tools (browse, analyze_metrics) + 4 +// keyless build_* tools (proposal lock, proposal spend, critique, endorse). +// KEYLESS since spec PR 8: no tool here takes key material. Proposal +// submission is two build_* calls because the deployed module validator +// needs a confirmed lock before the spend+mint step can reference it - the +// agent orchestrates both transactions through the 4-call flow. Constants, +// parsers, and build logic live in gov-build.ts. import { z } from "zod/v4"; -import { Lucid, fromText, toText, Data, Constr, credentialToAddress, getAddressDetails, SLOT_CONFIG_NETWORK } from '@lucid-evolution/lucid'; - -// Vector slot config - resolved from chain via Ogmios queryNetwork/startTime -// Cached after first query so SLOT_CONFIG_NETWORK is set before Lucid needs it -let vectorZeroTime: number | null = null; - -async function ensureSlotConfig(provider: OgmiosProvider): Promise { - if (vectorZeroTime === null) { - vectorZeroTime = await provider.getSystemStartMs(); - SLOT_CONFIG_NETWORK.Mainnet = { zeroTime: vectorZeroTime, zeroSlot: 0, slotLength: 1000 }; - } - return vectorZeroTime; -} -import { blake2b } from '@noble/hashes/blake2b'; -import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { UTxO } from '@lucid-evolution/lucid'; import { OgmiosProvider } from '@apexfusion/vector-mcp-shared/provider'; -import { - VECTOR_OGMIOS_URL, VECTOR_SUBMIT_URL, VECTOR_KOIOS_URL, VECTOR_EXPLORER_URL, explorerTxLink, -} from '@apexfusion/vector-mcp-shared/config'; -import { safetyLayer } from './safety.js'; +import { VECTOR_OGMIOS_URL, VECTOR_SUBMIT_URL, VECTOR_KOIOS_URL } from '@apexfusion/vector-mcp-shared/config'; import { limiterFor } from './rate-limiter.js'; - -// Agent Registry policy ID (shared across all modules) -const AGENT_REGISTRY_POLICY = process.env.AGENT_REGISTRY_POLICY || 'be1a0a2912da180757ed3cd61b56bb8eab0188c19dc3c0e3912d2c01'; - -// Module contract hashes (from deploy_state.json) -// These should be set via env vars in production -const GOV_PROPOSAL_SPEND_HASH = process.env.GOV_PROPOSAL_SPEND_HASH || 'f815f51a76002d6a973e83fecf60f45473e040acee85c631fcce134d'; -const GOV_PROPOSAL_MINT_HASH = process.env.GOV_PROPOSAL_MINT_HASH || 'e8f38052352a3d20c5fe025e2a02d615826a154b26f2239286b8d565'; -const GOV_CRITIQUE_SPEND_HASH = process.env.GOV_CRITIQUE_SPEND_HASH || 'ced52074861af95e2082004d6061b0fc4bb30fded61f9605bfc20e55'; -const GOV_CRITIQUE_MINT_HASH = process.env.GOV_CRITIQUE_MINT_HASH || '2e252a89894d379ce5c0023a57de4627056e4a96da72bd8fedba04bd'; -const GOV_ENDORSEMENT_SPEND_HASH = process.env.GOV_ENDORSEMENT_SPEND_HASH || '5fc449848d85f30287e5bc0bd2b3e95d872ef97be27f1480c12f1a9d'; -const GOV_TREASURY_ADDRESS = process.env.GOV_TREASURY_ADDRESS || 'addr1wx434t2jc3m5uhdf7tq05xjdqu3q5z7a2lhrmn5mapsd43srh7ll8'; - -// Reference script UTxOs (CIP-33) - for validated submit -const GOV_PROPOSAL_SPEND_REF = process.env.GOV_PROPOSAL_SPEND_REF || 'c70a410c9c0c543a0a1103049680b89cbf6fd2277e8469c4d52632dc52b80996#0'; -const GOV_PROPOSAL_MINT_REF = process.env.GOV_PROPOSAL_MINT_REF || 'c40b64fff5c4056689354076aa3d06431d786a4f8f0c1e492e70261d2309e50a#0'; - -// Infrastructure UTxOs (module reference inputs) -const GOV_PARAMS_UTXO = process.env.GOV_PARAMS_UTXO || '2c082e833649175b4a543a5a0cf61f9b736acdfa0d315d1184645185e9a52796#0'; -const GOV_ORACLE_UTXO = process.env.GOV_ORACLE_UTXO || '7a23dfdf9468dd35cee3cad03008f2538c86834d4e5140e0ffaf2ff93e7c04a7#0'; -const GOV_CROSSREFS_UTXO = process.env.GOV_CROSSREFS_UTXO || '96a4acff8be0fb96b3839ee6c9c1fa75809b94f4967218eaf813ac56b939c4b2#0'; - -// Proposal state CBOR constructor tags -const STATE_NAMES: Record = { - 0: 'Open', - 1: 'Amended', - 2: 'Adopted', - 3: 'Rejected', - 4: 'Expired', - 5: 'Withdrawn', -}; - -const TYPE_NAMES: Record = { - 0: 'ParameterChange', - 1: 'TreasurySpend', - 2: 'ProtocolUpgrade', - 3: 'GameActivation', - 4: 'GeneralSuggestion', -}; - -const PRIORITY_NAMES: Record = { - 0: 'Standard', - 1: 'Emergency', -}; - -const CRITIQUE_TYPE_NAMES: Record = { - 0: 'Supportive', - 1: 'Opposing', - 2: 'Amendment', -}; - -// ─── Helpers ───────────────────────────────────────────────────────────────── +import { lucidForAddress } from './build.js'; +import { + GOV_TREASURY_ADDRESS, GOV_PROPOSAL_SPEND_HASH, GOV_CRITIQUE_SPEND_HASH, GOV_ENDORSEMENT_SPEND_HASH, + scriptHashToAddress, lovelaceToApex, + parseProposalDatum, parseCritiqueDatum, parseEndorsementDatum, + MIN_PROPOSAL_STAKE_APEX, MIN_CRITIQUE_STAKE_APEX, MIN_ENDORSE_STAKE_APEX, + buildProposalLock, buildProposalSpend, buildCritique, buildEndorse, +} from './gov-build.js'; +import type { GovProposal } from './gov-build.js'; function newProvider() { return new OgmiosProvider({ ogmiosUrl: VECTOR_OGMIOS_URL, submitUrl: VECTOR_SUBMIT_URL, koiosUrl: VECTOR_KOIOS_URL }); } -function lovelaceToApex(lovelace: number | bigint): string { - return (Number(BigInt(String(lovelace))) / 1_000_000).toFixed(6); -} +const SIGN_AND_SUBMIT_COPY = + 'Sign it locally with vector_signer_sign { txCbor }, broadcast with vector_submit_transaction { signedTxCbor }, then confirm with vector_await_transaction { txHash }.'; -function scriptHashToAddress(hash: string): string { - return credentialToAddress('Mainnet', { type: 'Script', hash }); -} - -// Derive token name: prefix + blake2b_256(CBOR(data))[0..27] -function deriveTokenName(prefix: string, cborHex: string): string { - const hashBytes = blake2b(Buffer.from(cborHex, 'hex'), { dkLen: 32 }); - const prefixHex = Buffer.from(prefix, 'utf-8').toString('hex'); - const hashSlice = Buffer.from(hashBytes).toString('hex').slice(0, 54); // 27 bytes = 54 hex chars - return prefixHex + hashSlice; -} - -function deriveProposalTokenName(txHash: string, outputIndex: number): string { - const outRefCbor = Data.to(new Constr(0, [txHash, BigInt(outputIndex)])); - return deriveTokenName('prop_', outRefCbor); -} - -function deriveActivityTokenName(agentDid: string): string { - const didBytes = Buffer.from(agentDid, 'hex'); - const hashBytes = blake2b(didBytes, { dkLen: 32 }); - const prefixHex = Buffer.from('pact_', 'utf-8').toString('hex'); - const hashSlice = Buffer.from(hashBytes).toString('hex').slice(0, 54); - return prefixHex + hashSlice; -} - -// ─── Validated Submit Helpers ───────────────────────────────────────────────── - -function parseUtxoRef(ref: string): { txHash: string; outputIndex: number } { - const [txHash, idx] = ref.split('#'); - return { txHash, outputIndex: parseInt(idx) }; -} - -async function waitForTx(provider: any, txHash: string, maxAttempts = 30, delayMs = 2000): Promise { - for (let i = 0; i < maxAttempts; i++) { - try { - const utxos = await provider.getUtxosByOutRef([{ txHash, outputIndex: 0 }]); - if (utxos && utxos.length > 0) return; - } catch {} - await new Promise(r => setTimeout(r, delayMs)); - } - throw new Error(`TX ${txHash} not confirmed after ${maxAttempts * delayMs / 1000}s`); -} - -// ─── Filebase IPFS Upload ──────────────────────────────────────────────────── - -const FILEBASE_ACCESS_KEY = process.env.FILEBASE_ACCESS_KEY || ''; -const FILEBASE_SECRET_KEY = process.env.FILEBASE_SECRET_KEY || ''; -const FILEBASE_BUCKET = process.env.FILEBASE_BUCKET || ''; - -async function uploadToFilebase(document: string, namePrefix: string): Promise<{ cid: string; hash: string }> { - if (!FILEBASE_ACCESS_KEY || !FILEBASE_SECRET_KEY || !FILEBASE_BUCKET) { - throw new Error('Filebase not configured. Set FILEBASE_ACCESS_KEY, FILEBASE_SECRET_KEY, and FILEBASE_BUCKET env vars.'); - } - - // Canonical JSON: parse then re-stringify for deterministic bytes - const parsed = JSON.parse(document); - const canonical = JSON.stringify(parsed); - const docBytes = new TextEncoder().encode(canonical); - - // blake2b_256 hash - const hashBytes = blake2b(docBytes, { dkLen: 32 }); - const hashHex = Buffer.from(hashBytes).toString('hex'); - - const key = `${namePrefix}-${hashHex.slice(0, 16)}.json`; - - const s3 = new S3Client({ - region: 'us-east-1', - endpoint: 'https://s3.filebase.com', - credentials: { accessKeyId: FILEBASE_ACCESS_KEY, secretAccessKey: FILEBASE_SECRET_KEY }, - forcePathStyle: true, - }); - - const resp = await s3.send(new PutObjectCommand({ - Bucket: FILEBASE_BUCKET, - Key: key, - Body: canonical, - ContentType: 'application/json', - })); - - // Filebase returns the CID in response metadata - const cid = resp.$metadata?.httpStatusCode === 200 - ? (resp as any).VersionId || '' - : ''; - - // If CID not in response, try HeadObject to get it from x-amz-meta-cid - let finalCid = cid; - if (!finalCid) { - const { HeadObjectCommand } = await import('@aws-sdk/client-s3'); - const head = await s3.send(new HeadObjectCommand({ Bucket: FILEBASE_BUCKET, Key: key })); - finalCid = head.Metadata?.cid || ''; - } - - if (!finalCid) { - throw new Error('Filebase upload succeeded but CID not returned. Check bucket is IPFS-enabled.'); - } - - return { cid: finalCid, hash: hashHex }; -} - -// Parse ProposalDatum from CBOR -function parseProposalDatum(datumCbor: string): any | null { - try { - const c = Data.from(datumCbor); - if (c.fields.length < 12) return null; - - const stateField = c.fields[11]; - const typeField = c.fields[3]; - const priorityField = c.fields[8]; - - return { - proposerDid: c.fields[0], - proposalHash: c.fields[2], - proposalType: TYPE_NAMES[Number(typeField.index)] || 'Unknown', - storageUri: toText(c.fields[4]), - stakeAmount: Number(c.fields[5]), - submittedAt: Number(c.fields[6]), - reviewWindow: Number(c.fields[7]), - priority: PRIORITY_NAMES[Number(priorityField.index)] || 'Standard', - amendmentCount: Number(c.fields[9]), - incorporatedCritiques: c.fields[10]?.length || 0, - state: STATE_NAMES[Number(stateField.index)] || 'Unknown', - }; - } catch { - return null; - } -} - -// Parse CritiqueDatum from CBOR -function parseCritiqueDatum(datumCbor: string): any | null { - try { - const c = Data.from(datumCbor); - if (c.fields.length < 9) return null; - - const critiqueTypeField = c.fields[5]; - const incorporatedField = c.fields[8]; - - return { - criticDid: c.fields[0], - proposalRef: c.fields[2], - critiqueHash: c.fields[3], - storageUri: toText(c.fields[4]), - critiqueType: CRITIQUE_TYPE_NAMES[Number(critiqueTypeField.index)] || 'Unknown', - stakeAmount: Number(c.fields[6]), - submittedAt: Number(c.fields[7]), - incorporated: Number(incorporatedField.index) === 1, - }; - } catch { - return null; - } -} - -// Parse EndorsementDatum from CBOR -function parseEndorsementDatum(datumCbor: string): any | null { - try { - const c = Data.from(datumCbor); - if (c.fields.length < 5) return null; - - return { - endorserDid: c.fields[0], - proposalRef: c.fields[2], - stakeAmount: Number(c.fields[3]), - createdAt: Number(c.fields[4]), - }; - } catch { - return null; - } -} - -// ─── Tool Registration ────────────────────────────────────────────────────── - -export function registerSelfImprovementTools(server, identity) { +export function registerSelfImprovementTools(server: McpServer, identity: string) { const rateLimiter = limiterFor(identity); // Rate limit wrapper - defined inside the register function (not at module - // scope, where it used to live alongside the other helpers above) so this - // closes over the per-identity `rateLimiter` above rather than a - // module-wide global. + // scope) so this closes over the per-identity `rateLimiter` above rather + // than a module-wide global. function checkRateLimit() { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { - return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; + return { content: [{ type: "text" as const, text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; } return null; } - // ─── vector_self_improvement_browse (read-only) ─────────────────────────────── + // ─── vector_self_improvement_browse (read-only) ─────────────────────────── server.tool( "vector_self_improvement_browse", @@ -297,7 +69,7 @@ export function registerSelfImprovementTools(server, identity) { } return { content: [{ - type: "text", + type: "text" as const, text: `# Treasury Balance **Total:** ${lovelaceToApex(total)} AP3X @@ -324,21 +96,20 @@ Each batch UTxO holds ~30 AP3X for adoption rewards.`, parseFunc = parseEndorsementDatum; } - // Get script address from hash - const scriptAddress = credentialToAddress('Mainnet', { type: 'Script', hash: scriptHash }); + const scriptAddress = scriptHashToAddress(scriptHash); - let utxos; + let utxos: UTxO[]; try { utxos = await provider.getUtxos(scriptAddress); } catch { - // Fallback: try Koios address lookup + // Fallback: address query may fail if no UTxOs exist yet utxos = []; } if (!utxos || utxos.length === 0) { return { content: [{ - type: "text", + type: "text" as const, text: `No ${entity} found on-chain.`, }], }; @@ -385,7 +156,7 @@ Each batch UTxO holds ~30 AP3X for adoption rewards.`, }); return { content: [{ - type: "text", + type: "text" as const, text: `# Improvement Proposals (${items.length} found)\n\n${lines.join('\n\n') || 'No proposals match the filters.'}`, }], }; @@ -400,7 +171,7 @@ Each batch UTxO holds ~30 AP3X for adoption rewards.`, }); return { content: [{ - type: "text", + type: "text" as const, text: `# Critiques (${items.length} found)\n\n${lines.join('\n\n') || 'No critiques match the filters.'}`, }], }; @@ -413,7 +184,7 @@ Each batch UTxO holds ~30 AP3X for adoption rewards.`, }); return { content: [{ - type: "text", + type: "text" as const, text: `# Endorsements (${items.length} found)\n\n${lines.join('\n\n') || 'No endorsements match the filters.'}`, }], }; @@ -421,8 +192,8 @@ Each batch UTxO holds ~30 AP3X for adoption rewards.`, } catch (err) { return { content: [{ - type: "text", - text: `Failed to browse proposal data: ${err.message} + type: "text" as const, + text: `Failed to browse proposal data: ${err instanceof Error ? err.message : String(err)} **Troubleshooting Tips:** 1. Verify the module contracts are deployed on this network @@ -434,491 +205,11 @@ Each batch UTxO holds ~30 AP3X for adoption rewards.`, } ); - // ─── vector_self_improvement_submit_proposal ────────────────────────────────── - - server.tool( - "vector_self_improvement_submit_proposal", - "Submit an improvement proposal to the Vector Self-Improvement Module. Requires staking AP3X. Provide proposalDocument (JSON string) for automatic IPFS upload via Filebase and blake2b_256 hashing, OR provide proposalHash and storageUri manually.", - { - mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the wallet"), - agentDid: z.string().describe("Agent DID (hex) - the asset name from Agent Registry NFT"), - proposalDocument: z.string().optional().describe("Full proposal document as JSON string. Uploaded to IPFS via Filebase; hash and CID computed automatically. If provided, proposalHash and storageUri are ignored."), - proposalHash: z.string().optional().describe("blake2b_256 hash of proposal document (64 hex chars). Required if proposalDocument is not provided."), - proposalType: z.enum(["ParameterChange", "TreasurySpend", "ProtocolUpgrade", "GameActivation", "GeneralSuggestion"]).describe("Category of the proposal"), - storageUri: z.string().optional().describe("Off-chain storage URI for the full proposal (IPFS CID or OriginTrail UAL). Required if proposalDocument is not provided."), - stakeApex: z.number().min(25).describe("AP3X to stake (minimum 25)"), - typeParams: z.object({ - paramName: z.string().optional(), - currentValue: z.number().optional(), - proposedValue: z.number().optional(), - amount: z.number().optional(), - recipientDescription: z.string().optional(), - upgradeHash: z.string().optional(), - gameId: z.number().optional(), - }).optional().describe("Type-specific parameters (required for ParameterChange and TreasurySpend)"), - priority: z.enum(["Standard", "Emergency"]).default("Standard").describe("Priority level (Emergency requires higher stake and reputation)"), - }, - async ({ mnemonic, agentDid, proposalDocument, proposalHash, proposalType, storageUri, stakeApex, typeParams, priority }) => { - const rateLimited = checkRateLimit(); - if (rateLimited) return rateLimited; - - const stakeLovelace = stakeApex * 1_000_000; - const safetyCheck = safetyLayer.checkTransaction(stakeLovelace + 4_000_000); - if (!safetyCheck.allowed) { - return { content: [{ type: "text", text: `Safety limit exceeded: ${safetyCheck.reason}. Server spend limits for this tool are set via VECTOR_SPEND_LIMIT_PER_TX / VECTOR_SPEND_LIMIT_DAILY.` }] }; - } - - try { - // IPFS upload: if proposalDocument provided, upload to Filebase and derive hash + URI - let finalHash = proposalHash; - let finalUri = storageUri; - let ipfsCid = ''; - - if (proposalDocument) { - const uploaded = await uploadToFilebase(proposalDocument, 'proposal'); - finalHash = uploaded.hash; - finalUri = `ipfs://${uploaded.cid}`; - ipfsCid = uploaded.cid; - } - - if (!finalHash || finalHash.length !== 64) throw new Error('proposalHash must be 64 hex characters (32 bytes). Provide proposalDocument for auto-hashing or proposalHash manually.'); - if (!finalUri) throw new Error('storageUri is required. Provide proposalDocument for auto-upload or storageUri manually.'); - - const provider = newProvider(); - const zeroTime = await ensureSlotConfig(provider); - const lucid = await Lucid(provider, 'Mainnet'); - lucid.selectWallet.fromSeed(mnemonic.trim()); - const walletAddress = await lucid.wallet().address(); - - // Build proposal type datum - let typeDatum; - switch (proposalType) { - case 'ParameterChange': - if (!typeParams?.paramName || typeParams?.currentValue == null || typeParams?.proposedValue == null) { - throw new Error('ParameterChange requires paramName, currentValue, proposedValue'); - } - typeDatum = new Constr(0, [fromText(typeParams.paramName), BigInt(typeParams.currentValue), BigInt(typeParams.proposedValue)]); - break; - case 'TreasurySpend': - if (!typeParams?.amount || !typeParams?.recipientDescription) { - throw new Error('TreasurySpend requires amount, recipientDescription'); - } - typeDatum = new Constr(1, [BigInt(typeParams.amount), fromText(typeParams.recipientDescription)]); - break; - case 'ProtocolUpgrade': - typeDatum = new Constr(2, [typeParams?.upgradeHash || '']); - break; - case 'GameActivation': - typeDatum = new Constr(3, [BigInt(typeParams?.gameId || 0)]); - break; - default: - typeDatum = new Constr(4, []); - } - - // Get current POSIX time for datum (Plutus validity range uses POSIX ms, not slots) - const tip = await provider.getNetworkTip?.() || { slot: 0 }; - const currentSlot = zeroTime + (tip.slot || 0) * 1000; // POSIX ms - - // Priority datum - const priorityDatum = priority === 'Emergency' ? new Constr(1, []) : new Constr(0, []); - - // Build ProposalDatum - const walletAddr = await lucid.wallet().address(); - const addrDetails = getAddressDetails(walletAddr); - const vkeyHash = addrDetails.paymentCredential?.hash || ''; - - const proposalDatum = Data.to(new Constr(0, [ - agentDid, // proposer_did - new Constr(0, [vkeyHash]), // proposer_credential (VerificationKey) - finalHash, // proposal_hash - typeDatum, // proposal_type - fromText(finalUri), // storage_uri - BigInt(stakeLovelace), // stake_amount - BigInt(currentSlot), // submitted_at - BigInt(604_800_000), // review_window (~7 days in ms) - priorityDatum, // priority - 0n, // amendment_count - [], // incorporated_critiques - new Constr(0, []), // state = Open - ])); - - // === Step 1: Lock ProposalDatum at proposal_spend address === - const proposalSpendAddress = credentialToAddress('Mainnet', { type: 'Script', hash: GOV_PROPOSAL_SPEND_HASH }); - - const lockTx = await lucid.newTx() - .pay.ToAddressWithData( - proposalSpendAddress, - { kind: "inline", value: proposalDatum }, - { lovelace: BigInt(stakeLovelace + 2_000_000) } - ) - .complete({ localUPLCEval: false }); - - const signedLockTx = await lockTx.sign.withWallet().complete(); - const lockTxHash = await signedLockTx.submit(); - - // === Step 2: Spend + Mint (validated submit) === - await waitForTx(provider, lockTxHash); - - // Derive token names - const propTokenName = deriveProposalTokenName(lockTxHash, 0); - const actTokenName = deriveActivityTokenName(agentDid); - const propTokenUnit = GOV_PROPOSAL_MINT_HASH + propTokenName; - const actTokenUnit = GOV_PROPOSAL_MINT_HASH + actTokenName; - - // Get current slot and derive POSIX times for validity range - // The validator checks: activity.last_proposal_slot == tx_validity_lower_bound - const tip2 = await provider.getNetworkTip(); - const spendSlot = tip2.slot; - const validFromMs = zeroTime + spendSlot * 1000; - const validToMs = validFromMs + 360_000; // 6 minutes from spendSlot - - // Activity datum (first proposal: count=1) - // TODO: For subsequent proposals, find existing pact_ UTxO and increment count - const activityDatum = Data.to(new Constr(0, [ - agentDid, // agent_did - new Constr(0, [vkeyHash]), // agent_credential - 1n, // active_proposal_count - BigInt(validFromMs), // last_proposal_slot (POSIX ms, matches tx validity lower bound) - ])); - - // Redeemer: SubmitProposal = Constructor 0 - const submitRedeemer = Data.to(new Constr(0, [])); - - // Get the locked UTxO - const lockedUtxos = await lucid.utxosByOutRef([{ txHash: lockTxHash, outputIndex: 0 }]); - if (!lockedUtxos.length) throw new Error('Locked UTxO not found after confirmation'); - - // Get reference script UTxOs (CIP-33) - const refScriptUtxos = await lucid.utxosByOutRef([ - parseUtxoRef(GOV_PROPOSAL_SPEND_REF), - parseUtxoRef(GOV_PROPOSAL_MINT_REF), - ]); - - // Validate reference scripts are available (they can be accidentally consumed) - if (refScriptUtxos.length < 2) { - throw new Error( - `Reference script UTxOs missing: found ${refScriptUtxos.length}/2. ` + - `spend_ref=${GOV_PROPOSAL_SPEND_REF}, mint_ref=${GOV_PROPOSAL_MINT_REF}. ` + - `These UTxOs may have been consumed - redeploy with scripts/redeploy_ref_scripts.py ` + - `and update GOV_PROPOSAL_SPEND_REF / GOV_PROPOSAL_MINT_REF env vars.` - ); - } - const missingScriptRef = refScriptUtxos.filter(u => !u.scriptRef); - if (missingScriptRef.length > 0) { - throw new Error( - `Reference script UTxOs found but missing scriptRef field for ${missingScriptRef.length} UTxO(s). ` + - `The UTxOs at ${missingScriptRef.map(u => u.txHash).join(', ')} exist but don't contain scripts. ` + - `Redeploy reference scripts and update env vars.` - ); - } - - // Get module infrastructure reference inputs - const govRefUtxos = await lucid.utxosByOutRef([ - parseUtxoRef(GOV_PARAMS_UTXO), - parseUtxoRef(GOV_ORACLE_UTXO), - parseUtxoRef(GOV_CROSSREFS_UTXO), - ]); - - // Find agent's registry NFT UTxO for DID validation (CIP-31 reference input) - // The NFT lives at the registry script address (locked with deposit), not the wallet - const nftUnit = AGENT_REGISTRY_POLICY + agentDid; - let nftUtxo; - try { - nftUtxo = await provider.getUtxoByUnit(nftUnit); - } catch { - throw new Error( - `Agent registry NFT not found on-chain. Expected token: ${AGENT_REGISTRY_POLICY.slice(0, 12)}...${agentDid.slice(0, 12)}... ` + - `The agent may need to re-register with vector_build_register_agent.` - ); - } - - // Build Step 2 transaction: spend locked UTxO + mint tokens - const spendTx = await lucid.newTx() - .collectFrom(lockedUtxos, submitRedeemer) - .readFrom(refScriptUtxos) - .readFrom(govRefUtxos) - .readFrom([nftUtxo]) - .mintAssets( - { [propTokenUnit]: 1n, [actTokenUnit]: 1n }, - submitRedeemer - ) - .pay.ToAddressWithData( - proposalSpendAddress, - { kind: "inline", value: proposalDatum }, - { lovelace: BigInt(stakeLovelace + 2_000_000), [propTokenUnit]: 1n } - ) - .pay.ToAddressWithData( - proposalSpendAddress, - { kind: "inline", value: activityDatum }, - { lovelace: 2_000_000n, [actTokenUnit]: 1n } - ) - .addSigner(walletAddr) - .validFrom(validFromMs) - .validTo(validToMs) - .complete({ localUPLCEval: false }); - - const signedSpendTx = await spendTx.sign.withWallet().complete(); - const spendTxHash = await signedSpendTx.submit(); - - safetyLayer.recordTransaction(spendTxHash, stakeLovelace + 4_000_000, proposalSpendAddress); - - return { - content: [{ - type: "text", - text: `# Proposal Submitted (Validated) - -**Proposal TX (use this for critiques/endorsements):** ${spendTxHash} -**Stake:** ${stakeApex} AP3X -**Type:** ${proposalType} -**Priority:** ${priority} -**Storage:** ${finalUri} -**Proposal Token:** ${propTokenName} -**Activity Token:** ${actTokenName} -**Script Address:** ${proposalSpendAddress} -${ipfsCid ? `**IPFS CID:** ${ipfsCid}\n**Hash (auto-computed):** ${finalHash}` : ''} - -Proposal submitted with on-chain validation. Proposal token (\`prop_\`) and -activity tracking token (\`pact_\`) minted. Visible on the Foundation dashboard. - -> **Note:** When submitting critiques or endorsements, use the Proposal TX hash above as the \`proposalTxHash\`. - -[View on Explorer](${explorerTxLink(spendTxHash)})`, - }], - }; - } catch (err) { - return { - content: [{ - type: "text", - text: `Failed to submit proposal: ${err.message} - -**Troubleshooting Tips:** -1. Ensure wallet has at least ${stakeApex + 5} AP3X (stake + fees) -2. For Emergency proposals, your agent needs Established reputation (100+ AP3X staked in Game 3) -3. proposalHash must be 64 hex characters (blake2b_256 of your proposal document) -4. Server spend limits for this tool are set via VECTOR_SPEND_LIMIT_PER_TX / VECTOR_SPEND_LIMIT_DAILY`, - }], - }; - } - } - ); - - // ─── vector_self_improvement_critique ───────────────────────────────────────── - - server.tool( - "vector_self_improvement_critique", - "Submit a critique on an improvement proposal. Critiques can support, oppose, or propose amendments. Requires staking AP3X. Provide critiqueDocument (JSON string) for automatic IPFS upload, or critiqueHash + storageUri manually.", - { - mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the wallet"), - agentDid: z.string().describe("Agent DID (hex)"), - proposalTxHash: z.string().describe("TX hash of the proposal UTxO to critique"), - // Note: a bare .default() without .optional() is reported as *required* by the - // SDK's isSchemaOptional() under zod/v4 (it returned optional under v3). That - // helper is only used for server.prompt() argument listing, which this codebase - // does not use — harmless for server.tool(), but relevant if prompts are added. - proposalOutputIndex: z.number().default(0).describe("Output index of the proposal UTxO"), - critiqueDocument: z.string().optional().describe("Full critique document as JSON string. Uploaded to IPFS via Filebase; hash and CID computed automatically."), - critiqueHash: z.string().optional().describe("blake2b_256 hash of critique document (64 hex chars). Required if critiqueDocument is not provided."), - critiqueType: z.enum(["Supportive", "Opposing", "Amendment"]).describe("Type of critique"), - storageUri: z.string().optional().describe("Off-chain storage URI for the critique document. Required if critiqueDocument is not provided."), - stakeApex: z.number().min(10).describe("AP3X to stake (minimum 10)"), - }, - async ({ mnemonic, agentDid, proposalTxHash, proposalOutputIndex, critiqueDocument, critiqueHash, critiqueType, storageUri, stakeApex }) => { - const rateLimited = checkRateLimit(); - if (rateLimited) return rateLimited; - - const stakeLovelace = stakeApex * 1_000_000; - const safetyCheck = safetyLayer.checkTransaction(stakeLovelace + 2_000_000); - if (!safetyCheck.allowed) { - return { content: [{ type: "text", text: `Safety limit exceeded: ${safetyCheck.reason}` }] }; - } - - try { - // IPFS upload: if critiqueDocument provided, upload to Filebase - let finalHash = critiqueHash; - let finalUri = storageUri; - let ipfsCid = ''; - - if (critiqueDocument) { - const uploaded = await uploadToFilebase(critiqueDocument, 'critique'); - finalHash = uploaded.hash; - finalUri = `ipfs://${uploaded.cid}`; - ipfsCid = uploaded.cid; - } - - if (!finalHash || finalHash.length !== 64) throw new Error('critiqueHash must be 64 hex characters. Provide critiqueDocument or critiqueHash manually.'); - if (!finalUri) throw new Error('storageUri is required. Provide critiqueDocument or storageUri manually.'); - - const provider = newProvider(); - await ensureSlotConfig(provider); - const lucid = await Lucid(provider, 'Mainnet'); - lucid.selectWallet.fromSeed(mnemonic.trim()); - const walletAddress = await lucid.wallet().address(); - - // Build CritiqueType datum - let critiqueTypeDatum; - switch (critiqueType) { - case 'Supportive': critiqueTypeDatum = new Constr(0, []); break; - case 'Opposing': critiqueTypeDatum = new Constr(1, []); break; - case 'Amendment': critiqueTypeDatum = new Constr(2, [finalHash]); break; - } - - const tip = await provider.getNetworkTip?.() || { slot: 0 }; - const currentSlot = tip.slot || 0; - - const walletAddr = await lucid.wallet().address(); - const addrDetails = getAddressDetails(walletAddr); - const vkeyHash = addrDetails.paymentCredential?.hash || ''; - - // Build CritiqueDatum - const critiqueDatum = Data.to(new Constr(0, [ - agentDid, // critic_did - new Constr(0, [vkeyHash]), // critic_credential - new Constr(0, [proposalTxHash, BigInt(proposalOutputIndex)]), // proposal_ref - finalHash, // critique_hash - fromText(finalUri), // storage_uri - critiqueTypeDatum, // critique_type - BigInt(stakeLovelace), // stake_amount - BigInt(currentSlot), // submitted_at - new Constr(0, []), // incorporated = False - ])); - - const critiqueSpendAddress = credentialToAddress('Mainnet', { type: 'Script', hash: GOV_CRITIQUE_SPEND_HASH }); - - const tx = await lucid.newTx() - .pay.ToAddressWithData( - critiqueSpendAddress, - { kind: "inline", value: critiqueDatum }, - { lovelace: BigInt(stakeLovelace + 2_000_000) } - ) - .complete({ localUPLCEval: false }); - - const signedTx = await tx.sign.withWallet().complete(); - const txHash = await signedTx.submit(); - - safetyLayer.recordTransaction(txHash, stakeLovelace + 2_000_000, critiqueSpendAddress); - - return { - content: [{ - type: "text", - text: `# Critique Submitted - -**Transaction:** ${txHash} -**Type:** ${critiqueType} -**Stake:** ${stakeApex} AP3X -**Proposal:** ${proposalTxHash}#${proposalOutputIndex} -**Storage:** ${finalUri} -${ipfsCid ? `**IPFS CID:** ${ipfsCid}\n**Hash (auto-computed):** ${finalHash}` : ''} - -[View on Explorer](${explorerTxLink(txHash)})`, - }], - }; - } catch (err) { - return { - content: [{ - type: "text", - text: `Failed to submit critique: ${err.message} - -**Troubleshooting Tips:** -1. Ensure wallet has at least ${stakeApex + 3} AP3X -2. Verify the proposal UTxO exists (use vector_self_improvement_browse) -3. critiqueHash must be 64 hex characters (blake2b_256)`, - }], - }; - } - } - ); - - // ─── vector_self_improvement_endorse ────────────────────────────────────────── - - server.tool( - "vector_self_improvement_endorse", - "Endorse an improvement proposal by staking AP3X. Endorsements signal support to the Foundation Council and are weighted by stake amount.", - { - mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the wallet"), - agentDid: z.string().describe("Agent DID (hex)"), - proposalTxHash: z.string().describe("TX hash of the proposal UTxO to endorse"), - proposalOutputIndex: z.number().default(0).describe("Output index of the proposal UTxO"), - stakeApex: z.number().min(5).describe("AP3X to stake as endorsement (minimum 5)"), - }, - async ({ mnemonic, agentDid, proposalTxHash, proposalOutputIndex, stakeApex }) => { - const rateLimited = checkRateLimit(); - if (rateLimited) return rateLimited; - - const stakeLovelace = stakeApex * 1_000_000; - const safetyCheck = safetyLayer.checkTransaction(stakeLovelace + 2_000_000); - if (!safetyCheck.allowed) { - return { content: [{ type: "text", text: `Safety limit exceeded: ${safetyCheck.reason}` }] }; - } - - try { - const provider = newProvider(); - await ensureSlotConfig(provider); - const lucid = await Lucid(provider, 'Mainnet'); - lucid.selectWallet.fromSeed(mnemonic.trim()); - - const walletAddr = await lucid.wallet().address(); - const addrDetails = getAddressDetails(walletAddr); - const vkeyHash = addrDetails.paymentCredential?.hash || ''; - - const tip = await provider.getNetworkTip?.() || { slot: 0 }; - const currentSlot = tip.slot || 0; - - // Build the endorsement datum - const endorsementDatum = Data.to(new Constr(0, [ - agentDid, // endorser_did - new Constr(0, [vkeyHash]), // endorser_credential - new Constr(0, [proposalTxHash, BigInt(proposalOutputIndex)]), // proposal_ref - BigInt(stakeLovelace), // stake_amount - BigInt(currentSlot), // created_at - ])); - - const endorsementSpendAddress = credentialToAddress('Mainnet', { type: 'Script', hash: GOV_ENDORSEMENT_SPEND_HASH }); - - const tx = await lucid.newTx() - .pay.ToAddressWithData( - endorsementSpendAddress, - { kind: "inline", value: endorsementDatum }, - { lovelace: BigInt(stakeLovelace + 2_000_000) } - ) - .complete({ localUPLCEval: false }); - - const signedTx = await tx.sign.withWallet().complete(); - const txHash = await signedTx.submit(); - - safetyLayer.recordTransaction(txHash, stakeLovelace + 2_000_000, endorsementSpendAddress); - - return { - content: [{ - type: "text", - text: `# Endorsement Submitted - -**Transaction:** ${txHash} -**Stake:** ${stakeApex} AP3X -**Proposal:** ${proposalTxHash}#${proposalOutputIndex} - -[View on Explorer](${explorerTxLink(txHash)})`, - }], - }; - } catch (err) { - return { - content: [{ - type: "text", - text: `Failed to submit endorsement: ${err.message} - -**Troubleshooting Tips:** -1. Ensure wallet has at least ${stakeApex + 3} AP3X -2. Verify the proposal UTxO exists and is in Open state`, - }], - }; - } - } - ); - - // ─── vector_self_improvement_analyze_metrics ────────────────────────────────── + // ─── vector_self_improvement_analyze_metrics (read-only) ────────────────── server.tool( "vector_self_improvement_analyze_metrics", - "Analyze proposal metrics: proposal activity, adoption rate, treasury health, and engagement statistics. Read-only - no mnemonic needed.", + "Analyze proposal metrics: proposal activity, adoption rate, treasury health, and engagement statistics. Read-only, takes no key material.", { focus: z.enum(["overview", "adoption", "treasury", "activity"]).default("overview").describe("Analysis focus area"), }, @@ -937,16 +228,16 @@ ${ipfsCid ? `**IPFS CID:** ${ipfsCid}\n**Hash (auto-computed):** ${finalHash}` : } // Query proposals at spend address - let proposalUtxos = []; + let proposalUtxos: UTxO[] = []; try { - const proposalSpendAddress = credentialToAddress('Mainnet', { type: 'Script', hash: GOV_PROPOSAL_SPEND_HASH }); + const proposalSpendAddress = scriptHashToAddress(GOV_PROPOSAL_SPEND_HASH); proposalUtxos = await provider.getUtxos(proposalSpendAddress); } catch { // Address query may fail if no UTxOs exist } // Parse proposal datums - const proposals: any[] = []; + const proposals: GovProposal[] = []; for (const u of proposalUtxos) { if (!u.datum) continue; const parsed = parseProposalDatum(u.datum); @@ -972,7 +263,7 @@ ${ipfsCid ? `**IPFS CID:** ${ipfsCid}\n**Hash (auto-computed):** ${finalHash}` : if (focus === "treasury") { return { content: [{ - type: "text", + type: "text" as const, text: `# Treasury Health **Balance:** ${lovelaceToApex(treasuryTotal)} AP3X @@ -986,7 +277,7 @@ ${Number(treasuryTotal) < 2_500_000_000 ? '**WARNING:** Treasury below 2,500 AP3 return { content: [{ - type: "text", + type: "text" as const, text: `# Proposal Metrics${focus !== 'overview' ? ` (${focus})` : ''} ## Proposals @@ -1010,8 +301,8 @@ ${Number(treasuryTotal) < 2_500_000_000 ? '\n**WARNING:** Treasury below alert t } catch (err) { return { content: [{ - type: "text", - text: `Failed to analyze proposal metrics: ${err.message} + type: "text" as const, + text: `Failed to analyze proposal metrics: ${err instanceof Error ? err.message : String(err)} **Troubleshooting Tips:** 1. Verify Ogmios endpoint is reachable @@ -1021,4 +312,245 @@ ${Number(treasuryTotal) < 2_500_000_000 ? '\n**WARNING:** Treasury below alert t } } ); + + // ─── vector_build_self_improvement_proposal_lock - KEYLESS (step 1 of 2) ── + + server.tool( + "vector_build_self_improvement_proposal_lock", + "Build an UNSIGNED transaction that locks an improvement-proposal stake (step 1 of 2). Requires staking at least 25 AP3X. Provide proposalDocument for automatic IPFS upload, or proposalHash and storageUri manually. Takes no key material. Sign it locally with vector_signer_sign, broadcast with vector_submit_transaction, then confirm with vector_await_transaction. After this confirms, call this tool's counterpart, vector_build_self_improvement_proposal_spend, with the lock transaction hash to complete the submission.", + { + changeAddress: z.string().describe("Your wallet address (holds the stake AP3X; receives change). Get it from vector_signer_get_address."), + agentDid: z.string().describe("Agent DID (hex) - the asset name from Agent Registry NFT"), + proposalType: z.enum(["ParameterChange", "TreasurySpend", "ProtocolUpgrade", "GameActivation", "GeneralSuggestion"]).describe("Category of the proposal"), + stakeApex: z.number().min(MIN_PROPOSAL_STAKE_APEX).describe(`AP3X to stake (minimum ${MIN_PROPOSAL_STAKE_APEX})`), + typeParams: z.object({ + paramName: z.string().optional(), + currentValue: z.number().optional(), + proposedValue: z.number().optional(), + amount: z.number().optional(), + recipientDescription: z.string().optional(), + upgradeHash: z.string().optional(), + gameId: z.number().optional(), + }).optional().describe("Type-specific parameters (required for ParameterChange and TreasurySpend)"), + priority: z.enum(["Standard", "Emergency"]).default("Standard").describe("Priority level (Emergency requires higher stake and reputation)"), + proposalDocument: z.string().optional().describe("Full proposal document as JSON string. Uploaded to IPFS via Filebase; hash and CID computed automatically. If provided, proposalHash and storageUri are ignored."), + proposalHash: z.string().optional().describe("blake2b_256 hash of proposal document (64 hex chars). Required if proposalDocument is not provided."), + storageUri: z.string().optional().describe("Off-chain storage URI for the full proposal (IPFS CID or OriginTrail UAL). Required if proposalDocument is not provided."), + }, + async ({ changeAddress, agentDid, proposalType, stakeApex, typeParams, priority, proposalDocument, proposalHash, storageUri }) => { + const rateLimited = checkRateLimit(); + if (rateLimited) return rateLimited; + try { + const lucid = await lucidForAddress(newProvider(), changeAddress); + const r = await buildProposalLock(lucid, { + agentDid, proposalType, stakeApex, typeParams, priority, proposalDocument, proposalHash, storageUri, + }); + return { + content: [{ + type: "text" as const, + text: `# Unsigned Proposal Lock Built (Not Submitted) (step 1 of 2) + +**Stake:** ${stakeApex} AP3X +**Type:** ${proposalType} +**Priority:** ${priority} +**Storage:** ${r.storageUri} +**Hash:** ${r.proposalHash} +**Script Address:** ${r.scriptAddress} +**Estimated Fee:** ${r.feeAda} AP3X +${r.ipfsCid ? `**IPFS CID:** ${r.ipfsCid}\n` : ''} +**Unsigned TX CBOR:** +\`\`\` +${r.txCbor} +\`\`\` + +${SIGN_AND_SUBMIT_COPY} After it confirms, call vector_build_self_improvement_proposal_spend { changeAddress, agentDid, lockTxHash: } to complete the submission.`, + }], + }; + } catch (err) { + return { + content: [{ + type: "text" as const, + text: `Failed to build proposal lock: ${err instanceof Error ? err.message : String(err)} + +**Troubleshooting Tips:** +1. Ensure the changeAddress wallet holds at least ${stakeApex + 3} AP3X (stake + module minimum + fees). +2. For Emergency priority, the agent needs Established reputation (100+ AP3X staked elsewhere in the network). +3. proposalHash must be 64 hex characters (blake2b_256 of the proposal document). +4. Your local signer's per-transaction limit must allow the stake plus fees.`, + }], + }; + } + } + ); + + // ─── vector_build_self_improvement_proposal_spend - KEYLESS (step 2 of 2) ─ + + server.tool( + "vector_build_self_improvement_proposal_spend", + "Build an UNSIGNED transaction that completes an improvement-proposal submission (step 2 of 2): spends the locked stake through the module validator and mints the proposal and activity tokens. Requires the confirmed lock transaction hash from vector_build_self_improvement_proposal_lock. Takes no key material. Sign it locally with vector_signer_sign, broadcast with vector_submit_transaction, then confirm with vector_await_transaction.", + { + changeAddress: z.string().describe("The SAME wallet address used to build the proposal lock. Get it from vector_signer_get_address."), + agentDid: z.string().describe("Agent DID (hex) - must match the proposer DID in the locked proposal"), + lockTxHash: z.string().describe("The CONFIRMED transaction hash from the proposal lock step"), + lockOutputIndex: z.number().default(0).describe("Output index of the locked proposal UTxO (default: 0)"), + }, + async ({ changeAddress, agentDid, lockTxHash, lockOutputIndex }) => { + const rateLimited = checkRateLimit(); + if (rateLimited) return rateLimited; + try { + const provider = newProvider(); + const lucid = await lucidForAddress(provider, changeAddress); + const r = await buildProposalSpend(lucid, provider, { agentDid, lockTxHash, lockOutputIndex }); + return { + content: [{ + type: "text" as const, + text: `# Unsigned Proposal Submission Built (Not Submitted) (step 2 of 2) + +**Proposal Token:** ${r.proposalTokenName} +**Activity Token:** ${r.activityTokenName} +**Script Address:** ${r.scriptAddress} +**Estimated Fee:** ${r.feeAda} AP3X + +**Unsigned TX CBOR:** +\`\`\` +${r.txCbor} +\`\`\` + +This transaction is valid until ${new Date(r.validToMs).toISOString()} (about 6 minutes). Sign and submit it before then, or rebuild. ${SIGN_AND_SUBMIT_COPY} Once this confirms, use THIS transaction's hash as proposalTxHash for critiques and endorsements.`, + }], + }; + } catch (err) { + return { + content: [{ + type: "text" as const, + text: `Failed to build proposal submission: ${err instanceof Error ? err.message : String(err)} + +**Troubleshooting Tips:** +1. Confirm the lock transaction first with vector_await_transaction { txHash } before calling this tool. +2. The changeAddress must be the same wallet that built the proposal lock. +3. Ensure the changeAddress wallet holds a few AP3X beyond the locked stake, to cover the activity-token output (2 AP3X) and fees.`, + }], + }; + } + } + ); + + // ─── vector_build_self_improvement_critique - KEYLESS ───────────────────── + + server.tool( + "vector_build_self_improvement_critique", + "Build an UNSIGNED transaction that submits a critique on an improvement proposal (Supportive, Opposing, or Amendment). Requires staking at least 10 AP3X. Provide critiqueDocument for automatic IPFS upload, or critiqueHash and storageUri manually. Takes no key material. Sign it locally with vector_signer_sign, broadcast with vector_submit_transaction, then confirm with vector_await_transaction. Find a proposalTxHash to critique with vector_self_improvement_browse.", + { + changeAddress: z.string().describe("Your wallet address (holds the stake AP3X; receives change). Get it from vector_signer_get_address."), + agentDid: z.string().describe("Agent DID (hex)"), + proposalTxHash: z.string().describe("TX hash of the proposal UTxO to critique"), + // Note: a bare .default() without .optional() is reported as *required* by the + // SDK's isSchemaOptional() under zod/v4 (it returned optional under v3). That + // helper is only used for server.prompt() argument listing, which this codebase + // does not use - harmless for server.tool(), but relevant if prompts are added. + proposalOutputIndex: z.number().default(0).describe("Output index of the proposal UTxO"), + critiqueDocument: z.string().optional().describe("Full critique document as JSON string. Uploaded to IPFS via Filebase; hash and CID computed automatically."), + critiqueHash: z.string().optional().describe("blake2b_256 hash of critique document (64 hex chars). Required if critiqueDocument is not provided."), + critiqueType: z.enum(["Supportive", "Opposing", "Amendment"]).describe("Type of critique"), + storageUri: z.string().optional().describe("Off-chain storage URI for the critique document. Required if critiqueDocument is not provided."), + stakeApex: z.number().min(MIN_CRITIQUE_STAKE_APEX).describe(`AP3X to stake (minimum ${MIN_CRITIQUE_STAKE_APEX})`), + }, + async ({ changeAddress, agentDid, proposalTxHash, proposalOutputIndex, critiqueDocument, critiqueHash, critiqueType, storageUri, stakeApex }) => { + const rateLimited = checkRateLimit(); + if (rateLimited) return rateLimited; + try { + const lucid = await lucidForAddress(newProvider(), changeAddress); + const r = await buildCritique(lucid, { + agentDid, proposalTxHash, proposalOutputIndex, critiqueType, stakeApex, critiqueDocument, critiqueHash, storageUri, + }); + const storageDisplay = r.ipfsCid ? `ipfs://${r.ipfsCid}` : storageUri; + return { + content: [{ + type: "text" as const, + text: `# Unsigned Critique Built (Not Submitted) + +**Type:** ${critiqueType} +**Stake:** ${stakeApex} AP3X +**Proposal:** ${proposalTxHash}#${proposalOutputIndex} +**Storage:** ${storageDisplay} +${r.ipfsCid ? `**IPFS CID:** ${r.ipfsCid}\n**Hash (auto-computed):** ${r.documentHash}\n` : ''} +**Script Address:** ${r.scriptAddress} +**Estimated Fee:** ${r.feeAda} AP3X + +**Unsigned TX CBOR:** +\`\`\` +${r.txCbor} +\`\`\` + +${SIGN_AND_SUBMIT_COPY}`, + }], + }; + } catch (err) { + return { + content: [{ + type: "text" as const, + text: `Failed to build critique: ${err instanceof Error ? err.message : String(err)} + +**Troubleshooting Tips:** +1. Ensure the changeAddress wallet holds at least ${stakeApex + 3} AP3X (stake + fees). +2. Verify the proposal UTxO exists with vector_self_improvement_browse. +3. critiqueHash must be 64 hex characters (blake2b_256 of the critique document). +4. Your local signer's per-transaction limit must allow the stake plus fees.`, + }], + }; + } + } + ); + + // ─── vector_build_self_improvement_endorse - KEYLESS ────────────────────── + + server.tool( + "vector_build_self_improvement_endorse", + "Build an UNSIGNED transaction that endorses an improvement proposal by staking at least 5 AP3X. Endorsements signal support to the Foundation Council and are weighted by stake amount. Takes no key material. Sign it locally with vector_signer_sign, broadcast with vector_submit_transaction, then confirm with vector_await_transaction. Find a proposalTxHash to endorse with vector_self_improvement_browse.", + { + changeAddress: z.string().describe("Your wallet address (holds the stake AP3X; receives change). Get it from vector_signer_get_address."), + agentDid: z.string().describe("Agent DID (hex)"), + proposalTxHash: z.string().describe("TX hash of the proposal UTxO to endorse"), + proposalOutputIndex: z.number().default(0).describe("Output index of the proposal UTxO"), + stakeApex: z.number().min(MIN_ENDORSE_STAKE_APEX).describe(`AP3X to stake as endorsement (minimum ${MIN_ENDORSE_STAKE_APEX})`), + }, + async ({ changeAddress, agentDid, proposalTxHash, proposalOutputIndex, stakeApex }) => { + const rateLimited = checkRateLimit(); + if (rateLimited) return rateLimited; + try { + const lucid = await lucidForAddress(newProvider(), changeAddress); + const r = await buildEndorse(lucid, { agentDid, proposalTxHash, proposalOutputIndex, stakeApex }); + return { + content: [{ + type: "text" as const, + text: `# Unsigned Endorsement Built (Not Submitted) + +**Stake:** ${stakeApex} AP3X +**Proposal:** ${proposalTxHash}#${proposalOutputIndex} +**Script Address:** ${r.scriptAddress} +**Estimated Fee:** ${r.feeAda} AP3X + +**Unsigned TX CBOR:** +\`\`\` +${r.txCbor} +\`\`\` + +${SIGN_AND_SUBMIT_COPY}`, + }], + }; + } catch (err) { + return { + content: [{ + type: "text" as const, + text: `Failed to build endorsement: ${err instanceof Error ? err.message : String(err)} + +**Troubleshooting Tips:** +1. Ensure the changeAddress wallet holds at least ${stakeApex + 3} AP3X (stake + fees). +2. Verify the proposal UTxO exists and is in Open state (use vector_self_improvement_browse). +3. Your local signer's per-transaction limit must allow the stake plus fees.`, + }], + }; + } + } + ); } diff --git a/packages/builder/test/boundary.test.ts b/packages/builder/test/boundary.test.ts index 9531a36..b559ecb 100644 --- a/packages/builder/test/boundary.test.ts +++ b/packages/builder/test/boundary.test.ts @@ -13,9 +13,11 @@ import { resolve, join, relative } from 'node:path'; */ const SRC_ROOT = resolve(import.meta.dirname!, '../src'); const SCRIPTS_ROOT = resolve(import.meta.dirname!, '../scripts'); -const CUSTODIAL_UNTIL_LATER_PR = new Set([ - 'vector/self-improvement.ts', // family 3 (governance) — goes keyless in spec PR 8 -]); +// Empty since spec PR 8: family 3 (self-improvement) went keyless, taking the +// last custodial file (vector/self-improvement.ts) off the allowlist. Every +// builder family is now keyless - this set may never grow again (see the +// size-0 pin below). +const CUSTODIAL_UNTIL_LATER_PR = new Set([]); // No \b boundaries: this is deliberately a substring match, not a whole-word // one. Word-bounding let `seedPhrase`, `WALLET_SEED` or `bip39x` slip past // undetected — those are real key-material vocabulary wearing a different @@ -70,11 +72,13 @@ describe('builder custody boundary', () => { }); // The allowlist may only shrink (PR 7 dropped agent-network.ts to reach 1, - // PR 8 drops self-improvement.ts to reach 0 - see the "goes keyless in spec - // PR N" comment above). Pinning the size means any widening is a deliberate, - // reviewable number change instead of a silent `.add(...)` slipping through. + // PR 8 drops self-improvement.ts to reach 0 - see the comment above + // CUSTODIAL_UNTIL_LATER_PR). Pinning the size means any widening is a + // deliberate, reviewable number change instead of a silent `.add(...)` + // slipping through - and since the migration is complete, that number can + // now only ever be 0. test('the allowlist may only shrink', () => { - assert.equal(CUSTODIAL_UNTIL_LATER_PR.size, 1, 'the custodial allowlist may only shrink - spec PR 8 takes it to 0'); + assert.equal(CUSTODIAL_UNTIL_LATER_PR.size, 0, 'the custodial allowlist is EMPTY - the migration is complete; it may never grow again'); }); // Pins every FORBIDDEN alternative individually against a fixture corpus, so diff --git a/packages/builder/test/smoke/tool-inventory.test.ts b/packages/builder/test/smoke/tool-inventory.test.ts index ee24358..c54c9d4 100644 --- a/packages/builder/test/smoke/tool-inventory.test.ts +++ b/packages/builder/test/smoke/tool-inventory.test.ts @@ -5,7 +5,7 @@ import { resolve } from 'node:path'; import { startServer, stopServer, ServerContext } from '../setup.ts'; /** - * Snapshot of the current tool surface (23 tools) for the non-custodial split + * Snapshot of the current tool surface (24 tools) for the non-custodial split * series (see docs/architecture/non-custodial-split.md). PR 6 (Task 5) is the * PR that took this list from 25 to 23: it deleted `vector_get_address` * outright, along with the tool that reported per-transaction/daily @@ -13,7 +13,14 @@ import { startServer, stopServer, ServerContext } from '../setup.ts'; * keyless equivalent — spend status is gone because the builder no longer * holds keys to limit) as part of making the remaining query tools * (`vector_get_utxos`, `vector_get_transaction_history`, `vector_dry_run`) - * keyless. PRs 2, 6, 7 and 8 each deliberately restructure and rename tool + * keyless. Spec PR 8 (Task 5) took it from 23 to 24: the self-improvement + * family went keyless, removing `vector_self_improvement_submit_proposal` + * and renaming `_critique`/`_endorse` to `vector_build_self_improvement_*`, + * and splitting the old single-shot submit into two build_* tools + * (`_proposal_lock` + `_proposal_spend`) since the deployed module validator + * requires a confirmed lock before the spend+mint step can reference it. + * This was the last custodial family - the builder now holds no key material + * anywhere. PRs 2, 6, 7 and 8 each deliberately restructure and rename tool * families, and each of those PRs is expected to update this list * deliberately, not accidentally. No wallet, no network beyond localhost — * this never needs a mnemonic. @@ -25,6 +32,10 @@ const EXPECTED_TOOLS = [ 'vector_build_interact_contract', 'vector_build_message_agent', 'vector_build_register_agent', + 'vector_build_self_improvement_critique', + 'vector_build_self_improvement_endorse', + 'vector_build_self_improvement_proposal_lock', + 'vector_build_self_improvement_proposal_spend', 'vector_build_send_apex', 'vector_build_send_tokens', 'vector_build_transaction', @@ -38,9 +49,6 @@ const EXPECTED_TOOLS = [ 'vector_get_utxos', 'vector_self_improvement_analyze_metrics', 'vector_self_improvement_browse', - 'vector_self_improvement_critique', - 'vector_self_improvement_endorse', - 'vector_self_improvement_submit_proposal', 'vector_submit_transaction', ]; @@ -92,11 +100,11 @@ after(async () => { }); describe('tool inventory smoke test', () => { - test('exposes exactly 23 tools', () => { + test('exposes exactly 24 tools', () => { assert.equal( actualNames.length, - 23, - `expected 23 tools, got ${actualNames.length}: ${JSON.stringify(actualNames)}` + 24, + `expected 24 tools, got ${actualNames.length}: ${JSON.stringify(actualNames)}` ); }); @@ -132,7 +140,7 @@ describe('tool schema snapshot', () => { }); // One assertion per tool (rather than a single deepStrictEqual over the whole - // 23-tool map) so a failure names the specific tool and shows a readable + // 24-tool map) so a failure names the specific tool and shows a readable // diff of just that tool's schema, instead of "objects differ" over an // unreadable combined structure. for (const toolName of Object.keys(schemaSnapshot).sort()) { diff --git a/packages/builder/test/smoke/tool-schemas.snapshot.json b/packages/builder/test/smoke/tool-schemas.snapshot.json index 7d30593..afca9d7 100644 --- a/packages/builder/test/smoke/tool-schemas.snapshot.json +++ b/packages/builder/test/smoke/tool-schemas.snapshot.json @@ -243,6 +243,220 @@ "$schema": "http://json-schema.org/draft-07/schema#" } }, + "vector_build_self_improvement_critique": { + "description": "Build an UNSIGNED transaction that submits a critique on an improvement proposal (Supportive, Opposing, or Amendment). Requires staking at least 10 AP3X. Provide critiqueDocument for automatic IPFS upload, or critiqueHash and storageUri manually. Takes no key material. Sign it locally with vector_signer_sign, broadcast with vector_submit_transaction, then confirm with vector_await_transaction. Find a proposalTxHash to critique with vector_self_improvement_browse.", + "inputSchema": { + "type": "object", + "properties": { + "changeAddress": { + "description": "Your wallet address (holds the stake AP3X; receives change). Get it from vector_signer_get_address.", + "type": "string" + }, + "agentDid": { + "description": "Agent DID (hex)", + "type": "string" + }, + "proposalTxHash": { + "description": "TX hash of the proposal UTxO to critique", + "type": "string" + }, + "proposalOutputIndex": { + "description": "Output index of the proposal UTxO", + "default": 0, + "type": "number" + }, + "critiqueDocument": { + "description": "Full critique document as JSON string. Uploaded to IPFS via Filebase; hash and CID computed automatically.", + "type": "string" + }, + "critiqueHash": { + "description": "blake2b_256 hash of critique document (64 hex chars). Required if critiqueDocument is not provided.", + "type": "string" + }, + "critiqueType": { + "description": "Type of critique", + "type": "string", + "enum": [ + "Supportive", + "Opposing", + "Amendment" + ] + }, + "storageUri": { + "description": "Off-chain storage URI for the critique document. Required if critiqueDocument is not provided.", + "type": "string" + }, + "stakeApex": { + "description": "AP3X to stake (minimum 10)", + "type": "number", + "minimum": 10 + } + }, + "required": [ + "changeAddress", + "agentDid", + "proposalTxHash", + "critiqueType", + "stakeApex" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + "vector_build_self_improvement_endorse": { + "description": "Build an UNSIGNED transaction that endorses an improvement proposal by staking at least 5 AP3X. Endorsements signal support to the Foundation Council and are weighted by stake amount. Takes no key material. Sign it locally with vector_signer_sign, broadcast with vector_submit_transaction, then confirm with vector_await_transaction. Find a proposalTxHash to endorse with vector_self_improvement_browse.", + "inputSchema": { + "type": "object", + "properties": { + "changeAddress": { + "description": "Your wallet address (holds the stake AP3X; receives change). Get it from vector_signer_get_address.", + "type": "string" + }, + "agentDid": { + "description": "Agent DID (hex)", + "type": "string" + }, + "proposalTxHash": { + "description": "TX hash of the proposal UTxO to endorse", + "type": "string" + }, + "proposalOutputIndex": { + "description": "Output index of the proposal UTxO", + "default": 0, + "type": "number" + }, + "stakeApex": { + "description": "AP3X to stake as endorsement (minimum 5)", + "type": "number", + "minimum": 5 + } + }, + "required": [ + "changeAddress", + "agentDid", + "proposalTxHash", + "stakeApex" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + "vector_build_self_improvement_proposal_lock": { + "description": "Build an UNSIGNED transaction that locks an improvement-proposal stake (step 1 of 2). Requires staking at least 25 AP3X. Provide proposalDocument for automatic IPFS upload, or proposalHash and storageUri manually. Takes no key material. Sign it locally with vector_signer_sign, broadcast with vector_submit_transaction, then confirm with vector_await_transaction. After this confirms, call this tool's counterpart, vector_build_self_improvement_proposal_spend, with the lock transaction hash to complete the submission.", + "inputSchema": { + "type": "object", + "properties": { + "changeAddress": { + "description": "Your wallet address (holds the stake AP3X; receives change). Get it from vector_signer_get_address.", + "type": "string" + }, + "agentDid": { + "description": "Agent DID (hex) - the asset name from Agent Registry NFT", + "type": "string" + }, + "proposalType": { + "description": "Category of the proposal", + "type": "string", + "enum": [ + "ParameterChange", + "TreasurySpend", + "ProtocolUpgrade", + "GameActivation", + "GeneralSuggestion" + ] + }, + "stakeApex": { + "description": "AP3X to stake (minimum 25)", + "type": "number", + "minimum": 25 + }, + "typeParams": { + "description": "Type-specific parameters (required for ParameterChange and TreasurySpend)", + "type": "object", + "properties": { + "paramName": { + "type": "string" + }, + "currentValue": { + "type": "number" + }, + "proposedValue": { + "type": "number" + }, + "amount": { + "type": "number" + }, + "recipientDescription": { + "type": "string" + }, + "upgradeHash": { + "type": "string" + }, + "gameId": { + "type": "number" + } + } + }, + "priority": { + "description": "Priority level (Emergency requires higher stake and reputation)", + "default": "Standard", + "type": "string", + "enum": [ + "Standard", + "Emergency" + ] + }, + "proposalDocument": { + "description": "Full proposal document as JSON string. Uploaded to IPFS via Filebase; hash and CID computed automatically. If provided, proposalHash and storageUri are ignored.", + "type": "string" + }, + "proposalHash": { + "description": "blake2b_256 hash of proposal document (64 hex chars). Required if proposalDocument is not provided.", + "type": "string" + }, + "storageUri": { + "description": "Off-chain storage URI for the full proposal (IPFS CID or OriginTrail UAL). Required if proposalDocument is not provided.", + "type": "string" + } + }, + "required": [ + "changeAddress", + "agentDid", + "proposalType", + "stakeApex" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + "vector_build_self_improvement_proposal_spend": { + "description": "Build an UNSIGNED transaction that completes an improvement-proposal submission (step 2 of 2): spends the locked stake through the module validator and mints the proposal and activity tokens. Requires the confirmed lock transaction hash from vector_build_self_improvement_proposal_lock. Takes no key material. Sign it locally with vector_signer_sign, broadcast with vector_submit_transaction, then confirm with vector_await_transaction.", + "inputSchema": { + "type": "object", + "properties": { + "changeAddress": { + "description": "The SAME wallet address used to build the proposal lock. Get it from vector_signer_get_address.", + "type": "string" + }, + "agentDid": { + "description": "Agent DID (hex) - must match the proposer DID in the locked proposal", + "type": "string" + }, + "lockTxHash": { + "description": "The CONFIRMED transaction hash from the proposal lock step", + "type": "string" + }, + "lockOutputIndex": { + "description": "Output index of the locked proposal UTxO (default: 0)", + "default": 0, + "type": "number" + } + }, + "required": [ + "changeAddress", + "agentDid", + "lockTxHash" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, "vector_build_send_apex": { "description": "Build an UNSIGNED transaction sending AP3X to a recipient. Takes no key material - sign the returned CBOR with your local signer (vector_signer_sign), then broadcast with vector_submit_transaction and confirm with vector_await_transaction.", "inputSchema": { @@ -589,7 +803,7 @@ } }, "vector_self_improvement_analyze_metrics": { - "description": "Analyze proposal metrics: proposal activity, adoption rate, treasury health, and engagement statistics. Read-only - no mnemonic needed.", + "description": "Analyze proposal metrics: proposal activity, adoption rate, treasury health, and engagement statistics. Read-only, takes no key material.", "inputSchema": { "type": "object", "properties": { @@ -646,189 +860,6 @@ "$schema": "http://json-schema.org/draft-07/schema#" } }, - "vector_self_improvement_critique": { - "description": "Submit a critique on an improvement proposal. Critiques can support, oppose, or propose amendments. Requires staking AP3X. Provide critiqueDocument (JSON string) for automatic IPFS upload, or critiqueHash + storageUri manually.", - "inputSchema": { - "type": "object", - "properties": { - "mnemonic": { - "description": "15 or 24-word BIP39 mnemonic for the wallet", - "type": "string" - }, - "agentDid": { - "description": "Agent DID (hex)", - "type": "string" - }, - "proposalTxHash": { - "description": "TX hash of the proposal UTxO to critique", - "type": "string" - }, - "proposalOutputIndex": { - "description": "Output index of the proposal UTxO", - "default": 0, - "type": "number" - }, - "critiqueDocument": { - "description": "Full critique document as JSON string. Uploaded to IPFS via Filebase; hash and CID computed automatically.", - "type": "string" - }, - "critiqueHash": { - "description": "blake2b_256 hash of critique document (64 hex chars). Required if critiqueDocument is not provided.", - "type": "string" - }, - "critiqueType": { - "description": "Type of critique", - "type": "string", - "enum": [ - "Supportive", - "Opposing", - "Amendment" - ] - }, - "storageUri": { - "description": "Off-chain storage URI for the critique document. Required if critiqueDocument is not provided.", - "type": "string" - }, - "stakeApex": { - "description": "AP3X to stake (minimum 10)", - "type": "number", - "minimum": 10 - } - }, - "required": [ - "mnemonic", - "agentDid", - "proposalTxHash", - "critiqueType", - "stakeApex" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - "vector_self_improvement_endorse": { - "description": "Endorse an improvement proposal by staking AP3X. Endorsements signal support to the Foundation Council and are weighted by stake amount.", - "inputSchema": { - "type": "object", - "properties": { - "mnemonic": { - "description": "15 or 24-word BIP39 mnemonic for the wallet", - "type": "string" - }, - "agentDid": { - "description": "Agent DID (hex)", - "type": "string" - }, - "proposalTxHash": { - "description": "TX hash of the proposal UTxO to endorse", - "type": "string" - }, - "proposalOutputIndex": { - "description": "Output index of the proposal UTxO", - "default": 0, - "type": "number" - }, - "stakeApex": { - "description": "AP3X to stake as endorsement (minimum 5)", - "type": "number", - "minimum": 5 - } - }, - "required": [ - "mnemonic", - "agentDid", - "proposalTxHash", - "stakeApex" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - "vector_self_improvement_submit_proposal": { - "description": "Submit an improvement proposal to the Vector Self-Improvement Module. Requires staking AP3X. Provide proposalDocument (JSON string) for automatic IPFS upload via Filebase and blake2b_256 hashing, OR provide proposalHash and storageUri manually.", - "inputSchema": { - "type": "object", - "properties": { - "mnemonic": { - "description": "15 or 24-word BIP39 mnemonic for the wallet", - "type": "string" - }, - "agentDid": { - "description": "Agent DID (hex) - the asset name from Agent Registry NFT", - "type": "string" - }, - "proposalDocument": { - "description": "Full proposal document as JSON string. Uploaded to IPFS via Filebase; hash and CID computed automatically. If provided, proposalHash and storageUri are ignored.", - "type": "string" - }, - "proposalHash": { - "description": "blake2b_256 hash of proposal document (64 hex chars). Required if proposalDocument is not provided.", - "type": "string" - }, - "proposalType": { - "description": "Category of the proposal", - "type": "string", - "enum": [ - "ParameterChange", - "TreasurySpend", - "ProtocolUpgrade", - "GameActivation", - "GeneralSuggestion" - ] - }, - "storageUri": { - "description": "Off-chain storage URI for the full proposal (IPFS CID or OriginTrail UAL). Required if proposalDocument is not provided.", - "type": "string" - }, - "stakeApex": { - "description": "AP3X to stake (minimum 25)", - "type": "number", - "minimum": 25 - }, - "typeParams": { - "description": "Type-specific parameters (required for ParameterChange and TreasurySpend)", - "type": "object", - "properties": { - "paramName": { - "type": "string" - }, - "currentValue": { - "type": "number" - }, - "proposedValue": { - "type": "number" - }, - "amount": { - "type": "number" - }, - "recipientDescription": { - "type": "string" - }, - "upgradeHash": { - "type": "string" - }, - "gameId": { - "type": "number" - } - } - }, - "priority": { - "description": "Priority level (Emergency requires higher stake and reputation)", - "default": "Standard", - "type": "string", - "enum": [ - "Standard", - "Emergency" - ] - } - }, - "required": [ - "mnemonic", - "agentDid", - "proposalType", - "stakeApex" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, "vector_submit_transaction": { "description": "Submit an already-signed transaction to Vector. Use this to broadcast CBOR signed elsewhere (for example by a local signer). Does not sign anything.", "inputSchema": { diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index fd05553..2cb7122 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -1,26 +1,5 @@ // Vector-specific type definitions -export interface SpendLimits { - perTransaction: number; // lovelace - daily: number; // lovelace -} - -export interface SpendStatus { - perTransactionLimit: number; - dailyLimit: number; - dailySpent: number; - dailyRemaining: number; - resetTime: string; -} - -export interface AuditEntry { - timestamp: string; - txHash: string; - amountLovelace: number; - recipient: string; - action: string; -} - // --- Build Transaction --- export interface TxOutput {