@oracle-agent/oracle 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,11 +20,11 @@ alongside it — same split we run ourselves.
20
20
 
21
21
  ## Start in 5 steps
22
22
 
23
- Latest: **oracle `0.3.6`** · **operator `0.6.1`**
23
+ Latest: **oracle `0.4.1`** · **operator `0.13.6`**
24
24
 
25
25
  ```bash
26
26
  # 1) install both packages
27
- npm i @oracle-agent/oracle@0.3.6 @oracle-agent/operator@0.6.1
27
+ npm i @oracle-agent/oracle@latest @oracle-agent/operator@latest
28
28
 
29
29
  # 2) put keys ONLY on your machine (example: HL/EVM hex key file)
30
30
  mkdir -p ~/.config/oracle/keys && chmod 700 ~/.config/oracle/keys
@@ -47,9 +47,13 @@ await hlSignAndSubmit(prepared, {
47
47
  ```
48
48
 
49
49
  ```bash
50
- # 5) unattended desk (no click each trade) — HL/Poly capped
50
+ # 5) unattended desk (no click each trade) — capped, opt-in
51
51
  export ORACLE_AUTONOMOUS_TRADING=1
52
- # or: npx oracle-signer
52
+ # or run the signer daemon: npx oracle-signer
53
+ #
54
+ # Daemon signing requires the SAME HMAC secret in the preparer and the signer,
55
+ # otherwise every sign fails with "ORACLE_STAMP_HMAC_SECRET missing in signer":
56
+ export ORACLE_STAMP_HMAC_SECRET='long-random-secret'
53
57
  ```
54
58
 
