Merge pull request #4 from FilipBl4gojevic/ci/harness-and-gitignore
CI harness, secret hygiene, and the non-custodial architecture doc
This commit is contained in:
44
.github/workflows/ci.yml
vendored
Normal file
44
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=12288
|
||||
|
||||
- name: Unit tests
|
||||
run: npm run test:unit
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=12288
|
||||
|
||||
- name: Tool inventory smoke test
|
||||
run: npm run test:smoke
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=12288
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -29,4 +29,15 @@ coverage/
|
||||
|
||||
# Misc
|
||||
.tmp/
|
||||
.temp/
|
||||
.temp/
|
||||
|
||||
# Secrets — never commit
|
||||
mnemonic.txt
|
||||
.secrets/
|
||||
*.mnemonic
|
||||
|
||||
# Runtime state (contains transaction history)
|
||||
vector-audit-log.json
|
||||
|
||||
# Incremental typecheck cache (npm run typecheck uses --incremental)
|
||||
*.tsbuildinfo
|
||||
|
||||
37
README.md
37
README.md
@@ -21,7 +21,14 @@ Connect from Claude Code in one command:
|
||||
claude mcp add --transport sse vector-mcp https://mcp.vector.mainnet.apexfusion.org/sse
|
||||
```
|
||||
|
||||
The mnemonic is passed per-call by the MCP client and is never stored server-side - see the [security model](https://apex-fusion.github.io/vector-ai-documentation/mcp-server/security/). Self-hosting instructions are below.
|
||||
> **Security notice — custody.** Signing tools currently take your wallet mnemonic as a
|
||||
> tool-call parameter. It therefore passes through your MCP client, your model provider,
|
||||
> and the hosted server's memory — it is not written to disk, but it is exposed. Use only
|
||||
> a hot wallet holding funds you can afford to lose, and prefer self-hosting until the
|
||||
> non-custodial migration lands. See
|
||||
> [docs/architecture/non-custodial-split.md](docs/architecture/non-custodial-split.md).
|
||||
|
||||
Self-hosting instructions are below.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -102,7 +109,8 @@ cp .env.example .env
|
||||
# Edit .env with your endpoint URLs (defaults point to Vector testnet; mainnet URLs below)
|
||||
```
|
||||
|
||||
The mnemonic is passed per-call by the MCP client, not stored in the environment.
|
||||
The mnemonic is passed per-call by the MCP client rather than stored in the environment. This
|
||||
avoids environment dumps, but does not make it private — see the security notice above.
|
||||
|
||||
### 3. Run
|
||||
|
||||
@@ -148,7 +156,7 @@ docker run -p 3000:3000 vector-mcp
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | HTTP server port | `3000` |
|
||||
| `VECTOR_OGMIOS_URL` | Ogmios HTTP JSON-RPC endpoint | `https://ogmios.vector.testnet.apexfusion.org` |
|
||||
| `VECTOR_KOIOS_URL` | Koios REST API endpoint | `https://koios.vector.testnet.apexfusion.org/` |
|
||||
| `VECTOR_KOIOS_URL` | Koios REST API endpoint | `https://v2.koios.vector.testnet.apexfusion.org/` |
|
||||
| `VECTOR_SUBMIT_URL` | Transaction submit API | `https://submit.vector.testnet.apexfusion.org/api/submit/tx` |
|
||||
| `VECTOR_EXPLORER_URL` | Block explorer base URL | `https://vector.testnet.apexscan.org` |
|
||||
| `VECTOR_SPEND_LIMIT_PER_TX` | Max lovelace per transaction | `100000000` (100 AP3X) |
|
||||
@@ -156,8 +164,6 @@ docker run -p 3000:3000 vector-mcp
|
||||
| `VECTOR_AUDIT_LOG_PATH` | Persistent audit log file path | `./vector-audit-log.json` |
|
||||
| `VECTOR_RATE_LIMIT_PER_MINUTE` | Max tool calls per minute | `60` |
|
||||
|
||||
> **Note:** the default testnet Koios URL predates the v2 migration and currently returns 503. Override `VECTOR_KOIOS_URL` with `https://v2.koios.vector.testnet.apexfusion.org/` until the default is updated in code.
|
||||
|
||||
### Mainnet endpoints
|
||||
|
||||
| Variable | Mainnet value |
|
||||
@@ -170,14 +176,23 @@ docker run -p 3000:3000 vector-mcp
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Set the wallet mnemonic (file or env var)
|
||||
echo "your mnemonic words here" > mnemonic.txt
|
||||
# or: export VECTOR_MNEMONIC="your mnemonic words here"
|
||||
|
||||
npm test
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
Tests cover the core tools end-to-end against Vector testnet, including the full agent lifecycle: register, discover, profile, update, transfer, message, and deregister.
|
||||
No wallet, no network — pure logic only (CBOR encode/decode assertions).
|
||||
|
||||
```bash
|
||||
npm run test:smoke
|
||||
```
|
||||
|
||||
Builds the server, boots it, and asserts the exposed tool inventory matches the checked-in snapshot. No wallet, no external network. CI runs this and `test:unit` on every PR.
|
||||
|
||||
```bash
|
||||
echo "your mnemonic words here" > mnemonic.txt
|
||||
npm run test:integration
|
||||
```
|
||||
|
||||
Requires `mnemonic.txt` in the repo root containing a **funded Vector testnet** mnemonic. Covers the core tools end-to-end against Vector testnet, including the full agent lifecycle: register, discover, profile, update, transfer, message, and deregister. Never runs in CI.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
259
docs/architecture/non-custodial-split.md
Normal file
259
docs/architecture/non-custodial-split.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Non-Custodial Split Architecture
|
||||
|
||||
**Status:** approved · **Date:** 2026-07-27
|
||||
|
||||
Why the Vector MCP server is moving from seed-phrase-as-a-parameter to a keyless
|
||||
hosted builder plus a local signer, and the PR sequence that gets us there.
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem
|
||||
|
||||
Every signing tool on the Vector MCP server takes the user's BIP39 **seed phrase as a call parameter**. Because the LLM must emit that parameter to invoke the tool, the seed travels through the MCP client, **the model provider's context window and request logs**, the network, and finally the **process memory of the hosted server**. A seed is root custody: unlimited, irrevocable, unscopable.
|
||||
|
||||
The server's spend limits and rate limits are app-layer only, so they do not constrain anyone who has captured the seed. They are also **process-global singletons**, so on the hosted box every user shares one daily budget, one rate limit, and one audit file.
|
||||
|
||||
## 2. Goal
|
||||
|
||||
**The agent holds its own key. The shared server never sees a seed.**
|
||||
|
||||
Success criteria:
|
||||
1. No hosted tool accepts a mnemonic or private key. Grep-able: zero `mnemonic` params in the builder package.
|
||||
2. The signing key never enters the model context, never crosses the network.
|
||||
3. Spend policy is enforced where it is actually enforceable — locally, per user.
|
||||
4. Every PR in the series is independently reviewable and leaves `main` coherent.
|
||||
|
||||
## 3. Decisions (locked)
|
||||
|
||||
| Decision | Choice |
|
||||
|---|---|
|
||||
| Deployment reality | Hosted multi-tenant is flagship → seed-to-server is P0 |
|
||||
| Architecture | Keyless hosted **builder** + local companion **signer** |
|
||||
| Repo topology | Single repo, **npm workspaces** monorepo |
|
||||
| Migration | **Hard cut per family** — no dual-mode, no deprecation window |
|
||||
| Key storage | Pluggable `KeySource`; v1 = env var / `0600` file |
|
||||
| Signer policy | Decode tx → **net outflow** → limits → audit → sign. Fully autonomous |
|
||||
| Tool flow | 4 calls: `get_address` → `build_*` → `sign` → `submit` |
|
||||
| Tool naming | Explicit `build_*` / `signer_*` / `submit_*` prefixes |
|
||||
| Cross-cutting fixes | Entangled ones inline; independent ones as separate small PRs |
|
||||
| Rollout | Testnet auto-deploys and may break; mainnet held until complete |
|
||||
| Testing | **TDD** — tests written first |
|
||||
| Auth | Enforced when `MCP_AUTH_TOKENS` is configured |
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
```
|
||||
mcp-server/ npm workspaces root
|
||||
├── packages/
|
||||
│ ├── shared/ @apexfusion/vector-mcp-shared
|
||||
│ │ exports "./tx" pure CBOR, types, formatting — NO network
|
||||
│ │ exports "./provider" OgmiosProvider, endpoints — network
|
||||
│ ├── builder/ @apexfusion/vector-mcp-builder HOSTED · keyless · network
|
||||
│ └── signer/ @apexfusion/vector-mcp-signer LOCAL · holds key · no network
|
||||
```
|
||||
|
||||
Two properties carry the security argument:
|
||||
|
||||
**The signer has no network interface, inbound or outbound.** It speaks **stdio** (the natural MCP local transport) — no port, no listener. Submission lives on the builder, so it has no egress either. The signer is a pure function: `CBOR in → signed CBOR out`, gated by policy.
|
||||
|
||||
**The boundary is enforced by the module graph, not convention.** `shared` uses subpath exports; the signer imports `@apexfusion/vector-mcp-shared/tx` and *cannot* reach `/provider`. A reviewer can verify the signer is network-free by reading its imports.
|
||||
|
||||
### Why zero-network is possible
|
||||
|
||||
Net outflow is computable from the transaction body alone:
|
||||
|
||||
```
|
||||
netOutflowLovelace = Σ { output.lovelace | output.address ∉ ownAddresses } + fee
|
||||
```
|
||||
|
||||
No input resolution, therefore no chain access. Change returning to us is excluded automatically because its address is ours.
|
||||
|
||||
**Known over-count:** an output to the user's own wallet at a *different derivation index* is classified as foreign and counted as outflow. This fails safe (refuses too much, never too little) and is accepted for v1.
|
||||
|
||||
## 5. Components
|
||||
|
||||
| Component | Package | Responsibility |
|
||||
|---|---|---|
|
||||
| `OgmiosProvider` | shared `/provider` | Chain queries + submit. Relocated unchanged. |
|
||||
| CBOR / tx helpers | shared `/tx` | Decode, asset-name derivation, formatting. Pure. |
|
||||
| Tool modules | builder | `build_*` per family, read-only queries, `submit`, `await` |
|
||||
| Auth + rate limit | builder | Bearer token; **per-identity** limits replace the global singleton |
|
||||
| `KeySource` | signer | `getSigningKey()`. v1: `EnvKeySource`, `FileKeySource` (0600) |
|
||||
| `PolicyEngine` | signer | `evaluate(decoded, ownAddresses) → { allowed, reason, netOutflow }` |
|
||||
| `SpendLimitPolicy` | signer | Ported `safety.ts`, now per-user and enforceable |
|
||||
| Audit log | signer | Local, per-user |
|
||||
|
||||
`PolicyEngine` is an interface with one v1 implementation so recipient-allowlist and threshold-approval land later without touching the sign path.
|
||||
|
||||
### Interfaces
|
||||
|
||||
```ts
|
||||
interface KeySource {
|
||||
getSigningKey(): Promise<PrivateKey>;
|
||||
getAddress(): Promise<string>;
|
||||
}
|
||||
|
||||
interface PolicyDecision {
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
netOutflowLovelace: bigint;
|
||||
assetMovements: AssetMovement[];
|
||||
}
|
||||
|
||||
interface Policy {
|
||||
evaluate(tx: DecodedTx, ownAddresses: string[]): PolicyDecision;
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Data flow
|
||||
|
||||
```
|
||||
1. vector_signer_get_address → { address } [signer]
|
||||
2. vector_build_send_apex → { txCbor, summary, fee } [builder, keyless]
|
||||
{ changeAddress, recipientAddress, amount }
|
||||
3. vector_signer_sign → { signedTxCbor } [signer, key]
|
||||
{ txCbor } decode → net outflow → policy → audit → sign
|
||||
4. vector_submit_transaction → { txHash, explorerUrl } [builder]
|
||||
{ signedTxCbor }
|
||||
```
|
||||
|
||||
`vector_signer_decode_transaction` is a read-only preview so an agent can show a human what it is about to sign.
|
||||
|
||||
**Accepted trade-off of the 4-call shape:** the signer records **intent to spend** at signing time, not confirmed submission. If a submit later fails, the audit log over-counts. Conservative, therefore acceptable — and it is the price of the signer having no network access. Documented, not hidden.
|
||||
|
||||
### Two-step governance flow
|
||||
|
||||
`submit_proposal` is a lock-then-spend pair with a confirmation wait between. Keyless, that orchestration becomes explicit:
|
||||
|
||||
```
|
||||
build_self_improvement_proposal_lock → sign → submit → await_transaction
|
||||
→ build_self_improvement_proposal_spend(lockTxHash) → sign → submit
|
||||
```
|
||||
|
||||
This is why governance is the last family: the LLM must drive a stateful, multi-transaction sequence.
|
||||
|
||||
## 7. Tool inventory
|
||||
|
||||
**Builder — 24 tools, zero keys.**
|
||||
|
||||
| Group | Tools |
|
||||
|---|---|
|
||||
| Read-only (8, unchanged) | `get_balance`, `get_utxos`, `get_transaction_history`, `dry_run`, `discover_agents`, `get_agent_profile`, `self_improvement_browse`, `self_improvement_analyze_metrics` |
|
||||
| Build — tx (3) | `build_send_apex`, `build_send_tokens`, `build_transaction` |
|
||||
| Build — contracts (2) | `build_deploy_contract`, `build_interact_contract` |
|
||||
| Build — registry (5) | `build_register_agent`, `build_update_agent`, `build_transfer_agent`, `build_deregister_agent`, `build_message_agent` |
|
||||
| Build — governance (4) | `build_self_improvement_proposal_lock`, `build_self_improvement_proposal_spend`, `build_self_improvement_critique`, `build_self_improvement_endorse` |
|
||||
| Chain ops (2, new) | `submit_transaction`, `await_transaction` |
|
||||
|
||||
**Signer — 4 tools, holds the key.**
|
||||
|
||||
`signer_get_address` · `signer_decode_transaction` · `signer_sign` · `signer_get_spend_limits`
|
||||
|
||||
**Removed:** `vector_get_address` (replaced by `signer_get_address`), `vector_get_spend_limits` (moved to signer).
|
||||
|
||||
## 8. Error handling
|
||||
|
||||
**The signer fails closed.** Any CBOR it cannot fully parse, any unrecognised output type, any address it cannot classify as ours-or-foreign → **refuse**. A signer that guesses is worthless. Refusals return a structured reason the agent relays verbatim: `"net outflow 250.00 AP3X exceeds per-transaction limit 100.00 AP3X"`.
|
||||
|
||||
**The builder never fabricates.** Submission returns a real error when it fails, never a synthesised or placeholder result.
|
||||
|
||||
## 9. Testing
|
||||
|
||||
TDD throughout: failing test before implementation, every PR.
|
||||
|
||||
| Layer | Coverage | Wallet | Network |
|
||||
|---|---|---|---|
|
||||
| `shared` | Golden CBOR encode/decode, asset-name derivation | no | no |
|
||||
| `builder` | Fixture UTxOs + mocked provider → assert structure, fee, change | no | no |
|
||||
| `signer` | **Adversarial** (below) | throwaway key | no |
|
||||
| integration | build→sign→submit round trip | **funded testnet** | yes |
|
||||
|
||||
**Mandatory adversarial cases for the signer** — these are the security boundary:
|
||||
|
||||
- Drain to a foreign address exceeding the limit → **refused**
|
||||
- Change returning to own address → **excluded** from net outflow
|
||||
- Malformed / truncated CBOR → **refused**, never signed
|
||||
- Unrecognised output type → **refused**
|
||||
- Daily limit accumulation across multiple signs → enforced
|
||||
- Fee included in net outflow
|
||||
|
||||
## 10. PR sequence
|
||||
|
||||
### Spine (ordered)
|
||||
|
||||
| PR | Content | Breaks testnet |
|
||||
|---|---|---|
|
||||
| **1** | CI harness: build + unit tests. Ships this design doc into `docs/architecture/` so reviewers have the rationale | no |
|
||||
| **2** | MCP SDK upgrade + fix 8 type errors + restructure tool registration + **typecheck becomes a hard gate** | no |
|
||||
| **3** | Workspace scaffolding — pure `git mv`, zero logic change, validated by PR 1–2 CI | no |
|
||||
| **4** | **Signer package** (purely additive): `KeySource`, decode, `PolicyEngine`, audit, 4 tools. Heavy TDD | no |
|
||||
| **5** | Builder hardening: auth (enforced when configured), per-identity rate limits, `submit_transaction`, `await_transaction` | no |
|
||||
| **6** | **Family 1 — wallet/tx keyless.** Mnemonics removed; `build_*` tools land | **yes** |
|
||||
| **7** | **Family 2 — agent registry keyless.** Drops `@ts-nocheck` from `agent-network.ts` | yes |
|
||||
| **8** | **Family 3 — governance keyless.** Plus env-driven ref-UTxO config and a CBOR encoding fix | yes |
|
||||
| **9** | Mainnet cutover: README, security-page correction, deliberate `workflow_dispatch` | — |
|
||||
|
||||
**PR 4 is the review that matters.** It is additive, self-contained, and *is* the security boundary. It breaks nothing while it is reviewed.
|
||||
|
||||
**PR 2 rationale:** `main` does not currently typecheck (§12). Gating CI on a red tree is impossible, so the gate is established before any refactor lands on top of it.
|
||||
|
||||
### Independent singles (any order, parallel)
|
||||
|
||||
| PR | Fix |
|
||||
|---|---|
|
||||
| A | Koios v2 default; reconcile `.env.example` / `docker-compose` |
|
||||
| B | `engines.node` floor; `postinstall` symlink hack |
|
||||
| C | Submission error handling |
|
||||
| D | `min(1)` APEX floor → real dust floor |
|
||||
| E | Endpoint URLs in error text |
|
||||
| F | SSE → Streamable HTTP |
|
||||
| G | Float → bigint money math |
|
||||
| H | Dependency updates |
|
||||
|
||||
### Dependency graph
|
||||
|
||||
```
|
||||
PR1 ─→ PR2 ─→ PR3 ─┬─→ PR4 (signer) ─┐
|
||||
└─→ PR5 (builder hardening) ─┴─→ PR6 ─→ PR7 ─→ PR8 ─→ PR9
|
||||
|
||||
A…H independent of the spine and of each other — land any time
|
||||
```
|
||||
|
||||
PR 4 and PR 5 both depend on the workspace split (PR 3) and can proceed in parallel; PR 6 needs both, because the first keyless family requires a signer to sign with and a `submit_transaction` to submit through.
|
||||
|
||||
## 11. Rollout
|
||||
|
||||
Testnet auto-deploys on push to `main` (`deploy.yml`) and **is the proving ground** — breaking it from PR 6 onward is expected and useful signal. Mainnet deploys only by manual `workflow_dispatch` and **stays on the last custodial image until PR 9**. This uses the existing CI split as designed; no new infrastructure.
|
||||
|
||||
## 12. Findings from local verification
|
||||
|
||||
Verified locally on Windows 11, **Node v25.8.0 / npm 11.11.0**: `npm ci` clean in 16 s (396 packages), `tsup` build succeeds (131 KB, 67 ms).
|
||||
|
||||
> **Verification caveat:** local Node is **25.8.0**; the Dockerfile deploys on **node:22-alpine**, and `package.json` claims `>=14`. Local green does not prove green on the deploy runtime. PR 1's CI must pin the **Node 22** matrix so the gate matches production, and PR B corrects the declared floor.
|
||||
|
||||
**`npm run typecheck` fails on `main` — 8 errors, all `src/vector/vector.ts`.**
|
||||
|
||||
```
|
||||
512,3 · 702,3 · 782,3 · 920,3 · 990,3 · 1192,3 · 1250,3 TS2589 instantiation excessively deep
|
||||
1250,10 TS2769 no overload — z.record(z.string()).optional()
|
||||
```
|
||||
|
||||
CI only runs `npm run build`, and `tsup`/esbuild strips types without checking them, so this has never been caught.
|
||||
|
||||
**This corrects an earlier misattribution.** The `@ts-nocheck` headers are attributed in-code to "Cardano WASM imports," but the real cause is `server.tool()` generic inference exceeding TypeScript's instantiation depth on rich Zod schemas — the same failure leaking out of the one file lacking the escape hatch. Removing `@ts-nocheck` therefore requires restructuring tool registration, not a cleanup pass. Suspected root cause is the stale `@modelcontextprotocol/sdk@^1.4.1`, which would also explain the deprecated SSE transport — **unverified; PR 2 opens with a timeboxed spike.**
|
||||
|
||||
## 13. Residual risks
|
||||
|
||||
**Native-asset outflow is not limited in v1.** Spend limits apply to lovelace. A transaction moving ~2 AP3X plus a valuable NFT to a foreign address passes the lovelace check. Blanket-denying asset outflow is not viable because legitimate registry flows mint an NFT to the registry script address.
|
||||
|
||||
v1 mitigation: all asset movements are decoded, logged, and returned in the policy decision, and `assetOutflowPolicy` is configurable (`allow` default | `deny` | `allowlist`) for high-value wallets. **Stated as a known limitation, not glossed.** Post-v1: per-policy asset rules.
|
||||
|
||||
**Key at rest is plaintext in v1** (env var or `0600` file). The win is that the key is *local and never transits the LLM or a shared host* — not that it is in an HSM. `KeySource` exists so keychain/HSM/hardware land later without touching the sign path.
|
||||
|
||||
**Prompt injection is mitigated, not eliminated.** The signer refuses transactions violating policy, but an injected LLM can still cause any transaction *within* policy. Recipient allowlisting is the next lever.
|
||||
|
||||
**SDK upgrade may not resolve TS2589.** PR 2's scope is contingent on the spike; if the upgrade does not fix it, fall back to extracting schemas into typed constants and re-scope PR 2.
|
||||
|
||||
## 14. Open items
|
||||
|
||||
1. **PR 2 spike result** determines whether the SDK upgrade is in scope or the type errors are patched directly.
|
||||
@@ -12,7 +12,10 @@
|
||||
"build": "tsup && chmod +x build/index.js",
|
||||
"typecheck": "tsc --noEmit --incremental",
|
||||
"start": "node build/index.js",
|
||||
"test": "npm run build && node --import tsx --test test/run.test.ts"
|
||||
"test": "npm run test:unit",
|
||||
"test:unit": "node --import tsx --test test/unit/*.test.ts",
|
||||
"test:integration": "npm run build && node --import tsx --test test/integration/*.test.ts",
|
||||
"test:smoke": "npm run build && node --import tsx --test test/smoke/*.test.ts"
|
||||
},
|
||||
"files": [
|
||||
"build"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, test, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { startServer, stopServer, callTool, getMnemonic, wait, ServerContext } from './setup.ts';
|
||||
import { startServer, stopServer, callTool, getMnemonic, wait, ServerContext } from '../setup.ts';
|
||||
|
||||
// Always-succeeds PlutusV2 validator (accepts any datum/redeemer/context, returns True)
|
||||
const ALWAYS_SUCCEEDS_V2 = '49480100002221200101';
|
||||
@@ -31,7 +31,7 @@ export async function startServer(): Promise<ServerContext> {
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Server start timeout (15s)')), 15_000);
|
||||
const timeout = setTimeout(() => reject(new Error('Server start timeout (60s)')), 60_000);
|
||||
child.stderr!.on('data', (chunk: Buffer) => {
|
||||
if (chunk.toString().includes('listening on port')) {
|
||||
clearTimeout(timeout);
|
||||
|
||||
75
test/smoke/tool-inventory.test.ts
Normal file
75
test/smoke/tool-inventory.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { describe, test, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { startServer, stopServer, ServerContext } from '../setup.ts';
|
||||
|
||||
/**
|
||||
* Snapshot of the pre-migration tool surface (23 tools), captured from a real
|
||||
* `listTools()` call against a built server on the `ci/harness-and-gitignore`
|
||||
* branch. This is the baseline for the non-custodial split series
|
||||
* (see docs/architecture/non-custodial-split.md) — PRs 2, 6, 7 and 8
|
||||
* 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.
|
||||
*/
|
||||
const EXPECTED_TOOLS = [
|
||||
'vector_build_transaction',
|
||||
'vector_deploy_contract',
|
||||
'vector_deregister_agent',
|
||||
'vector_discover_agents',
|
||||
'vector_dry_run',
|
||||
'vector_get_address',
|
||||
'vector_get_agent_profile',
|
||||
'vector_get_balance',
|
||||
'vector_get_spend_limits',
|
||||
'vector_get_transaction_history',
|
||||
'vector_get_utxos',
|
||||
'vector_interact_contract',
|
||||
'vector_message_agent',
|
||||
'vector_register_agent',
|
||||
'vector_self_improvement_analyze_metrics',
|
||||
'vector_self_improvement_browse',
|
||||
'vector_self_improvement_critique',
|
||||
'vector_self_improvement_endorse',
|
||||
'vector_self_improvement_submit_proposal',
|
||||
'vector_send_apex',
|
||||
'vector_send_tokens',
|
||||
'vector_transfer_agent',
|
||||
'vector_update_agent',
|
||||
];
|
||||
|
||||
let ctx: ServerContext;
|
||||
let actualNames: string[];
|
||||
|
||||
before(async () => {
|
||||
ctx = await startServer();
|
||||
const result = await ctx.client.listTools();
|
||||
actualNames = result.tools.map((t) => t.name).sort();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopServer(ctx);
|
||||
});
|
||||
|
||||
describe('tool inventory smoke test', () => {
|
||||
test('exposes exactly 23 tools', () => {
|
||||
assert.equal(
|
||||
actualNames.length,
|
||||
23,
|
||||
`expected 23 tools, got ${actualNames.length}: ${JSON.stringify(actualNames)}`
|
||||
);
|
||||
});
|
||||
|
||||
test('tool names match the checked-in inventory exactly', () => {
|
||||
const added = actualNames.filter((name) => !EXPECTED_TOOLS.includes(name));
|
||||
const removed = EXPECTED_TOOLS.filter((name) => !actualNames.includes(name));
|
||||
|
||||
assert.deepEqual(
|
||||
actualNames,
|
||||
EXPECTED_TOOLS,
|
||||
'Tool inventory drifted from the checked-in snapshot in test/smoke/tool-inventory.test.ts.\n' +
|
||||
` added: ${added.length ? JSON.stringify(added) : '(none)'}\n` +
|
||||
` removed: ${removed.length ? JSON.stringify(removed) : '(none)'}\n` +
|
||||
'If this drift is intentional, update EXPECTED_TOOLS in this file to match.'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { deriveNftAssetName } from '../src/vector/agent-network.ts';
|
||||
import { deriveNftAssetName } from '../../src/vector/agent-network.ts';
|
||||
|
||||
// Parity vectors — verified against pycardano OutputReference.to_cbor() and
|
||||
// agent-sdk-py plutus_serialise_data(0, [tx_hash_bytes, idx]). All three
|
||||
Reference in New Issue
Block a user