@oracle-agent/oracle 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +74 -20
  2. package/SETUP.md +15 -2
  3. package/bin/oracle-data-mcp.mjs +1 -1
  4. package/bin/oracle.mjs +33 -0
  5. package/docs/cli.md +19 -0
  6. package/docs/connectors.md +13 -0
  7. package/docs/oracle-pack-standard.md +29 -0
  8. package/docs/profiles.md +1 -1
  9. package/examples/oracle-pack-template.mjs +38 -0
  10. package/package.json +13 -4
  11. package/profiles/oracle/SOUL.md +3 -1
  12. package/profiles/robinhood-agent/SOUL.md +5 -3
  13. package/public/agent-connect/app.js +84 -0
  14. package/public/agent-connect/index.html +83 -0
  15. package/public/agent-connect/styles.css +70 -0
  16. package/public/oracle-splash/assets/wordmarks/across.svg +4 -0
  17. package/public/oracle-splash/assets/wordmarks/aerodrome.svg +23 -0
  18. package/public/oracle-splash/assets/wordmarks/balancer.svg +3 -0
  19. package/public/oracle-splash/assets/wordmarks/cowswap.svg +1 -0
  20. package/public/oracle-splash/assets/wordmarks/gmx.svg +13 -0
  21. package/public/oracle-splash/assets/wordmarks/hyperliquid.svg +21 -0
  22. package/public/oracle-splash/assets/wordmarks/hyperswap.svg +13 -0
  23. package/public/oracle-splash/assets/wordmarks/jupiter.svg +35 -0
  24. package/public/oracle-splash/assets/wordmarks/lifi.png +0 -0
  25. package/public/oracle-splash/assets/wordmarks/magic-eden.svg +1 -0
  26. package/public/oracle-splash/assets/wordmarks/morpho.svg +1 -0
  27. package/public/oracle-splash/assets/wordmarks/odos.svg +3 -0
  28. package/public/oracle-splash/assets/wordmarks/oneinch.svg +6 -0
  29. package/public/oracle-splash/assets/wordmarks/opensea.svg +1 -0
  30. package/public/oracle-splash/assets/wordmarks/pancakeswap.svg +18 -0
  31. package/public/oracle-splash/assets/wordmarks/paraswap.svg +16 -0
  32. package/public/oracle-splash/assets/wordmarks/pendle.png +0 -0
  33. package/public/oracle-splash/assets/wordmarks/polymarket.png +0 -0
  34. package/public/oracle-splash/assets/wordmarks/quickswap.png +0 -0
  35. package/public/oracle-splash/assets/wordmarks/relay.svg +8 -0
  36. package/public/oracle-splash/assets/wordmarks/stargate.svg +15 -0
  37. package/public/oracle-splash/assets/wordmarks/uniswap.svg +18 -0
  38. package/public/oracle-splash/assets/wordmarks/velodrome.svg +84 -0
  39. package/public/oracle-splash/index.html +789 -485
  40. package/scripts/adversarial-bench.mjs +114 -0
  41. package/scripts/check-doc-drift.mjs +111 -0
  42. package/skills/oracle-action-semantics/SKILL.md +1 -1
  43. package/src/action-receipts.mjs +168 -0
  44. package/src/address-book.mjs +53 -5
  45. package/src/cli/commands/credential.mjs +9 -0
  46. package/src/cli/commands/data-mcp.mjs +18 -0
  47. package/src/cli/commands/data.mjs +73 -0
  48. package/src/cli/commands/doctor.mjs +131 -0
  49. package/src/cli/commands/help.mjs +7 -0
  50. package/src/cli/commands/init.mjs +27 -0
  51. package/src/cli/commands/mcp.mjs +142 -0
  52. package/src/cli/commands/prepare.mjs +12 -0
  53. package/src/cli/commands/public.mjs +20 -0
  54. package/src/cli/commands/route.mjs +12 -0
  55. package/src/cli/commands/runner.mjs +9 -0
  56. package/src/cli/commands/scan.mjs +12 -0
  57. package/src/cli/commands/sign.mjs +21 -0
  58. package/src/cli/commands/signer.mjs +9 -0
  59. package/src/cli/commands/upgrade.mjs +12 -0
  60. package/src/cli/commands/vault.mjs +9 -0
  61. package/src/cli/commands/version.mjs +22 -0
  62. package/src/cli/first-run.mjs +20 -0
  63. package/src/cli/kernel.mjs +198 -0
  64. package/src/cli/mcp-targets/chatgpt.mjs +51 -0
  65. package/src/cli/mcp-targets/claude-code.mjs +39 -0
  66. package/src/cli/mcp-targets/claude-desktop.mjs +24 -0
  67. package/src/cli/mcp-targets/codex.mjs +41 -0
  68. package/src/cli/mcp-targets/shared.mjs +61 -0
  69. package/src/cli/operator-dispatch.mjs +258 -0
  70. package/src/cli/paths.mjs +78 -0
  71. package/src/cli/spawn-child.mjs +43 -0
  72. package/src/exec-policy.mjs +6 -0
  73. package/src/index.mjs +24 -0
  74. package/src/onboarding/harness-configs.mjs +102 -0
  75. package/src/oracle-env.mjs +11 -2
  76. package/src/portfolio-risk.mjs +170 -0
  77. package/src/public-api/connect-agent.mjs +29 -10
  78. package/src/signals/engine.mjs +146 -0
  79. package/src/signals/index.mjs +1 -0
  80. package/src/watch-preferences.mjs +85 -0