55
59
  Full vault / multi-chain / daemon detail → operator [SETUP](https://github.com/demi-hl/oracle-operator/blob/main/SETUP.md).
@@ -262,7 +266,7 @@ owner-local source lane.** The short version:
262
266
  Oracle keeps capability and authorization separate:
263
267
 
264
268
  - Public Oracle reads, quotes, simulates, and prepares unsigned artifacts.
265
- - The generic unattended signer remains limited to `hl` and `poly`.
269
+ - The generic unattended signer exposes six bounded surfaces: `hl`, `poly`, `evm-swap`, `evm-bridge`, `btc`, `sol`. Each decodes its own envelope and refuses while its allowlists are empty.
266
270
  - Ordinary EVM preparation remains user-wallet signed.
267
271
  - A deployment may separately install a same-host, owner-gated EVM executor. Oracle must verify that executor before describing bounded EVM execution as available. Missing deployment capability means "unavailable here," not "Oracle can never execute EVM."
268
272
  - `watch`, `watch this`, and `ping me` always create `active: true, actionMode: alert_only`.
@@ -408,8 +412,9 @@ it — verified in practice when Odos sunset their public API mid-development
408
412
  ## Examples
409
413
 
410
414
  ```bash
411
- node examples/add-a-chain.mjs # register an unseen chain, no code
412
- node examples/research-a-token.mjs # honest token research on live data
415
+ node examples/add-a-chain.mjs # register an unseen chain, no code
416
+ node examples/research-a-token.mjs # honest token research on live data
417
+ node examples/oracle-pack-template.mjs # skeleton for a safe Oracle protocol pack
413
418
  ```
414
419
 
415
420
  ## MCP
@@ -449,7 +454,7 @@ don't imply endorsement by an official deployment when shipping a fork.
449
454
  Prefer the local signer daemon so the agent never sees the vault passphrase:
450
455
 
451
456
  ```bash
452
- npx oracle-signer # has ORACLE_VAULT_PASSPHRASE
457
+ npx oracle-signer # reads the passphrase from the OS credential store
453
458
  # agent: ORACLE_SIGNER_URL + ORACLE_SIGNER_TOKEN only
454
459
  ```
455
460
 
package/SETUP.md CHANGED
@@ -45,7 +45,7 @@ The public Oracle profile must report these as separate planes:
45
45
  | Plane | Default capability |
46
46
  |---|---|
47
47
  | Public Oracle package | read, quote, simulate, unsigned prepare |
48
- | Generic `oracle-signer` daemon | policy-bounded `hl` and `poly` only |
48
+ | Generic `oracle-signer` daemon | six bounded surfaces: `hl`, `poly`, `evm-swap`, `evm-bridge`, `btc`, `sol` (each fails closed on empty allowlists) |
49
49
  | Ordinary EVM | unsigned preparation, user-wallet signature |
50
50
  | Optional owner-gated EVM executor | one exact bounded action after explicit `arm` |
51
51
 
@@ -302,7 +302,7 @@ router that knows which brain to use for which job.
302
302
  Prefer the local signer daemon so the agent never sees the vault passphrase:
303
303
 
304
304
  ```bash
305
- npx oracle-signer # has ORACLE_VAULT_PASSPHRASE
305
+ npx oracle-signer # reads the passphrase from the OS credential store
306
306
  # agent: ORACLE_SIGNER_URL + ORACLE_SIGNER_TOKEN only
307
307
  ```
308
308
 
@@ -263,7 +263,7 @@ const tools = [
263
263
  {
264
264
  name: "nft_prepare_list",
265
265
  description:
266
- "Prepare an NFT listing for OpenSea EVM, Magic Eden Solana, or Satflow Bitcoin after explicit user review. Requires userConfirmed=true plus asset, marketplace, price, currency where applicable, and future expiry. Returns approval/signing actions, an unsigned Solana transaction, or an unsigned Bitcoin PSBT. Never signs, submits, or broadcasts.",
266
+ "Prepare an NFT listing for OpenSea EVM, Magic Eden Solana, or Satflow Bitcoin after explicit user review. Requires userConfirmed=true plus asset, marketplace, price, currency where applicable, and future expiry. Magic Eden Solana also requires confirmation exactly: list <tokenMint> on magiceden-sol for <priceSol> SOL until <expiry>. Returns approval/signing actions, an unsigned Solana transaction, or an unsigned Bitcoin PSBT. Never signs, submits, or broadcasts.",
267
267
  inputSchema: {
268
268
  type: "object",
269
269
  required: ["marketplace", "userConfirmed"],
@@ -284,7 +284,8 @@ const tools = [
284
284
  tokenATA: { type: "string" },
285
285
  auctionHouse: { type: "string" },
286
286
  priceSol: { type: "string" },
287
- expiry: { description: "Future ISO 8601 timestamp or Unix seconds." },
287
+ expiry: { description: "Future ISO 8601 timestamp or Unix seconds. Magic Eden Solana listings require finite future expiry; no-expiry is refused." },
288
+ confirmation: { type: "string", description: "Required for Magic Eden Solana listings. Exact phrase: list <tokenMint> on magiceden-sol for <priceSol> SOL until <expiry>." },
288
289
  inscriptionId: { type: "string" },
289
290
  runesOutput: { type: "string" },
290
291
  ordAddress: { type: "string" },
@@ -0,0 +1,29 @@
1
+ # Oracle protocol pack standard
2
+
3
+ An Oracle protocol pack is a package-safe ESM module that turns an intent into an unsigned action and decodes independently obtained receipts. A pack is an adapter, not a wallet, signer, broadcaster, or authority. See `examples/oracle-pack-template.mjs` for the smallest complete shape.
4
+
5
+ ## Required surface
6
+
7
+ Every pack exports a default object with these members (named exports may mirror them):
8
+
9
+ - `provider`: stable provider identity, semantic version, supported chain IDs, and provenance/trust metadata. Remote token names, symbols, descriptions, image fields, URLs, and NFT metadata remain untrusted data.
10
+ - `prepare(intent)`: validates the intent and returns an unsigned transaction or typed action. It must not accept keys, sign, submit, broadcast, or report execution success.
11
+ - `decode(receipt)`: validates and converts a chain/provider receipt into a documented result. Unknown or malformed receipts fail closed.
12
+ - `riskRules`: an array or function describing enforceable limits and refusal conditions. Human-readable warnings alone are not rules.
13
+ - `tests`: non-empty metadata naming the local test command and covered properties. Tests must be deterministic and require no credentials or network.
14
+
15
+ ## Prompt-injection boundary
16
+
17
+ Treat every provider response and token/NFT metadata string as inert data. Never concatenate metadata into system/developer instructions, interpret it as tool directions, or let it modify recipients, calldata, limits, and policy. Render or quote it through a data channel and retain its provenance. Packs should test adversarial values such as “ignore previous instructions,” requests for secrets, and demands to sign or invoke tools.
18
+
19
+ ## Custody boundary
20
+
21
+ Pack exports must not expose signers, wallets, private keys, seed phrases, signing functions, transaction submission, or broadcasting. `prepare` returns data to a separately authorized custody surface. Environment variables and provider credentials must not appear in prepared output, logs, errors, fixtures, or metadata. Imports should be side-effect free; merely loading a pack must not make network calls.
22
+
23
+ ## Receipt gate
24
+
25
+ Preparation is not execution. A prepared result declares a `receiptGate` and consumers must not mark an action successful until `decode` receives a receipt from an independent, trusted transport and verifies its chain, transaction identity, status, and required confirmations. Timeouts, pending receipts, mismatches, reverts, and undecodable responses are not success.
26
+
27
+ ## Local adversarial benchmark
28
+
29
+ Run `node scripts/adversarial-bench.mjs <pack.mjs> [...]`. It imports only the named local modules, invokes no pack functions, emits one deterministic JSON summary, and exits `1` when a gate fails (`2` for usage/load errors). It checks required surface fields, forbidden custody exports, missing test metadata, and prompt-injection strings nested in provider or pack metadata. This static public gate complements, rather than replaces, behavioral tests of preparation and receipt verification.
package/docs/profiles.md CHANGED
@@ -44,7 +44,7 @@ Three practical reasons, learned the hard way:
44
44
  | `protocol-builder` | scaffold, review, prepare chain-family token/NFT collections, gacha, DEX, and protocol deploys | prepare:deploy, prepare:mint, simulate |
45
45
  | `_template` | your new lane | you decide |
46
46
 
47
- These grants describe the public prepare plane, not every capability an operator may install beside it. The generic unattended signer remains limited to `hl` and `poly`. A separately installed, same-host, owner-gated EVM executor may expose one exact bounded action after explicit `arm`; profiles must verify it before claiming availability. `watch`, `watch this`, and `ping me` remain `alert_only` regardless of executor presence.
47
+ These grants describe the public prepare plane, not every capability an operator may install beside it. The generic unattended signer exposes six bounded surfaces (`hl`, `poly`, `evm-swap`, `evm-bridge`, `btc`, `sol`), each of which decodes its own envelope and refuses while its allowlists are empty. A separately installed, same-host, owner-gated EVM executor may expose one exact bounded action after explicit `arm`; profiles must verify it before claiming availability. `watch`, `watch this`, and `ping me` remain `alert_only` regardless of executor presence.
48
48
 
49
49
  `protocol-builder` classifies each launch by chain family, then designs and
50
50
  prepares unsigned token, NFT collection, protocol, or mint-bot transactions. It
@@ -0,0 +1,38 @@
1
+ import assert from "node:assert/strict";
2
+
3
+ // Minimal Oracle protocol pack. It prepares unsigned actions; it never receives
4
+ // keys, signs, broadcasts, or claims that untrusted metadata is an instruction.
5
+
6
+ export const provider = Object.freeze({
7
+ id: "example-protocol",
8
+ version: "1.0.0",
9
+ chains: [8453],
10
+ metadata: { source: "on-chain", trust: "untrusted-data" },
11
+ });
12
+
13
+ export function prepare(intent) {
14
+ assert.ok(intent && provider.chains.includes(intent.chainId), "unsupported chain");
15
+ return {
16
+ kind: "unsigned-transaction",
17
+ chainId: intent.chainId,
18
+ transaction: { to: intent.to, data: intent.data ?? "0x", value: intent.value ?? "0" },
19
+ receiptGate: { required: true, confirmations: 1, match: ["chainId", "transaction.to"] },
20
+ };
21
+ }
22
+
23
+ export function decode(receipt) {
24
+ assert.ok(receipt && typeof receipt.status !== "undefined", "receipt required");
25
+ return { success: receipt.status === 1 || receipt.status === "0x1", transactionHash: receipt.transactionHash };
26
+ }
27
+
28
+ export const riskRules = Object.freeze([
29
+ { id: "explicit-recipient", field: "transaction.to", required: true },
30
+ { id: "receipt-before-success", field: "receiptGate.required", equals: true },
31
+ ]);
32
+
33
+ export const tests = Object.freeze({
34
+ command: "node --test test/oracle-pack-standard.test.mjs",
35
+ covers: ["prepare-is-unsigned", "decode-requires-receipt", "metadata-is-untrusted"],
36
+ });
37
+
38
+ export default Object.freeze({ provider, prepare, decode, riskRules, tests });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oracle-agent/oracle",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Oracle: prepare-only multichain agent control plane. Policy-bounded intents for a user-signed wallet. Self-custody by default — the public package never takes your key. Built for Hermes; no model key required.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -44,7 +44,12 @@
44
44
  "./action-semantics": "./src/action-semantics.mjs",
45
45
  "./address-book": "./src/address-book.mjs",
46
46
  "./nft-gas-war": "./src/nft-gas-war-guard.mjs",
47
- "./prepare-envelope": "./src/prepare-envelope.mjs"
47
+ "./prepare-envelope": "./src/prepare-envelope.mjs",
48
+ "./connect": "./src/onboarding/harness-configs.mjs",
49
+ "./receipts": "./src/action-receipts.mjs",
50
+ "./risk": "./src/portfolio-risk.mjs",
51
+ "./watch": "./src/watch-preferences.mjs",
52
+ "./signals": "./src/signals/index.mjs"
48
53
  },
49
54
  "scripts": {
50
55
  "test": "ORACLE_TEST_ISOLATE_SECRETS=1 ORACLE_ROUTE_ATTESTATION_SECRET=oracle-unit-test-secret node --test test/*.test.mjs",
@@ -41,7 +41,9 @@ the data plane). If it stays ambiguous, ask. Do not guess a chain.
41
41
  1. **The public router never signs.** It prepares, simulates, and explains; ordinary
42
42
  EVM artifacts require the user's wallet signature. Do not turn that public-plane
43
43
  boundary into the false claim that EVM execution is universally impossible. The
44
- generic unattended signer remains limited to `hl` and `poly`; a deployment may
44
+ generic unattended signer exposes six bounded surfaces (`hl`, `poly`, `evm-swap`,
45
+ `evm-bridge`, `btc`, `sol`), each of which decodes its own envelope and refuses
46
+ while its allowlists are empty; a deployment may also
45
47
  separately expose a same-host, owner-gated EVM executor. Verify that executor
46
48
  before describing it as available.
47
49
  `watch`, `watch this`, and `ping me` always mean `actionMode: alert_only`.
@@ -0,0 +1,81 @@
1
+ (() => {
2
+ "use strict";
3
+
4
+ const apiUrl = document.querySelector("#api-url");
5
+ const apiKey = document.querySelector("#api-key");
6
+ const walletAddress = document.querySelector("#wallet-address");
7
+ const output = document.querySelector("#config-output");
8
+ const label = document.querySelector("#config-label");
9
+ const status = document.querySelector("#copy-status");
10
+ const tabs = [...document.querySelectorAll("[data-harness]")];
11
+ let selected = "hermes";
12
+
13
+ const cleanUrl = () => apiUrl.value.trim().replace(/\/+$/, "");
14
+ const env = () => {
15
+ const values = {
16
+ ORACLE_API_URL: cleanUrl() || "https://api.oracle-agent.dev",
17
+ ORACLE_READ_API_KEY: apiKey.value.trim() || "YOUR_READ_API_KEY"
18
+ };
19
+ if (walletAddress.value.trim()) values.ORACLE_WALLET_ADDRESS = walletAddress.value.trim();
20
+ return values;
21
+ };
22
+
23
+ const configs = {
24
+ hermes: () => ({
25
+ mcpServers: { oracle: { command: "npx", args: ["-y", "@oracle-agent/oracle-data-mcp"], env: env() } }
26
+ }),
27
+ claude: () => ({
28
+ mcpServers: { oracle: { command: "npx", args: ["-y", "@oracle-agent/oracle-data-mcp"], env: env() } }
29
+ }),
30
+ codex: () => {
31
+ const values = env();
32
+ const lines = [
33
+ "[mcp_servers.oracle]",
34
+ 'command = "npx"',
35
+ 'args = ["-y", "@oracle-agent/oracle-data-mcp"]',
36
+ "",
37
+ "[mcp_servers.oracle.env]",
38
+ ...Object.entries(values).map(([key, value]) => `${key} = ${JSON.stringify(value)}`)
39
+ ];
40
+ return lines.join("\n");
41
+ },
42
+ cursor: () => ({
43
+ mcpServers: { oracle: { command: "npx", args: ["-y", "@oracle-agent/oracle-data-mcp"], env: env() } }
44
+ }),
45
+ mcp: () => ({
46
+ name: "oracle",
47
+ transport: "stdio",
48
+ command: "npx",
49
+ args: ["-y", "@oracle-agent/oracle-data-mcp"],
50
+ env: env()
51
+ })
52
+ };
53
+
54
+ const names = { hermes: "Hermes", claude: "Claude Code", codex: "Codex", cursor: "Cursor", mcp: "Generic MCP" };
55
+
56
+ function render() {
57
+ const config = configs[selected]();
58
+ output.textContent = typeof config === "string" ? config : JSON.stringify(config, null, 2);
59
+ label.textContent = `${names[selected]} config`;
60
+ status.textContent = "";
61
+ }
62
+
63
+ async function copy(text, message) {
64
+ try {
65
+ await navigator.clipboard.writeText(text);
66
+ status.textContent = message;
67
+ } catch {
68
+ status.textContent = "Copy was blocked. Select the text and copy it manually.";
69
+ }
70
+ }
71
+
72
+ tabs.forEach((tab) => tab.addEventListener("click", () => {
73
+ selected = tab.dataset.harness;
74
+ tabs.forEach((item) => item.setAttribute("aria-selected", String(item === tab)));
75
+ render();
76
+ }));
77
+ [apiUrl, apiKey, walletAddress].forEach((input) => input.addEventListener("input", render));
78
+ document.querySelector("#copy-button").addEventListener("click", () => copy(output.textContent, `${names[selected]} config copied.`));
79
+ document.querySelector("#copy-install").addEventListener("click", () => copy("npx @oracle-agent/oracle init", "Local operator install command copied."));
80
+ render();
81
+ })();
@@ -0,0 +1,83 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="description" content="Connect your agent harness to Oracle read tools without giving up custody." />
7
+ <title>Oracle Agent Connect</title>
8
+ <link rel="stylesheet" href="styles.css" />
9
+ </head>
10
+ <body>
11
+ <main class="shell">
12
+ <header class="hero">
13
+ <a class="wordmark" href="/">ORACLE</a>
14
+ <p class="eyebrow">AGENT CONNECT</p>
15
+ <h1>Give your agent market context.<br /><span>Keep custody local.</span></h1>
16
+ <p class="lede">Connect a read-only Oracle API to your preferred harness. The model proposes actions; you review and authorize them through your local operator.</p>
17
+ </header>
18
+
19
+ <section class="card setup" aria-labelledby="connection-title">
20
+ <div class="section-heading">
21
+ <span class="step">01</span>
22
+ <div>
23
+ <h2 id="connection-title">Your connection</h2>
24
+ <p>These values stay in this browser and are only used to build the config below.</p>
25
+ </div>
26
+ </div>
27
+ <div class="fields">
28
+ <label>Agent API URL
29
+ <input id="api-url" type="url" value="https://api.oracle-agent.dev" spellcheck="false" autocomplete="url" />
30
+ </label>
31
+ <label>Read API key
32
+ <input id="api-key" type="password" placeholder="Your read-only API key" spellcheck="false" autocomplete="off" />
33
+ <span class="hint">Use only a user-owned, read-only API credential.</span>
34
+ </label>
35
+ <label>Public wallet address <span class="optional">optional</span>
36
+ <input id="wallet-address" type="text" placeholder="0x... or a public Solana address" spellcheck="false" autocomplete="off" />
37
+ <span class="hint">Used to personalize portfolio reads. No wallet connection or signature.</span>
38
+ </label>
39
+ </div>
40
+ </section>
41
+
42
+ <section class="card config" aria-labelledby="config-title">
43
+ <div class="section-heading">
44
+ <span class="step">02</span>
45
+ <div>
46
+ <h2 id="config-title">Choose your harness</h2>
47
+ <p>Copy a ready-to-paste cloud/read MCP configuration.</p>
48
+ </div>
49
+ </div>
50
+ <div class="tabs" role="tablist" aria-label="Agent harness">
51
+ <button role="tab" aria-selected="true" data-harness="hermes">Hermes</button>
52
+ <button role="tab" aria-selected="false" data-harness="claude">Claude Code</button>
53
+ <button role="tab" aria-selected="false" data-harness="codex">Codex</button>
54
+ <button role="tab" aria-selected="false" data-harness="cursor">Cursor</button>
55
+ <button role="tab" aria-selected="false" data-harness="mcp">Generic MCP</button>
56
+ </div>
57
+ <div class="code-wrap">
58
+ <div class="code-bar">
59
+ <span id="config-label">Hermes config</span>
60
+ <button id="copy-button" class="copy" type="button">Copy config</button>
61
+ </div>
62
+ <pre><code id="config-output"></code></pre>
63
+ </div>
64
+ <p id="copy-status" class="status" aria-live="polite"></p>
65
+ </section>
66
+
67
+ <section class="operator" aria-labelledby="operator-title">
68
+ <div class="operator-mark" aria-hidden="true">LOCAL</div>
69
+ <div>
70
+ <p class="eyebrow">SEPARATE EXECUTION LANE</p>
71
+ <h2 id="operator-title">Install the local operator to execute</h2>
72
+ <p>The cloud/read MCP can research, quote, simulate, and prepare. Execution belongs in a separate local process under your policy and approval controls.</p>
73
+ <div class="command"><code>npx @oracle-agent/oracle init</code><button id="copy-install" type="button">Copy</button></div>
74
+ </div>
75
+ </section>
76
+
77
+ <footer>
78
+ <p><strong>Custody stays local.</strong> The model proposes; the owner authorizes. This page never connects a wallet, requests a signature, or sends the values you enter.</p>
79
+ </footer>
80
+ </main>
81
+ <script src="app.js"></script>
82
+ </body>
83
+ </html>
@@ -0,0 +1,70 @@
1
+ :root {
2
+ color-scheme: dark;
3
+ --ink: #07110f;
4
+ --panel: #0d1a17;
5
+ --panel-high: #12231f;
6
+ --line: #26443d;
7
+ --mint: #55e6c1;
8
+ --paper: #edf8f4;
9
+ --muted: #9bb8b0;
10
+ }
11
+
12
+ * { box-sizing: border-box; }
13
+
14
+ body {
15
+ margin: 0;
16
+ background:
17
+ radial-gradient(circle at 85% 8%, rgba(85, 230, 193, 0.12), transparent 25rem),
18
+ linear-gradient(rgba(85, 230, 193, 0.025) 1px, transparent 1px),
19
+ linear-gradient(90deg, rgba(85, 230, 193, 0.025) 1px, transparent 1px),
20
+ var(--ink);
21
+ background-size: auto, 48px 48px, 48px 48px, auto;
22
+ color: var(--paper);
23
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
24
+ min-height: 100vh;
25
+ }
26
+
27
+ button, input { font: inherit; }
28
+ .shell { width: min(980px, calc(100% - 32px)); margin: auto; padding: 56px 0 32px; }
29
+ .hero { max-width: 780px; margin-bottom: 42px; }
30
+ .wordmark { color: var(--paper); text-decoration: none; font-size: 14px; font-weight: 800; letter-spacing: .28em; }
31
+ .eyebrow { margin: 36px 0 10px; color: var(--mint); font: 700 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .18em; }
32
+ h1 { margin: 0; font-size: clamp(40px, 7vw, 76px); line-height: .98; letter-spacing: -.055em; }
33
+ h1 span { color: var(--mint); }
34
+ .lede { max-width: 660px; margin: 24px 0 0; color: var(--muted); font-size: 18px; line-height: 1.65; }
35
+ .card { background: rgba(13, 26, 23, .92); border: 1px solid var(--line); border-radius: 18px; padding: clamp(22px, 4vw, 38px); margin-top: 18px; box-shadow: 0 22px 70px rgba(0, 0, 0, .22); }
36
+ .section-heading { display: flex; align-items: flex-start; gap: 18px; margin-bottom: 28px; }
37
+ .step { display: grid; place-items: center; min-width: 42px; height: 42px; border: 1px solid var(--mint); border-radius: 50%; color: var(--mint); font: 700 12px ui-monospace, monospace; }
38
+ h2 { margin: 0; font-size: 24px; letter-spacing: -.02em; }
39
+ .section-heading p, .operator p { color: var(--muted); margin: 6px 0 0; line-height: 1.55; }
40
+ .fields { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
41
+ label { display: flex; flex-direction: column; gap: 9px; color: var(--paper); font-size: 13px; font-weight: 700; }
42
+ label:first-child { grid-column: 1 / -1; }
43
+ .optional { color: var(--muted); font-weight: 400; }
44
+ input { width: 100%; color: var(--paper); background: var(--ink); border: 1px solid var(--line); border-radius: 9px; padding: 13px 14px; outline: none; }
45
+ input:focus { border-color: var(--mint); box-shadow: 0 0 0 3px rgba(85, 230, 193, .1); }
46
+ .hint { color: var(--muted); font-size: 11px; font-weight: 400; line-height: 1.4; }
47
+ .tabs { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; }
48
+ .tabs button { flex: 0 0 auto; color: var(--muted); background: transparent; border: 1px solid var(--line); border-radius: 999px; padding: 9px 15px; cursor: pointer; }
49
+ .tabs button[aria-selected="true"] { color: var(--ink); background: var(--mint); border-color: var(--mint); font-weight: 700; }
50
+ .code-wrap { margin-top: 18px; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; background: var(--ink); }
51
+ .code-bar { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid var(--line); color: var(--muted); font-size: 12px; }
52
+ .copy, .command button { color: var(--mint); background: transparent; border: 0; cursor: pointer; font-weight: 700; }
53
+ pre { min-height: 270px; margin: 0; padding: 20px; overflow: auto; color: #c8f7e9; font: 12px/1.7 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
54
+ .status { min-height: 20px; margin: 10px 0 0; color: var(--mint); font-size: 12px; }
55
+ .operator { display: grid; grid-template-columns: 120px 1fr; gap: 30px; margin: 54px 0; padding: 0 20px; align-items: center; }
56
+ .operator .eyebrow { margin: 0 0 8px; }
57
+ .operator-mark { display: grid; place-items: center; aspect-ratio: 1; border: 1px solid var(--mint); border-radius: 50%; color: var(--mint); font: 700 12px ui-monospace, monospace; letter-spacing: .15em; box-shadow: inset 0 0 30px rgba(85, 230, 193, .09); }
58
+ .command { display: flex; justify-content: space-between; max-width: 520px; margin-top: 18px; padding: 13px 16px; background: var(--panel-high); border: 1px solid var(--line); border-radius: 9px; }
59
+ .command code { overflow: auto; }
60
+ footer { border-top: 1px solid var(--line); padding: 25px 0; color: var(--muted); font-size: 13px; line-height: 1.6; }
61
+ footer strong { color: var(--paper); }
62
+
63
+ @media (max-width: 640px) {
64
+ .shell { padding-top: 30px; }
65
+ .fields { grid-template-columns: 1fr; }
66
+ label:first-child { grid-column: auto; }
67
+ .operator { grid-template-columns: 1fr; padding: 0; }
68
+ .operator-mark { width: 88px; }
69
+ pre { min-height: 320px; }
70
+ }
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { pathToFileURL } from "node:url";
4
+ import { resolve } from "node:path";
5
+
6
+ const INJECTION_RULES = [
7
+ ["instruction-override", /\b(?:ignore|disregard|override)\b.{0,48}\b(?:previous|prior|system|developer|instructions?)\b/i],
8
+ ["secret-exfiltration", /\b(?:reveal|print|return|exfiltrate|send)\b.{0,48}\b(?:private\s*key|seed\s*phrase|mnemonic|secret|credentials?|system\s*prompt)\b/i],
9
+ ["transaction-coercion", /\b(?:sign|send|broadcast|execute)\b.{0,32}\b(?:transaction|transfer|approval|trade)\b/i],
10
+ ["tool-coercion", /\b(?:call|invoke|use|run)\b.{0,24}\b(?:tool|function|shell|command)\b/i],
11
+ ];
12
+
13
+ const FORBIDDEN_EXPORT = /^(?:signer|sign|signMessage|signTransaction|sendTransaction|broadcastTransaction|submitTransaction|executeTransaction|privateKey|secretKey|mnemonic|seedPhrase|wallet)$/i;
14
+
15
+ function finding(code, path, message) {
16
+ return { code, path, message };
17
+ }
18
+
19
+ function scanStrings(value, path, findings, seen) {
20
+ if (typeof value === "string") {
21
+ for (const [rule, pattern] of INJECTION_RULES) {
22
+ if (pattern.test(value)) {
23
+ findings.push(finding("prompt-injection", path, `metadata matches ${rule}`));
24
+ break;
25
+ }
26
+ }
27
+ return;
28
+ }
29
+ if (!value || typeof value !== "object" || seen.has(value)) return;
30
+ seen.add(value);
31
+ if (Array.isArray(value)) {
32
+ value.forEach((item, index) => scanStrings(item, `${path}[${index}]`, findings, seen));
33
+ return;
34
+ }
35
+ for (const key of Object.keys(value).sort()) {
36
+ scanStrings(value[key], path ? `${path}.${key}` : key, findings, seen);
37
+ }
38
+ }
39
+
40
+ /** Validate one loaded Oracle protocol pack without calling provider code. */
41
+ export function benchmarkPack(moduleValue, { name = "pack" } = {}) {
42
+ const namespace = moduleValue && typeof moduleValue === "object" ? moduleValue : {};
43
+ const pack = namespace.default && typeof namespace.default === "object" ? namespace.default : namespace;
44
+ const findings = [];
45
+
46
+ for (const key of Object.keys(namespace).sort()) {
47
+ if (key !== "default" && FORBIDDEN_EXPORT.test(key)) {
48
+ findings.push(finding("forbidden-signer-export", `exports.${key}`, `pack exports custody capability ${key}`));
49
+ }
50
+ }
51
+ for (const key of Object.keys(pack).sort()) {
52
+ if (FORBIDDEN_EXPORT.test(key)) {
53
+ findings.push(finding("forbidden-signer-export", `pack.${key}`, `pack exports custody capability ${key}`));
54
+ }
55
+ }
56
+
57
+ if (!pack.provider || typeof pack.provider !== "object") {
58
+ findings.push(finding("missing-provider", "pack.provider", "provider metadata is required"));
59
+ }
60
+ if (typeof pack.prepare !== "function") {
61
+ findings.push(finding("missing-prepare", "pack.prepare", "prepare must be a function"));
62
+ }
63
+ if (typeof pack.decode !== "function") {
64
+ findings.push(finding("missing-decoder", "pack.decode", "decode must be a function"));
65
+ }
66
+ if (!(Array.isArray(pack.riskRules) || typeof pack.riskRules === "function")) {
67
+ findings.push(finding("missing-risk-rules", "pack.riskRules", "riskRules must be an array or function"));
68
+ }
69
+ if (!pack.tests || typeof pack.tests !== "object" || Array.isArray(pack.tests) || Object.keys(pack.tests).length === 0) {
70
+ findings.push(finding("missing-tests", "pack.tests", "non-empty tests metadata is required"));
71
+ }
72
+
73
+ // Metadata is untrusted even when nested below token/NFT records. Scan data only;
74
+ // functions are deliberately never invoked by this local harness.
75
+ scanStrings(pack.metadata, "pack.metadata", findings, new Set());
76
+ scanStrings(pack.provider, "pack.provider", findings, new Set());
77
+
78
+ findings.sort((a, b) => `${a.code}\0${a.path}\0${a.message}`.localeCompare(`${b.code}\0${b.path}\0${b.message}`));
79
+ return { name, ok: findings.length === 0, findings };
80
+ }
81
+
82
+ /** Run a stable summary over already-loaded fixture/module records. */
83
+ export function runAdversarialBenchmark(entries) {
84
+ const results = entries
85
+ .map((entry, index) => benchmarkPack(entry.module ?? entry, { name: entry.name ?? `pack-${index + 1}` }))
86
+ .sort((a, b) => a.name.localeCompare(b.name));
87
+ return {
88
+ ok: results.every((result) => result.ok),
89
+ packs: results.length,
90
+ passed: results.filter((result) => result.ok).length,
91
+ failed: results.filter((result) => !result.ok).length,
92
+ findings: results.reduce((count, result) => count + result.findings.length, 0),
93
+ results,
94
+ };
95
+ }
96
+
97
+ async function main(paths) {
98
+ if (paths.length === 0) throw new Error("usage: node scripts/adversarial-bench.mjs <pack.mjs> [...]");
99
+ const entries = [];
100
+ for (const input of paths) {
101
+ const absolute = resolve(input);
102
+ entries.push({ name: input, module: await import(pathToFileURL(absolute).href) });
103
+ }
104
+ const summary = runAdversarialBenchmark(entries);
105
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
106
+ if (!summary.ok) process.exitCode = 1;
107
+ }
108
+
109
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
110
+ main(process.argv.slice(2)).catch((error) => {
111
+ process.stdout.write(`${JSON.stringify({ ok: false, error: String(error.message || error) }, null, 2)}\n`);
112
+ process.exitCode = 2;
113
+ });
114
+ }
@@ -8,7 +8,7 @@ description: Use for watch, ping, prepare, arm, sign, send, or execution-capabil
8
8
  Keep these planes distinct:
9
9
 
10
10
  1. **Public prepare plane**: reads, quotes, simulates, and prepares unsigned artifacts. It never signs or broadcasts.
11
- 2. **Generic Oracle signer**: the unattended daemon surfaces remain `hl` and `poly`. This does not grant EVM authority.
11
+ 2. **Generic Oracle signer**: the unattended daemon exposes six *bounded* surfaces `hl`, `poly`, `evm-swap`, `evm-bridge`, `btc`, `sol`. Bounded is not generic chain authority: each surface decodes and validates its own envelope shape and refuses while its allowlists are empty. Enablement is never authority; every signature still needs one exact owner-confirmed grant.
12
12
  3. **User-wallet EVM**: ordinary EVM preparations require the user's wallet signature.
13
13
  4. **Optional bounded EVM execution**: a deployment may expose a separate same-host, owner-gated EVM executor such as MAD. Verify its status and policy before saying it is available. Never infer it from the public package or the generic signer.
14
14