diff --git a/skills/ito-trade-planner/SKILL.md b/skills/ito-trade-planner/SKILL.md index ffeed6852..43049d3ec 100644 --- a/skills/ito-trade-planner/SKILL.md +++ b/skills/ito-trade-planner/SKILL.md @@ -10,8 +10,8 @@ metadata: Use this skill when a user wants a structured worksheet for a prediction-market idea, basket adjustment, venue comparison, or manual execution plan. -The skill is intentionally non-executing. It produces checklists and parameter -tables the user can review manually. +The skill is intentionally non-executing. It produces indicative, non-executable +checklists and parameter tables the user can review manually. ## Guardrails @@ -20,16 +20,58 @@ tables the user can review manually. - Do not place, cancel, route, or sign orders. - Do not request private keys, seed phrases, exchange passwords, or wallet credentials. -- Require explicit user approval before any workflow moves from research to - execution-capable tooling. +- Require a separate workflow and explicit user approval before moving from + research to execution-capable tooling. This approval does not authorize this + skill to execute anything. +- If execution is requested, stop after the worksheet without invoking, calling, + or opening an execution-capable tool or venue. + +## Read-Only API And Authentication Boundary + +The canonical developer surface is `https://itomarkets.com/api/v1`. Use only +authenticated `GET` endpoints requiring `baskets:read` or `markets:read`, either +with HTTPS and `Authorization: Bearer $ITO_API_KEY` or the official +`ito-markets` Python SDK. Trading is not part of this API. + +On first use, check for an already configured key with exactly `baskets:read` and +`markets:read` without printing it. Least-privilege public keys use the `bkt_*` +form and are operator-issued; the dashboard's **Settings -> Keys & credentials** +flow issues a broader `ito_*` automation key. Do not create or rotate that broader +key merely to unblock this skill. If a scoped key is unavailable, report the +read-only API route as blocked and continue with clearly labeled public or user- +supplied inputs. Key issuance creates persistent access and needs confirmation in +the controlling harness. After the user or operator stores the one-time value +securely, return control to the originating agent and run one minimal +`GET /baskets` auth probe. This API does not use device authorization or device +login; do not invent a verification-code handoff. + +The `ecc ito` bridge is a separate compute-procurement surface. Do not use +`ecc ito login`, `ecc ito find`, or its MCP tools for prediction-market data or +trade planning. Never print, log, persist, or place `ITO_API_KEY` in arguments, +reports, screenshots, tracked files, or chat. Retrieve only the minimum field at +runtime and keep it in process memory. + +Mark API observations indicative. Use `GET /baskets`, +`GET /baskets/{basket_id}`, `GET /baskets/{basket_id}/price`, +`GET /baskets/{basket_id}/underlyers`, `GET /markets/search`, and +`GET /markets/{market_id}` as needed. Do not use write or backtest submission +endpoints for a trade-planning worksheet. ## Planning Workflow 1. Restate the user's idea as a neutral hypothesis. 2. Identify markets, venues, underliers, resolution rules, fees, and data freshness constraints. -3. If `ITO_API_KEY` is configured and requested, read Itô basket metadata. -4. Build a manual worksheet: +3. If the user requested live Itô data, make the smallest authenticated read and + record the endpoint URL and `retrieved_at` timestamp. Never infer a live price + from stale, missing, or inaccessible data; use `unknown`. +4. Collect constraints without inventing values: jurisdiction/account + eligibility, venue, market identifier, side (if the user supplied one), + limit, time-in-force, maximum spend, fees, liquidity/slippage boundary, + resolution rule, and decision deadline. Missing constraints remain `unknown`. +5. Run `prediction-market-risk-review` before discussing automation, keys, + venue auth, capital constraints, or a manual action link. +6. Build a manual worksheet: - market/underlier - venue - data source @@ -38,8 +80,24 @@ tables the user can review manually. - liquidity caveat - open questions - manual action link or next review step -5. Run `prediction-market-risk-review` before discussing automation, keys, - venue auth, or capital constraints. +7. If the user asks to continue toward execution, list the unresolved gates and + request separate explicit confirmation in the future execution-capable + workflow. Do not treat confirmation given during planning as an order. + +## Recovery And Failure States + +- On `401`, set `plan_status: blocked` and ask the user to inspect or replace the + key in Settings. On `403`, report the missing read scope; never request a write + scope for this skill. Redact any credential-like text. +- On `429`, honor `Retry-After` once within the user's time budget. Do not loop or + exceed the documented read budget of 120 requests per minute. +- On timeout or ambiguous transport failure, set affected values to `unknown`. + Retry at most once for a read; never turn a read failure into a write. +- On expired or revoked access, stop, redact server details that could contain + credentials, and direct the user to Settings. Never weaken scopes or reuse + cached secrets. +- Public and private sources must be labeled separately. Do not present cached + or fixture data as live behavior. ## Allowed Language @@ -58,9 +116,38 @@ Avoid: - "risk-free" - "optimal size" -## Output Contract +## Structured Output Contract -End every plan with: +Return this shape in Markdown or YAML. Preserve `unknown` rather than guessing. + +```yaml +plan_status: ready_for_manual_review | blocked +mode: indicative_non_executable +hypothesis: "neutral restatement" +markets: + - market: "identifier or unknown" + venue: "venue or unknown" + observable_status: "value or unknown" + source_url: "source URL or unknown" + retrieved_at: "ISO-8601 timestamp or unknown" + resolution_rule: "summary or unknown" + liquidity_caveat: "text or unknown" +constraints: + jurisdiction_eligibility: "confirmed | unconfirmed | unknown" + limit: "user supplied value or unknown" + maximum_spend: "user supplied value or unknown" + fees: "value or unknown" + decision_deadline: "value or unknown" +data_freshness: "timestamp and caveats" +risk_review: + status: pass | warn | fail | not_run + findings: [] +blocked_actions: + - "order placement, cancellation, routing, signing, and submission" +next_safe_step: "one non-executing review action" +``` + +End every plan with exactly: ```text This is a planning worksheet, not investment or trading advice. Review venue diff --git a/tests/ci/ito-trade-planner-skill.test.js b/tests/ci/ito-trade-planner-skill.test.js new file mode 100644 index 000000000..eec5dc4ac --- /dev/null +++ b/tests/ci/ito-trade-planner-skill.test.js @@ -0,0 +1,113 @@ +/** + * Contract tests for the installable Itô trade-planner skill. + */ + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const REPO_ROOT = path.join(__dirname, '..', '..'); + +function read(relativePath) { + return fs.readFileSync(path.join(REPO_ROOT, relativePath), 'utf8'); +} + +function readJson(relativePath) { + return JSON.parse(read(relativePath)); +} + +function runTest(name, fn) { + try { + fn(); + console.log(` ✓ ${name}`); + return true; + } catch (error) { + console.log(` ✗ ${name}`); + console.error(` ${error.message}`); + return false; + } +} + +function main() { + console.log('\n=== Testing Itô trade-planner skill surface ===\n'); + + const skill = read('skills/ito-trade-planner/SKILL.md'); + const tests = [ + ['has portable discovery metadata and representative triggers', () => { + assert.match(skill, /^---\nname: ito-trade-planner\ndescription: [^\n]+\nmetadata:\n origin: ECC\n---/); + for (const trigger of ['trade plan', 'planning worksheet', 'venue comparison', 'basket adjustment']) { + assert.match(skill, new RegExp(trigger, 'i'), `missing trigger phrase: ${trigger}`); + } + }], + ['installs with the complete risk-review dependency pack', () => { + const modules = readJson('manifests/install-modules.json').modules; + const module = modules.find(candidate => candidate.id === 'prediction-market-skills'); + assert.ok(module, 'prediction-market-skills module is missing'); + for (const requiredPath of [ + 'skills/ito-trade-planner', + 'skills/prediction-market-risk-review', + ]) { + assert.ok(module.paths.includes(requiredPath), `${requiredPath} is not installed`); + } + assert.strictEqual(module.defaultInstall, false); + assert.ok(readJson('package.json').files.includes('skills/ito-trade-planner/')); + }], + ['keeps indicative planning separate from executable behavior', () => { + assert.match(skill, /indicative/i); + assert.match(skill, /not executable|non-executable/i); + assert.match(skill, /Trading is not part of this API/i); + assert.match(skill, /do not (?:place|cancel|route|sign|submit)/i); + assert.match(skill, /separate[^.]*explicit (?:user )?(?:approval|confirmation)/i); + assert.match(skill, /stop[^.]*without (?:invoking|calling|opening)/i); + assert.doesNotMatch(skill, /(?:run|invoke|call) `?ecc ito (?:find|status)/i); + }], + ['documents the real API-key first run and rejects invented device login', () => { + assert.match(skill, /https:\/\/itomarkets\.com\/api\/v1/); + assert.match(skill, /Authorization: Bearer/); + assert.match(skill, /baskets:read/); + assert.match(skill, /markets:read/); + assert.match(skill, /bkt_\*/); + assert.match(skill, /broader `ito_\*` automation key/); + assert.match(skill, /Do not create or rotate that broader/); + assert.match(skill, /ito-markets/); + assert.match(skill, /Settings/i); + assert.match(skill, /originating agent/i); + assert.match(skill, /does not use device (?:authorization|login)/i); + assert.match(skill, /do not use\s+`ecc ito login`/i); + assert.match(skill, /never (?:print|log|persist)[^.]*ITO_API_KEY/i); + }], + ['defines structured output, provenance, and recovery states', () => { + for (const field of [ + 'plan_status', 'mode', 'hypothesis', 'markets', 'constraints', + 'data_freshness', 'risk_review', 'blocked_actions', 'next_safe_step', + ]) { + assert.match(skill, new RegExp(`\\b${field}\\b`), `missing output field: ${field}`); + } + assert.match(skill, /source URL/i); + assert.match(skill, /retrieved_at/i); + assert.match(skill, /timeout/i); + assert.match(skill, /revok/i); + assert.match(skill, /401/); + assert.match(skill, /403/); + assert.match(skill, /429/); + assert.match(skill, /Retry-After/); + assert.match(skill, /redact/i); + assert.match(skill, /unknown/i); + }], + ['preserves the non-advisory disclaimer exactly', () => { + assert.match(skill, /This is a planning worksheet, not investment or trading advice\. Review venue\n+rules and make any trading decisions yourself\./); + }], + ]; + + let passed = 0; + let failed = 0; + for (const [name, fn] of tests) { + if (runTest(name, fn)) passed += 1; + else failed += 1; + } + console.log(`\nPassed: ${passed}`); + console.log(`Failed: ${failed}`); + process.exit(failed > 0 ? 1 : 0); +} + +main();