@@ -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
+ }
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // Release gate: shipped prose must not contradict shipped behavior.
3
+ //
4
+ // Why this exists. The 2026-08-01 four-model audit found the same stale claim
5
+ // in 20 places across both packages: docs said the unattended signer was
6
+ // limited to `hl` and `poly` long after six bounded surfaces had shipped. The
7
+ // worst instances were not READMEs — they were `profiles/oracle/SOUL.md` and
8
+ // `skills/oracle-action-semantics/SKILL.md`, which are read by the MODEL. An
9
+ // agent told that EVM daemon execution is impossible reasons incorrectly about
10
+ // its own authority, and no test suite can see a false sentence.
11
+ //
12
+ // This scans every doc surface that ships in the tarball, including model-facing
13
+ // SOUL and skill text, which is exactly where drift hides longest.
14
+
15
+ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
16
+ import { fileURLToPath } from "node:url";
17
+ import path from "node:path";
18
+
19
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
+ const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
21
+ const failures = [];
22
+
23
+ // Claims that shipped in a real release and are now false.
24
+ const STALE_CLAIMS = [
25
+ { re: /limited\s+to\s+`?hl`?\s+and\s+`?poly`?/i, why: "says the unattended signer is limited to hl+poly" },
26
+ { re: /only\s+`?hl`?\s+and\s+`?poly`?\s+are\s+enabled/i, why: "says only hl+poly are daemon-enabled" },
27
+ { re: /daemon\s+surfaces\s+remain\s+`?hl`?\s+and\s+`?poly`?/i, why: "freezes the surface list at hl+poly" },
28
+ { re: /policy-bounded\s+`?hl`?\s+and\s+`?poly`?\s+only/i, why: "restricts the daemon to hl+poly" },
29
+ { re: /agent-facing\s+daemon\s+supports\s+only\s+policy-bounded\s+Hyperliquid\s+and\s+Polymarket\s+execution/i, why: "says the agent-facing daemon supports only Hyperliquid+Polymarket" },
30
+ { re: /`?evm`?,?\s+`?btc`?,?\s+and\s+`?sol`?\s+fail\s+closed\s+at\s+the\s+daemon/i, why: "claims evm/btc/sol are refused at the daemon" },
31
+ { re: /EVM,\s*BTC,?\s*and\s+Solana\s+are\s+not\s+daemon\s+surfaces/i, why: "denies evm/btc/sol are daemon surfaces" },
32
+ { re: /companion\s+daemon\s+is\s+`?hl`?\s*\/\s*`?poly`?[- ]only/i, why: "describes the companion daemon as hl/poly-only" },
33
+ { re: /daemon\s+supports\s+only\s+policy-bounded\s+Hyperliquid\s+and\s+Polymarket/i, why: "describes the daemon as Hyperliquid/Polymarket-only" },
34
+ { re: /unattended\s+signer\s+remains\s+`?hl`?\s*\/\s*`?poly`?/i, why: "freezes the generic signer at hl/poly" },
35
+ { re: /supports\s+autonomous\s+Hyperliquid\s+and\s+Polymarket\s+execution/i, why: "describes the daemon as Hyperliquid/Polymarket-only" },
36
+ { re: /\(HL\s*\+\s*Poly,\s*capped\)/i, why: "documents autosign as hl/poly-only" },
37
+ { re: /policy-bounded\s+HL\s*\/\s*Polymarket\s+exec/i, why: "documents the signer daemon as hl/poly-only" },
38
+ { re: /policy-bounded\s+daemon\s+surfaces:\s*hl\s*\|\s*poly\s*$/i, why: "documents only two of six daemon surfaces" },
39
+ { re: /ORACLE_SIGNER_SURFACES=hl,poly\s*$/i, why: "quickstart freezes ORACLE_SIGNER_SURFACES at hl,poly" },
40
+ { re: /ORACLE_AUTONOMOUS_TRADING=1.*(?:or\s+run\s+the\s+signer\s+daemon|same(?:-tier)?).*oracle-signer/i, why: "frames direct exec and oracle-signer as equivalent agent paths" },
41
+ { re: /(?:or\s+run\s+the\s+signer\s+daemon|same(?:-tier)?).*oracle-signer.*ORACLE_AUTONOMOUS_TRADING=1/i, why: "frames direct exec and oracle-signer as equivalent agent paths" },
42
+ ];
43
+
44
+ // Version pins that go stale the moment a release ships. The audit found the
45
+ // quickstart advertising 0.3.6/0.6.1 inside a 0.4.0 tarball, so a copy-paste
46
+ // install handed users an older wall set.
47
+ function checkVersionPins(rel, text) {
48
+ const lines = text.split("\n");
49
+ lines.forEach((line, i) => {
50
+ const pin = line.match(/@oracle-agent\/oracle@(\d+\.\d+\.\d+)/);
51
+ if (pin && pin[1] !== pkg.version) {
52
+ failures.push(
53
+ `${rel}:${i + 1} pins @oracle-agent/oracle@${pin[1]} but this package is ${pkg.version}\n ${line.trim().slice(0, 120)}`,
54
+ );
55
+ }
56
+ const latest = line.match(/Latest:.*oracle\s+`(\d+\.\d+\.\d+)`/i);
57
+ if (latest && latest[1] !== pkg.version) {
58
+ failures.push(
59
+ `${rel}:${i + 1} advertises oracle ${latest[1]} as latest but this package is ${pkg.version}\n ${line.trim().slice(0, 120)}`,
60
+ );
61
+ }
62
+ });
63
+ }
64
+
65
+ // Every markdown surface that ships to a user or a model.
66
+ function docFiles() {
67
+ const out = [];
68
+ for (const rel of ["README.md", "SETUP.md", "SECURITY.md"]) {
69
+ if (existsSync(path.join(root, rel))) out.push(rel);
70
+ }
71
+ for (const dir of ["docs", "profiles", "skills"]) {
72
+ const abs = path.join(root, dir);
73
+ if (!existsSync(abs)) continue;
74
+ const walk = (d) => {
75
+ for (const entry of readdirSync(d)) {
76
+ const full = path.join(d, entry);
77
+ if (statSync(full).isDirectory()) walk(full);
78
+ else if (entry.endsWith(".md")) out.push(path.relative(root, full));
79
+ }
80
+ };
81
+ walk(abs);
82
+ }
83
+ return out;
84
+ }
85
+
86
+ const files = docFiles();
87
+ for (const rel of files) {
88
+ const text = readFileSync(path.join(root, rel), "utf8");
89
+ text.split("\n").forEach((line, i) => {
90
+ for (const claim of STALE_CLAIMS) {
91
+ if (claim.re.test(line)) {
92
+ failures.push(`${rel}:${i + 1} ${claim.why}\n ${line.trim().slice(0, 140)}`);
93
+ }
94
+ }
95
+ });
96
+ checkVersionPins(rel, text);
97
+ }
98
+
99
+ console.log(`package version: ${pkg.version}`);
100
+ console.log(`doc surfaces scanned: ${files.length} (README/SETUP/SECURITY + docs/ + profiles/ + skills/)`);
101
+ console.log("");
102
+
103
+ if (failures.length) {
104
+ console.error(`FAIL — ${failures.length} doc/code drift finding(s):\n`);
105
+ for (const f of failures) console.error(` ${f}\n`);
106
+ console.error("Shipped prose contradicts shipped behavior. Model-facing SOUL/skill text counts:");
107
+ console.error("an agent that reads a false capability claim reasons wrongly about its own authority.");
108
+ process.exit(1);
109
+ }
110
+
111
+ console.log("PASS — no stale capability claims or version pins in shipped docs");
@@ -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
 
@@ -0,0 +1,168 @@
1
+ // Deterministic, public-only receipts for prepared and executed actions.
2
+ //
3
+ // This module deliberately has no authority and no I/O: it does not authorize,
4
+ // sign, broadcast, or fetch. It only records facts supplied by its caller.
5
+
6
+ import { createHash } from "node:crypto";
7
+
8
+ export const ACTION_RECEIPT_VERSION = 1;
9
+
10
+ const SECRET_KEYS = new Set([
11
+ "privatekey",
12
+ "mnemonic",
13
+ "bearer",
14
+ "authorization",
15
+ "signature",
16
+ ]);
17
+
18
+ export class ActionReceiptSecretError extends Error {
19
+ constructor(path) {
20
+ super(`action receipt refused: secret-bearing field at ${path}`);
21
+ this.name = "ActionReceiptSecretError";
22
+ this.code = "ACTION_RECEIPT_SECRET";
23
+ }
24
+ }
25
+
26
+ function normalizedKey(key) {
27
+ return String(key).replace(/[^a-z0-9]/gi, "").toLowerCase();
28
+ }
29
+
30
+ function isSecretKey(key) {
31
+ const normalized = normalizedKey(key);
32
+ return [...SECRET_KEYS].some((secret) => normalized.includes(secret));
33
+ }
34
+
35
+ function publicValue(value, path = "$") {
36
+ if (value == null || typeof value === "string" || typeof value === "boolean") return value;
37
+ if (typeof value === "number") {
38
+ if (!Number.isFinite(value)) throw new TypeError(`action receipt: non-finite number at ${path}`);
39
+ return value;
40
+ }
41
+ if (typeof value === "bigint") return value.toString();
42
+ if (Array.isArray(value)) return value.map((item, index) => publicValue(item, `${path}[${index}]`));
43
+ if (typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype) {
44
+ throw new TypeError(`action receipt: JSON-like value required at ${path}`);
45
+ }
46
+
47
+ const out = {};
48
+ for (const key of Object.keys(value).sort()) {
49
+ const childPath = `${path}.${key}`;
50
+ if (isSecretKey(key)) throw new ActionReceiptSecretError(childPath);
51
+ if (value[key] !== undefined) out[key] = publicValue(value[key], childPath);
52
+ }
53
+ return out;
54
+ }
55
+
56
+ /** Recursively reject secret-bearing fields and return a detached public copy. */
57
+ export function assertNoReceiptSecrets(value) {
58
+ return publicValue(value);
59
+ }
60
+
61
+ /** Canonical JSON with recursively sorted object keys. */
62
+ export function canonicalReceiptJson(value) {
63
+ const safe = publicValue(value);
64
+ if (safe === null || typeof safe !== "object") return JSON.stringify(safe);
65
+ if (Array.isArray(safe)) return `[${safe.map(canonicalReceiptJson).join(",")}]`;
66
+ return `{${Object.keys(safe).map((key) => `${JSON.stringify(key)}:${canonicalReceiptJson(safe[key])}`).join(",")}}`;
67
+ }
68
+
69
+ /** Hash exactly the public receipt fields, excluding the self-referential id. */
70
+ export function computeReceiptId(receipt) {
71
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
72
+ throw new TypeError("action receipt: object required");
73
+ }
74
+ const { receiptId: _ignored, summary: _derived, ...publicFields } = receipt;
75
+ return createHash("sha256").update(canonicalReceiptJson(publicFields)).digest("hex");
76
+ }
77
+
78
+ function supplied(input, ...keys) {
79
+ for (const key of keys) if (input[key] !== undefined) return input[key];
80
+ return undefined;
81
+ }
82
+
83
+ function required(input, keys, label) {
84
+ const value = supplied(input, ...keys);
85
+ if (value === undefined || value === null) throw new TypeError(`action receipt: ${label} required`);
86
+ return value;
87
+ }
88
+
89
+ function balanceFields(input) {
90
+ const balances = supplied(input, "balances", "balanceChanges");
91
+ const before = supplied(input, "beforeBalances", "balancesBefore", "before");
92
+ const after = supplied(input, "afterBalances", "balancesAfter", "after");
93
+ if (balances !== undefined) return { balances };
94
+ if (before === undefined && after === undefined) return {};
95
+ return { balances: { ...(before === undefined ? {} : { before }), ...(after === undefined ? {} : { after }) } };
96
+ }
97
+
98
+ /**
99
+ * Normalize caller-supplied prepare/execute facts into the versioned schema.
100
+ * Unknown input fields are intentionally omitted from the receipt.
101
+ */
102
+ export function normalizeActionReceipt(input = {}) {
103
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
104
+ throw new TypeError("action receipt: input object required");
105
+ }
106
+ // Scan the complete input before selecting fields so a secret cannot be
107
+ // hidden in an otherwise unknown property and accidentally appear later.
108
+ publicValue(input);
109
+
110
+ const txHash = supplied(input, "txHash", "transactionHash");
111
+ const phase = supplied(input, "phase", "stage", "resultType") ?? (txHash == null ? "prepare" : "execute");
112
+ if (phase !== "prepare" && phase !== "execute") {
113
+ throw new TypeError('action receipt: phase must be "prepare" or "execute"');
114
+ }
115
+
116
+ const fields = {
117
+ receiptVersion: ACTION_RECEIPT_VERSION,
118
+ phase,
119
+ intent: required(input, ["intent"], "intent"),
120
+ route: required(input, ["route"], "route"),
121
+ decodedAction: required(input, ["decodedAction", "action"], "decodedAction"),
122
+ policyChecks: required(input, ["policyChecks", "checks"], "policyChecks"),
123
+ allowlistHits: required(input, ["allowlistHits", "allowlistMatches"], "allowlistHits"),
124
+ prepareHash: required(input, ["prepareHash"], "prepareHash"),
125
+ ...(txHash == null ? {} : { txHash }),
126
+ ...balanceFields(input),
127
+ };
128
+ const receipt = publicValue(fields);
129
+ return Object.freeze({ ...receipt, receiptId: computeReceiptId(receipt) });
130
+ }
131
+
132
+ export const createActionReceipt = normalizeActionReceipt;
133
+
134
+ function label(value, preferred = []) {
135
+ if (typeof value === "string" || typeof value === "number") return String(value);
136
+ for (const key of preferred) if (value?.[key] != null) return String(value[key]);
137
+ return "recorded";
138
+ }
139
+
140
+ function checkResult(check) {
141
+ if (typeof check === "boolean") return check;
142
+ if (check && typeof check === "object") {
143
+ if (typeof check.ok === "boolean") return check.ok;
144
+ if (typeof check.allowed === "boolean") return check.allowed;
145
+ if (typeof check.passed === "boolean") return check.passed;
146
+ }
147
+ return null;
148
+ }
149
+
150
+ /** Produce a compact human-readable statement without changing the receipt. */
151
+ export function summarizeActionReceipt(receipt) {
152
+ if (!receipt || typeof receipt !== "object") throw new TypeError("action receipt: object required");
153
+ const action = label(receipt.decodedAction, ["type", "action", "method", "name"]);
154
+ const route = label(receipt.route, ["name", "provider", "venue", "id"]);
155
+ const checks = Array.isArray(receipt.policyChecks)
156
+ ? receipt.policyChecks
157
+ : Object.values(receipt.policyChecks || {});
158
+ const passed = checks.filter((check) => checkResult(check) === true).length;
159
+ const failed = checks.filter((check) => checkResult(check) === false).length;
160
+ const hits = Array.isArray(receipt.allowlistHits)
161
+ ? receipt.allowlistHits.length
162
+ : Object.keys(receipt.allowlistHits || {}).length;
163
+ const tx = receipt.txHash == null ? "no transaction hash" : `tx ${receipt.txHash}`;
164
+ const balances = receipt.balances == null ? "balances not provided" : "before/after balances recorded";
165
+ return `${receipt.phase} ${action} via ${route}; policy ${passed} passed, ${failed} failed; ${hits} allowlist hit${hits === 1 ? "" : "s"}; ${tx}; ${balances}; receipt ${receipt.receiptId}`;
166
+ }
167
+
168
+ export const formatActionReceipt = summarizeActionReceipt;
@@ -14,6 +14,47 @@ const FORBIDDEN_KEYS = new Set([
14
14
  "privatekey", "private_key", "secret", "seed", "mnemonic", "passphrase", "signature", "keystore",
15
15
  ]);
16
16
 
17
+ // Exact-name matching missed obvious aliases (secretKey, seedPhrase, wif,
18
+ // keyMaterial, xprv...). Match on a normalized key instead: strip separators,
19
+ // lowercase, then look for any secret-ish token as a substring. A label store
20
+ // has no legitimate field containing these words.
21
+ const FORBIDDEN_TOKENS = [
22
+ "privatekey", "privkey", "secretkey", "secret", "seedphrase", "seed", "mnemonic",
23
+ "passphrase", "password", "keystore", "keymaterial", "signature", "wif",
24
+ "xprv", "xpriv", "apikey", "bearer", "credential",
25
+ ];
26
+
27
+ // Key-shaped values must not be persisted even in a free-text field. WIF, xprv,
28
+ // and BIP39 shapes are unambiguous. A bare 64-hex string is NOT — a raw EVM
29
+ // private key and a bytes32 tx hash are byte-identical — so that rule matches
30
+ // only when the WHOLE field is the hex blob. "paid via tx 0xabc..." is a note,
31
+ // not a leaked key.
32
+ const KEY_SHAPED_VALUE = [
33
+ /\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\b/,
34
+ /\bxprv[0-9A-Za-z]{50,}\b/,
35
+ /\b(?:[a-z]{3,8}\s+){11,}[a-z]{3,8}\b/i,
36
+ ];
37
+
38
+ const BARE_32_BYTE_HEX = /^\s*(?:0x)?[0-9a-fA-F]{64}\s*$/;
39
+
40
+ function normalizeKeyName(key) {
41
+ return String(key).toLowerCase().replace(/[^a-z0-9]/g, "");
42
+ }
43
+
44
+ function assertNoSecretKeyName(key, path) {
45
+ const normalized = normalizeKeyName(key);
46
+ if (FORBIDDEN_KEYS.has(String(key).toLowerCase()) || FORBIDDEN_TOKENS.some((t) => normalized.includes(t))) {
47
+ throw new Error(`address-book: ${path} is forbidden; the book stores labels, never key material`);
48
+ }
49
+ }
50
+
51
+ function assertNoSecretValue(value, path) {
52
+ if (typeof value !== "string") return;
53
+ if (KEY_SHAPED_VALUE.some((re) => re.test(value)) || BARE_32_BYTE_HEX.test(value)) {
54
+ throw new Error(`address-book: ${path} looks like key material; the book stores labels, never key material`);
55
+ }
56
+ }
57
+
17
58
  export function addressBookPath() {
18
59
  const override = env("ORACLE_ADDRESS_BOOK", "MAD_ADDRESS_BOOK", "");
19
60
  if (override) return path.resolve(override);
@@ -21,11 +62,18 @@ export function addressBookPath() {
21
62
  return path.join(dir, "address-book.json");
22
63
  }
23
64
 
24
- function assertNoSecrets(input) {
25
- for (const key of Object.keys(input || {})) {
26
- if (FORBIDDEN_KEYS.has(key.toLowerCase())) {
27
- throw new Error(`address-book: ${key} is forbidden; the book stores labels, never key material`);
28
- }
65
+ function assertNoSecrets(input, path = "field") {
66
+ if (Array.isArray(input)) {
67
+ input.forEach((item, i) => assertNoSecrets(item, `${path}[${i}]`));
68
+ return;
69
+ }
70
+ if (!input || typeof input !== "object") {
71
+ assertNoSecretValue(input, path);
72
+ return;
73
+ }
74
+ for (const [key, value] of Object.entries(input)) {
75
+ assertNoSecretKeyName(key, key);
76
+ assertNoSecrets(value, key);
29
77
  }
30
78
  }
31
79
 
@@ -0,0 +1,9 @@
1
+ export default {
2
+ name: "credential",
3
+ summary: "OS credential store glue",
4
+ group: "sign",
5
+ usage: "oracle credential <get|set|delete> ...",
6
+ async run(ctx) {
7
+ return ctx.dispatchOperator("oracle-credential", ctx.argv, { appendHintOnError: true });
8
+ },
9
+ };
@@ -0,0 +1,18 @@
1
+ import path from "node:path";
2
+ import { spawnChild } from "../spawn-child.mjs";
3
+
4
+ export default {
5
+ name: "data-mcp",
6
+ summary: "start the read-only data MCP server",
7
+ group: "read",
8
+ usage: "oracle data-mcp",
9
+ async run(ctx) {
10
+ const bin = path.join(ctx.root, "bin", "oracle-data-mcp.mjs");
11
+ return spawnChild(
12
+ process.execPath,
13
+ [bin, ...ctx.argv],
14
+ { stdio: "inherit", env: process.env },
15
+ "oracle-data-mcp",
16
+ );
17
+ },
18
+ };
@@ -0,0 +1,73 @@
1
+ import path from "node:path";
2
+ import { parseArgs } from "node:util";
3
+ import { spawnChild } from "../spawn-child.mjs";
4
+
5
+ const DEFAULT_DATA_URL = "http://127.0.0.1:8787";
6
+
7
+ function usage() {
8
+ process.stderr.write(
9
+ "usage: oracle data <serve|call|catalog|health> [args]\n" +
10
+ " oracle data serve [--port N]\n" +
11
+ " oracle data call <provider> <op> [--args '<json>']\n" +
12
+ " oracle data catalog\n" +
13
+ " oracle data health\n",
14
+ );
15
+ }
16
+
17
+ async function dataFetch(pathname, { method = "GET", body } = {}) {
18
+ const base = (process.env.ORACLE_DATA_URL || DEFAULT_DATA_URL).replace(/\/$/, "");
19
+ const url = base + pathname;
20
+ try {
21
+ const res = await fetch(url, {
22
+ method,
23
+ headers: body ? { "content-type": "application/json" } : undefined,
24
+ body: body ? JSON.stringify(body) : undefined,
25
+ });
26
+ const text = await res.text();
27
+ process.stdout.write(text.endsWith("\n") ? text : text + "\n");
28
+ return res.ok ? 0 : 1;
29
+ } catch (err) {
30
+ if (err && (err.code === "ECONNREFUSED" || /fetch failed|ECONNREFUSED/i.test(String(err)))) {
31
+ process.stderr.write("oracle: data server not running — start it with 'oracle data serve'\n");
32
+ return 4;
33
+ }
34
+ process.stderr.write(`oracle data: ${err.message || err}\n`);
35
+ return 1;
36
+ }
37
+ }
38
+
39
+ export default {
40
+ name: "data", summary: "desk data plane", group: "read",
41
+ usage: "oracle data serve|call|catalog|health",
42
+ async run(ctx) {
43
+ const verb = ctx.argv[0];
44
+ if (!verb || verb === "--help" || verb === "-h") { usage(); return verb ? 0 : 1; }
45
+ if (verb === "serve") {
46
+ let port = null;
47
+ try {
48
+ const { values } = parseArgs({ args: ctx.argv.slice(1), options: { port: { type: "string" } }, allowPositionals: true, strict: false });
49
+ port = values.port || null;
50
+ } catch {}
51
+ const env = { ...process.env };
52
+ if (port) env.ORACLE_DATA_PORT = String(port);
53
+ const bin = path.join(ctx.root, "bin", "desk-server.mjs");
54
+ return spawnChild(process.execPath, [bin], { stdio: "inherit", env }, "desk-server");
55
+ }
56
+ if (verb === "health") return dataFetch("/");
57
+ if (verb === "catalog") return dataFetch("/data/catalog");
58
+ if (verb === "call") {
59
+ const provider = ctx.argv[1];
60
+ const op = ctx.argv[2];
61
+ if (!provider || !op) { usage(); return 1; }
62
+ let argsObj = {};
63
+ const i = ctx.argv.indexOf("--args");
64
+ if (i >= 0) {
65
+ try { argsObj = JSON.parse(ctx.argv[i + 1] || "{}"); }
66
+ catch (err) { process.stderr.write(`oracle data call: invalid --args JSON: ${err.message}\n`); return 1; }
67
+ }
68
+ return dataFetch("/data/call", { method: "POST", body: { provider, op, args: argsObj } });
69
+ }
70
+ usage();
71
+ return 1;
72
+ },
73
+ };