@oracle-agent/oracle 0.3.4 → 0.3.6
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 +33 -13
- package/SETUP.md +59 -1
- package/artifacts/specialist-packs/oracle-full-crypto.json +31 -9
- package/bin/oracle-data-mcp.mjs +317 -4
- package/bin/oracle-init.mjs +100 -27
- package/bin/oracle-upgrade.mjs +42 -0
- package/docs/profiles.md +29 -7
- package/package.json +4 -1
- package/plugins/oracle-owner-gate/__init__.py +213 -0
- package/plugins/oracle-owner-gate/plugin.yaml +9 -0
- package/profiles/_template/SOUL.md +8 -1
- package/profiles/oracle/SOUL.md +17 -2
- package/profiles/oracle/profile.json +6 -2
- package/profiles/protocol-builder/SOUL.md +13 -6
- package/profiles/protocol-builder/profile.json +3 -1
- package/profiles/robinhood-agent/SOUL.md +9 -3
- package/profiles/robinhood-agent/profile.json +1 -0
- package/skills/balance/SKILL.md +176 -0
- package/skills/oracle-action-semantics/SKILL.md +40 -0
- package/skills/oracle-multichain-nft-launch/SKILL.md +338 -0
- package/skills/oracle-multichain-token-launch/SKILL.md +300 -0
- package/src/action-semantics.mjs +62 -0
- package/src/data/catalog.mjs +27 -3
- package/src/data/desk-data.mjs +34 -4
- package/src/data/providers/magiceden-sol.mjs +21 -2
- package/src/data/providers/nft-gallery.mjs +163 -0
- package/src/data/providers/nft-portfolio.mjs +494 -0
- package/src/data/providers/opensea-nft.mjs +272 -0
- package/src/data/providers/portfolio-history.mjs +394 -0
- package/src/data/providers/portfolio.mjs +594 -0
- package/src/data/providers/satflow.mjs +1 -0
- package/src/exec-policy.mjs +5 -0
- package/src/gmx-attestation.mjs +1 -0
- package/src/index.mjs +9 -0
- package/src/profile-upgrade.mjs +277 -0
- package/src/scanner/chains.config.mjs +50 -1
- package/src/vault-attestation.mjs +1 -0
package/bin/oracle-init.mjs
CHANGED
|
@@ -176,6 +176,71 @@ function record(kind, detail) {
|
|
|
176
176
|
log(`${APPLY ? " ✔" : " ·"} ${kind}: ${detail}`);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// Write Hermes mcp_servers.oracle-data directly into the profile config.
|
|
180
|
+
// Prefer this over `hermes mcp add`: current Hermes takes --command and --args
|
|
181
|
+
// as separate tokens, prompts interactively for tool enablement, and the old
|
|
182
|
+
// installer form `--command "node <path>"` silently no-ops under execFileSync.
|
|
183
|
+
function yamlScalar(value) {
|
|
184
|
+
if (
|
|
185
|
+
value === "" ||
|
|
186
|
+
/[:#\[\]{},&*!|>'"%@`\s]/.test(value) ||
|
|
187
|
+
/^(?:null|true|false|\d+)$/i.test(value)
|
|
188
|
+
) {
|
|
189
|
+
return JSON.stringify(value);
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function mcpServerBlock(serverName, scriptPath) {
|
|
195
|
+
return [
|
|
196
|
+
` ${serverName}:`,
|
|
197
|
+
" command: node",
|
|
198
|
+
" args:",
|
|
199
|
+
` - ${yamlScalar(scriptPath)}`,
|
|
200
|
+
" enabled: true",
|
|
201
|
+
].join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function wireMcpIntoConfig(configPath, serverName, scriptPath) {
|
|
205
|
+
const entry = mcpServerBlock(serverName, scriptPath);
|
|
206
|
+
if (!fs.existsSync(configPath)) {
|
|
207
|
+
fs.writeFileSync(configPath, `mcp_servers:\n${entry}\n`, "utf8");
|
|
208
|
+
return "created";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let txt = fs.readFileSync(configPath, "utf8");
|
|
212
|
+
if (
|
|
213
|
+
txt.includes(scriptPath) &&
|
|
214
|
+
new RegExp(`^\\s*${serverName}:\\s*$`, "m").test(txt)
|
|
215
|
+
) {
|
|
216
|
+
return "present";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Replace an existing server block of the same name (simple indented map).
|
|
220
|
+
const serverRe = new RegExp(
|
|
221
|
+
`^([ \\t]*)${serverName}:\\s*\\n(?:\\1[ \\t]+.*\\n)*`,
|
|
222
|
+
"m",
|
|
223
|
+
);
|
|
224
|
+
if (serverRe.test(txt)) {
|
|
225
|
+
txt = txt.replace(serverRe, `${entry}\n`);
|
|
226
|
+
fs.writeFileSync(configPath, txt, "utf8");
|
|
227
|
+
return "updated";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (/^mcp_servers:\s*$/m.test(txt) || /^mcp_servers:\s*\n/m.test(txt)) {
|
|
231
|
+
txt = txt.replace(/^(mcp_servers:\s*\n)/m, `$1${entry}\n`);
|
|
232
|
+
} else if (/^_config_version:.*$/m.test(txt)) {
|
|
233
|
+
txt = txt.replace(
|
|
234
|
+
/^(_config_version:.*\n)/m,
|
|
235
|
+
`$1mcp_servers:\n${entry}\n`,
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
txt = `mcp_servers:\n${entry}\n${txt}`;
|
|
239
|
+
}
|
|
240
|
+
fs.writeFileSync(configPath, txt, "utf8");
|
|
241
|
+
return "wired";
|
|
242
|
+
}
|
|
243
|
+
|
|
179
244
|
// ---------------------------------------------------------------- main
|
|
180
245
|
|
|
181
246
|
const schema = loadSchema();
|
|
@@ -363,34 +428,39 @@ for (const def of profiles) {
|
|
|
363
428
|
}
|
|
364
429
|
}
|
|
365
430
|
|
|
366
|
-
// MCP:
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
431
|
+
// MCP: write Hermes config directly. CLI is optional convenience only.
|
|
432
|
+
for (const m of def.mcp || []) {
|
|
433
|
+
const scriptPath = path.join(ROOT, "bin", "oracle-data-mcp.mjs");
|
|
434
|
+
const configPath = path.join(laneDir, "config.yaml");
|
|
435
|
+
const manual = `hermes -p ${def.id} mcp add ${m} --command node --args ${scriptPath}`;
|
|
436
|
+
|
|
437
|
+
if (m !== "oracle-data") {
|
|
438
|
+
record("mcp manual", manual);
|
|
439
|
+
mcpManual.push({ lane: def.id, server: m, command: manual });
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
record(
|
|
444
|
+
"wire mcp",
|
|
445
|
+
`${m} (${def.id}) -> ${path.relative(os.homedir(), configPath)}`,
|
|
446
|
+
);
|
|
447
|
+
if (APPLY) {
|
|
448
|
+
try {
|
|
449
|
+
fs.mkdirSync(laneDir, { recursive: true });
|
|
450
|
+
const how = wireMcpIntoConfig(configPath, m, scriptPath);
|
|
451
|
+
record(
|
|
452
|
+
"mcp config",
|
|
453
|
+
`${how}: ${path.relative(os.homedir(), configPath)}`,
|
|
454
|
+
);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
record("mcp note", `could not write ${configPath}: ${err.message}`);
|
|
457
|
+
mcpManual.push({ lane: def.id, server: m, command: manual });
|
|
458
|
+
}
|
|
388
459
|
}
|
|
389
460
|
}
|
|
390
|
-
}
|
|
391
461
|
|
|
392
|
-
|
|
393
|
-
}
|
|
462
|
+
record("posture", `DISARMED (${(def.posture.grantActions || []).join(", ")})`);
|
|
463
|
+
}
|
|
394
464
|
|
|
395
465
|
const summary = {
|
|
396
466
|
ok: true,
|
|
@@ -414,10 +484,13 @@ if (JSON_OUT) {
|
|
|
414
484
|
log("pass --force to overwrite (a timestamped .bak is written first).");
|
|
415
485
|
}
|
|
416
486
|
if (mcpManual.length) {
|
|
417
|
-
log("\nMCP wiring
|
|
487
|
+
log("\nMCP wiring needed a manual follow-up:");
|
|
418
488
|
for (const m of mcpManual) log(` ${m.command}`);
|
|
419
489
|
}
|
|
420
490
|
if (!APPLY) log("re-run with --apply to make changes.");
|
|
421
|
-
log("\nEvery lane is DISARMED.
|
|
491
|
+
log("\nEvery lane is DISARMED. Start the read plane, then chat a lane:");
|
|
492
|
+
log(" npx oracle-data # 127.0.0.1:8787 — MCP tools need this");
|
|
493
|
+
log(" hermes -p oracle chat");
|
|
494
|
+
log("\nSet each lane's model in:");
|
|
422
495
|
log(` ${path.join(hermesRoot(), "profiles", "<lane>", "config.yaml")}`);
|
|
423
496
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { upgradeProfiles } from "../src/profile-upgrade.mjs";
|
|
6
|
+
|
|
7
|
+
const argv = process.argv.slice(2);
|
|
8
|
+
const value = (flag) => {
|
|
9
|
+
const index = argv.indexOf(flag);
|
|
10
|
+
if (index < 0) return null;
|
|
11
|
+
if (!argv[index + 1] || argv[index + 1].startsWith("--")) throw new Error(`${flag} requires a value`);
|
|
12
|
+
return argv[index + 1];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const known = new Set(["--apply", "--json", "--only", "--hermes-home", "--package-root", "--control-command"]);
|
|
17
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
18
|
+
if (!known.has(argv[i])) throw new Error(`unknown option: ${argv[i]}`);
|
|
19
|
+
if (["--only", "--hermes-home", "--package-root", "--control-command"].includes(argv[i])) i += 1;
|
|
20
|
+
}
|
|
21
|
+
const packageRoot = path.resolve(value("--package-root") || path.join(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
22
|
+
const hermesHome = path.resolve(value("--hermes-home") || process.env.HERMES_HOME || path.join(os.homedir(), ".hermes"));
|
|
23
|
+
const control = value("--control-command");
|
|
24
|
+
const result = upgradeProfiles({
|
|
25
|
+
hermesHome,
|
|
26
|
+
packageRoot,
|
|
27
|
+
only: value("--only"),
|
|
28
|
+
apply: argv.includes("--apply"),
|
|
29
|
+
controlCommand: control ? control.trim().split(/\s+/) : null,
|
|
30
|
+
});
|
|
31
|
+
if (argv.includes("--json")) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
32
|
+
else {
|
|
33
|
+
console.log(`oracle-upgrade ${result.applied ? "applied" : "dry run"}: ${result.profiles.length} profile(s)`);
|
|
34
|
+
for (const key of ["created", "updated", "unchanged", "backups"]) console.log(`${key}: ${result[key].length}`);
|
|
35
|
+
if (!result.applied) console.log("Re-run with --apply to make these changes.");
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const failure = { ok: false, error: error.message };
|
|
39
|
+
if (argv.includes("--json")) process.stdout.write(`${JSON.stringify(failure, null, 2)}\n`);
|
|
40
|
+
else console.error(`oracle-upgrade: ${error.message}`);
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
package/docs/profiles.md
CHANGED
|
@@ -34,19 +34,30 @@ Three practical reasons, learned the hard way:
|
|
|
34
34
|
|
|
35
35
|
| Profile | Owns | Typical grant |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| `oracle` | routing, synthesis, multi-chain comparison | read + simulate only |
|
|
37
|
+
| `oracle` | routing, synthesis, multi-chain comparison, `/balance` portfolio aggregation | read + simulate only |
|
|
38
38
|
| `polymarket-agent` | prediction markets, event odds, CLOB cards/API-key order intents | read, quote, prepare |
|
|
39
39
|
| `hyperliquid-agent` | perps, spot, HIP-3 builder dexs, HIP-4 outcomes | read, quote, prepare |
|
|
40
40
|
| `robinhood-agent` | Robinhood Chain (4663) tokens, NFTs, tokenized Robinhood-style assets, capped NFT mints | read, quote, prepare |
|
|
41
41
|
| `solana-agent` | Solana swaps, research, Jupiter routes | read, quote, prepare |
|
|
42
42
|
| `bitcoin-agent` | Bitcoin L1, Ordinals/runes, inscriptions | read, prepare:inscription |
|
|
43
43
|
| `stable-agent` | Stable (988), USDT-native gas quirks | read, quote, prepare |
|
|
44
|
-
| `protocol-builder` | scaffold, review, prepare
|
|
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
|
-
`
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
48
|
+
|
|
49
|
+
`protocol-builder` classifies each launch by chain family, then designs and
|
|
50
|
+
prepares unsigned token, NFT collection, protocol, or mint-bot transactions. It
|
|
51
|
+
fails closed when no verified adapter exists, never house-signs, and keeps deploy,
|
|
52
|
+
metadata, liquidity, mint, and authority actions as separate user approvals.
|
|
53
|
+
|
|
54
|
+
The root `oracle` lane owns `/balance`, natural-language balance, and portfolio
|
|
55
|
+
history requests. Its `balance` skill calls the read-only
|
|
56
|
+
`portfolio_snapshot` MCP tool once, records a compact profile-local observation,
|
|
57
|
+
reports partial coverage and unavailable providers, and labels `knownUsd` as
|
|
58
|
+
incomplete instead of inventing a full portfolio total. `portfolio_history`
|
|
59
|
+
reads those observations and `portfolio_value_graph` renders the known-value
|
|
60
|
+
series while omitting unavailable values rather than plotting fake zeroes.
|
|
50
61
|
|
|
51
62
|
## Model choice is yours
|
|
52
63
|
|
|
@@ -101,10 +112,21 @@ hermes profile create polymarket-agent
|
|
|
101
112
|
```
|
|
102
113
|
|
|
103
114
|
Then give it a `SOUL.md` (who it is, what it owns, what it must refuse) and a
|
|
104
|
-
`config.yaml` (model + provider). Point it at Oracle's MCP read plane
|
|
115
|
+
`config.yaml` (model + provider). Point it at Oracle's MCP read plane.
|
|
116
|
+
|
|
117
|
+
`oracle-init --apply` writes this for you. Manual form (Hermes wants command and
|
|
118
|
+
args as separate tokens):
|
|
105
119
|
|
|
106
120
|
```bash
|
|
107
|
-
|
|
121
|
+
# terminal 1 — local read plane the MCP tools call
|
|
122
|
+
npx oracle-data
|
|
123
|
+
|
|
124
|
+
# terminal 2 — wire MCP into a lane
|
|
125
|
+
hermes -p polymarket-agent mcp add oracle-data \
|
|
126
|
+
--command node \
|
|
127
|
+
--args "$(node -p "require.resolve('@oracle-agent/oracle/package.json').replace(/package\\.json$/, 'bin/oracle-data-mcp.mjs')")"
|
|
128
|
+
# or after npm link / PATH has the bin:
|
|
129
|
+
# hermes -p polymarket-agent mcp add oracle-data --command oracle-data-mcp
|
|
108
130
|
```
|
|
109
131
|
|
|
110
132
|
Now that lane can read 30+ providers across 11 chains, quote real routes, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oracle-agent/oracle",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"oracle-public": "./bin/oracle-public-server.mjs",
|
|
31
31
|
"oracle-data-mcp": "./bin/oracle-data-mcp.mjs",
|
|
32
32
|
"oracle-init": "./bin/oracle-init.mjs",
|
|
33
|
+
"oracle-upgrade": "./bin/oracle-upgrade.mjs",
|
|
33
34
|
"oracle-scan": "./bin/oracle-scan.mjs",
|
|
34
35
|
"oracle-route": "./bin/oracle-route.mjs"
|
|
35
36
|
},
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
"./chains": "./src/chains.mjs",
|
|
41
42
|
"./scanner": "./src/scanner/index.mjs",
|
|
42
43
|
"./router": "./src/router/index.mjs",
|
|
44
|
+
"./action-semantics": "./src/action-semantics.mjs",
|
|
43
45
|
"./nft-gas-war": "./src/nft-gas-war-guard.mjs",
|
|
44
46
|
"./prepare-envelope": "./src/prepare-envelope.mjs"
|
|
45
47
|
},
|
|
@@ -76,6 +78,7 @@
|
|
|
76
78
|
"src/",
|
|
77
79
|
"profiles/",
|
|
78
80
|
"skills/",
|
|
81
|
+
"plugins/",
|
|
79
82
|
"public/",
|
|
80
83
|
"artifacts/",
|
|
81
84
|
"README.md",
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Hermes owner and raw-message intent gate for Oracle execution tools.
|
|
2
|
+
|
|
3
|
+
This is a routing guard, not a signer. Local Operator policy remains the
|
|
4
|
+
final authority for every signature and broadcast.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from collections import OrderedDict
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
_MAX_SESSIONS = 256
|
|
17
|
+
_TURNS: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
|
|
18
|
+
|
|
19
|
+
_PROTECTED = {
|
|
20
|
+
"oracle_watch_create": "alert_only",
|
|
21
|
+
"oracle_control_arm": "execute",
|
|
22
|
+
"oracle_control_confirm": "execute",
|
|
23
|
+
"oracle_action_cancel": "cancel",
|
|
24
|
+
"oracle_action_list": "owner_read",
|
|
25
|
+
"oracle_action_status": "owner_read",
|
|
26
|
+
"oracle_operator_sign": "sign",
|
|
27
|
+
"oracle_operator_send": "send",
|
|
28
|
+
"oracle_operator_execute": "send",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_LEADING_INTENT = re.compile(
|
|
32
|
+
r"^\s*(watch|ping|arm\s+confirm|arm|cancel|disarm|sign|send|execute)(?=$|[\s:,.!?;\-])",
|
|
33
|
+
re.IGNORECASE,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _config_path() -> Path:
|
|
38
|
+
override = os.environ.get("ORACLE_CONFIG_DIR")
|
|
39
|
+
root = Path(override).expanduser() if override else Path.home() / ".config" / "oracle"
|
|
40
|
+
return root / "owner.json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read_config() -> Optional[Dict[str, Any]]:
|
|
44
|
+
path = _config_path()
|
|
45
|
+
try:
|
|
46
|
+
if path.stat().st_mode & 0o077:
|
|
47
|
+
return None
|
|
48
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
49
|
+
except (OSError, ValueError, TypeError):
|
|
50
|
+
return None
|
|
51
|
+
return value if isinstance(value, dict) else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _strings(value: Any) -> set[str]:
|
|
55
|
+
if isinstance(value, str):
|
|
56
|
+
return {value.strip()} if value.strip() else set()
|
|
57
|
+
if isinstance(value, (list, tuple, set)):
|
|
58
|
+
return {str(item).strip() for item in value if str(item).strip()}
|
|
59
|
+
return set()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _owner_ids(config: Dict[str, Any], platform: str) -> set[str]:
|
|
63
|
+
"""Accept the public flat form plus convenient per-platform mappings."""
|
|
64
|
+
result = set()
|
|
65
|
+
canonical = config.get("owner")
|
|
66
|
+
if isinstance(canonical, str) and ":" in canonical:
|
|
67
|
+
owner_platform, owner_id = canonical.split(":", 1)
|
|
68
|
+
if owner_platform.strip().lower() == platform and owner_id.strip():
|
|
69
|
+
result.add(owner_id.strip())
|
|
70
|
+
for key in ("owner_ids", "ownerIds", "owners"):
|
|
71
|
+
value = config.get(key)
|
|
72
|
+
if isinstance(value, dict):
|
|
73
|
+
result |= _strings(value.get(platform))
|
|
74
|
+
result |= _strings(value.get("*"))
|
|
75
|
+
else:
|
|
76
|
+
result |= _strings(value)
|
|
77
|
+
platforms = config.get("platforms")
|
|
78
|
+
if isinstance(platforms, dict):
|
|
79
|
+
entry = platforms.get(platform)
|
|
80
|
+
if isinstance(entry, dict):
|
|
81
|
+
result |= _strings(entry.get("owner_ids"))
|
|
82
|
+
result |= _strings(entry.get("owners"))
|
|
83
|
+
else:
|
|
84
|
+
result |= _strings(entry)
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _local_cli_allowed(config: Dict[str, Any]) -> bool:
|
|
89
|
+
value = config.get("local_cli", config.get("localCli", config.get("allow_local_cli", False)))
|
|
90
|
+
if isinstance(value, dict):
|
|
91
|
+
value = value.get("allow", value.get("owner", value.get("enabled", False)))
|
|
92
|
+
return value is True or (isinstance(value, str) and value.lower() in {"allow", "owner", "enabled", "true"})
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _intent(raw_message: Any) -> str:
|
|
96
|
+
if not isinstance(raw_message, str):
|
|
97
|
+
return "none"
|
|
98
|
+
match = _LEADING_INTENT.match(raw_message)
|
|
99
|
+
if not match:
|
|
100
|
+
return "none"
|
|
101
|
+
command = " ".join(match.group(1).lower().split())
|
|
102
|
+
if command in {"watch", "ping"}:
|
|
103
|
+
return "alert_only"
|
|
104
|
+
if command in {"arm", "arm confirm"}:
|
|
105
|
+
return "execute"
|
|
106
|
+
if command in {"cancel", "disarm"}:
|
|
107
|
+
return "cancel"
|
|
108
|
+
if command == "sign":
|
|
109
|
+
return "sign"
|
|
110
|
+
return "send"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _tool_class(tool_name: Any) -> Optional[str]:
|
|
114
|
+
# MCP adapters use several separators. Collapsing all non-alphanumerics
|
|
115
|
+
# lets us compare the authoritative suffix without trusting the prefix.
|
|
116
|
+
normalized = re.sub(r"[^a-z0-9]+", "_", str(tool_name or "").lower()).strip("_")
|
|
117
|
+
for suffix, tool_class in _PROTECTED.items():
|
|
118
|
+
if normalized == suffix or normalized.endswith("_" + suffix):
|
|
119
|
+
return tool_class
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _identity(config: Optional[Dict[str, Any]], platform: str, sender_id: str) -> Tuple[bool, str]:
|
|
124
|
+
if config is None:
|
|
125
|
+
return False, "owner configuration is unavailable"
|
|
126
|
+
platform = platform.strip().lower()
|
|
127
|
+
sender_id = sender_id.strip()
|
|
128
|
+
if platform in {"cli", "local", "terminal"}:
|
|
129
|
+
if _local_cli_allowed(config):
|
|
130
|
+
return True, ""
|
|
131
|
+
return False, "local CLI is not authorized by owner policy"
|
|
132
|
+
if not sender_id:
|
|
133
|
+
return False, "gateway sender identity is missing"
|
|
134
|
+
if sender_id not in _owner_ids(config, platform):
|
|
135
|
+
return False, "sender is not an owner"
|
|
136
|
+
return True, ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _block(reason: str) -> Dict[str, str]:
|
|
140
|
+
return {"action": "block", "message": "Oracle owner gate: " + reason + "."}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _on_pre_llm_call(
|
|
144
|
+
session_id: str = "",
|
|
145
|
+
sender_id: str = "",
|
|
146
|
+
platform: str = "",
|
|
147
|
+
user_message: Any = "",
|
|
148
|
+
parent_session_id: str = "",
|
|
149
|
+
**_: Any,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Snapshot only Hermes-trusted turn metadata and the original user input."""
|
|
152
|
+
session_id = str(session_id or "").strip()
|
|
153
|
+
if not session_id:
|
|
154
|
+
return None
|
|
155
|
+
_TURNS[session_id] = {
|
|
156
|
+
"session_id": session_id,
|
|
157
|
+
"parent_session_id": str(parent_session_id or "").strip(),
|
|
158
|
+
"sender_id": str(sender_id or "").strip(),
|
|
159
|
+
"platform": str(platform or "").strip().lower(),
|
|
160
|
+
"raw_message": user_message if isinstance(user_message, str) else "",
|
|
161
|
+
"intent": _intent(user_message),
|
|
162
|
+
}
|
|
163
|
+
_TURNS.move_to_end(session_id)
|
|
164
|
+
while len(_TURNS) > _MAX_SESSIONS:
|
|
165
|
+
_TURNS.popitem(last=False)
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _on_pre_tool_call(tool_name: str = "", session_id: str = "", **_: Any) -> Optional[Dict[str, str]]:
|
|
170
|
+
required = _tool_class(tool_name)
|
|
171
|
+
if required is None:
|
|
172
|
+
return None
|
|
173
|
+
session_id = str(session_id or "").strip()
|
|
174
|
+
turn = _TURNS.get(session_id)
|
|
175
|
+
if turn is None:
|
|
176
|
+
return _block("no trusted current-turn owner message exists for this session")
|
|
177
|
+
if turn["parent_session_id"]:
|
|
178
|
+
return _block("delegated and background sessions cannot execute or sign")
|
|
179
|
+
allowed, reason = _identity(_read_config(), turn["platform"], turn["sender_id"])
|
|
180
|
+
if not allowed:
|
|
181
|
+
return _block(reason)
|
|
182
|
+
if required == "owner_read":
|
|
183
|
+
return None
|
|
184
|
+
intent = turn["intent"]
|
|
185
|
+
if intent != required:
|
|
186
|
+
labels = {
|
|
187
|
+
"alert_only": "watch or ping",
|
|
188
|
+
"execute": "arm",
|
|
189
|
+
"cancel": "cancel or disarm",
|
|
190
|
+
"sign": "sign",
|
|
191
|
+
"send": "send or execute",
|
|
192
|
+
}
|
|
193
|
+
return _block(f"raw owner intent does not authorize {required}; start the message with {labels[required]}")
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _on_session_end(session_id: str = "", **_: Any) -> None:
|
|
198
|
+
_TURNS.pop(str(session_id or "").strip(), None)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _on_session_reset(session_id: str = "", **_: Any) -> None:
|
|
202
|
+
session_id = str(session_id or "").strip()
|
|
203
|
+
if session_id:
|
|
204
|
+
_TURNS.pop(session_id, None)
|
|
205
|
+
else:
|
|
206
|
+
_TURNS.clear()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def register(ctx: Any) -> None:
|
|
210
|
+
ctx.register_hook("pre_llm_call", _on_pre_llm_call)
|
|
211
|
+
ctx.register_hook("pre_tool_call", _on_pre_tool_call)
|
|
212
|
+
ctx.register_hook("on_session_end", _on_session_end)
|
|
213
|
+
ctx.register_hook("on_session_reset", _on_session_reset)
|
|
@@ -38,8 +38,15 @@ Terse. Answer first, evidence second. State confidence: `high` / `moderate` /
|
|
|
38
38
|
## Wiring it up
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
+
# preferred: installer writes SOUL, skills, and MCP config
|
|
42
|
+
npx oracle-init --apply
|
|
43
|
+
|
|
44
|
+
# or by hand
|
|
41
45
|
hermes profile create my-lane
|
|
42
|
-
|
|
46
|
+
npx oracle-data # keep running — MCP tools call 127.0.0.1:8787
|
|
47
|
+
hermes -p my-lane mcp add oracle-data --command oracle-data-mcp
|
|
48
|
+
# if the bin is not on PATH:
|
|
49
|
+
# hermes -p my-lane mcp add oracle-data --command node --args /abs/path/to/oracle-data-mcp.mjs
|
|
43
50
|
```
|
|
44
51
|
|
|
45
52
|
Then copy this `SOUL.md` into `~/.hermes/profiles/my-lane/SOUL.md` and set the
|
package/profiles/oracle/SOUL.md
CHANGED
|
@@ -20,9 +20,11 @@ value yourself.
|
|
|
20
20
|
| Stable (988), USDT-native gas | `stable-agent` |
|
|
21
21
|
| tokenized Robinhood-style assets / stock tokens | exact home-chain lane + `oracle-rfq-tokenized-assets` |
|
|
22
22
|
| meme-token launches, sniping, liquidity/pool watches | token's home-chain lane + `oracle-meme-token-sniper` |
|
|
23
|
-
|
|
|
23
|
+
| create a fungible token or NFT collection | home-chain specialist for chain facts + `protocol-builder` using the matching multichain launch skill |
|
|
24
|
+
| deploy/review custom contracts, gacha, DEX, launchpad, or capped NFT mint bot | `protocol-builder` + `oracle-nft-mint-gas-war` |
|
|
24
25
|
| RFQ / solver-intent route comparison across chains | `oracle` + `oracle-rfq-tokenized-assets` |
|
|
25
26
|
| graph/card alert rendering | token's home-chain lane + `oracle-chain-graphs-telegram-cards` |
|
|
27
|
+
| `/balance`, balance, holdings, wallet portfolio | `oracle` + `balance`; one deterministic `portfolio_snapshot` read plus profile-local observation |
|
|
26
28
|
| compare chains, "which is cheaper" | you, using the data plane |
|
|
27
29
|
|
|
28
30
|
If the chain is ambiguous, resolve the token's home chain first (DexScreener via
|
|
@@ -36,7 +38,15 @@ the data plane). If it stays ambiguous, ask. Do not guess a chain.
|
|
|
36
38
|
margin call for different decisions. If `rankedOn` is `gross`, gas was NOT
|
|
37
39
|
accounted for — say so. See the `oracle-best-execution` skill.
|
|
38
40
|
|
|
39
|
-
1. **
|
|
41
|
+
1. **The public router never signs.** It prepares, simulates, and explains; ordinary
|
|
42
|
+
EVM artifacts require the user's wallet signature. Do not turn that public-plane
|
|
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
|
|
45
|
+
separately expose a same-host, owner-gated EVM executor. Verify that executor
|
|
46
|
+
before describing it as available.
|
|
47
|
+
`watch`, `watch this`, and `ping me` always mean `actionMode: alert_only`.
|
|
48
|
+
Only an explicit `arm` may mean `actionMode: execute`, and only for one exact,
|
|
49
|
+
bounded owner-authorized action.
|
|
40
50
|
2. **RFQ is a route source, not a permission bypass.** Compare solver/RFQ
|
|
41
51
|
quotes net of gas/spread where configured, enforce expiry, and keep exact
|
|
42
52
|
artifact kinds separate.
|
|
@@ -53,6 +63,11 @@ the data plane). If it stays ambiguous, ask. Do not guess a chain.
|
|
|
53
63
|
delta → the action did not succeed. Say so plainly.
|
|
54
64
|
7. **Never invent chain facts.** If it did not come from a live read, label it
|
|
55
65
|
`unknown`.
|
|
66
|
+
8. **Balance uses one source of truth.** `/balance` and plain-language balance
|
|
67
|
+
requests call `portfolio_snapshot`; use `portfolio_history` and
|
|
68
|
+
`portfolio_value_graph` for historical requests. Report `knownUsd` as
|
|
69
|
+
incomplete whenever a provider, address, price, token/NFT indexer, or chain
|
|
70
|
+
adapter is missing. Never turn an unavailable historical value into zero.
|
|
56
71
|
|
|
57
72
|
## Confidence
|
|
58
73
|
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
"label": "oracle",
|
|
5
5
|
"role": "router",
|
|
6
6
|
"color": "#7CC4FF",
|
|
7
|
-
"description": "Router for multichain trading, building, analysis, scanners, RFQ, tokenized-asset buys, NFT mint gas limits, meme-token sniping,
|
|
7
|
+
"description": "Router for multichain trading, building, analysis, wallet/NFT inventory and value history, scanners, RFQ, tokenized-asset buys, NFT mint gas limits, meme-token sniping, and chain-family token/NFT launches.",
|
|
8
8
|
"model": {
|
|
9
9
|
"note": "Wants the strongest reasoner available: routing and synthesis are judgment calls.",
|
|
10
10
|
"suggested": "strong-reasoner"
|
|
11
11
|
},
|
|
12
12
|
"skills": [
|
|
13
13
|
"oracle-desk",
|
|
14
|
+
"oracle-action-semantics",
|
|
14
15
|
"oracle-grants",
|
|
15
16
|
"oracle-receipts",
|
|
16
17
|
"oracle-best-execution",
|
|
@@ -18,7 +19,10 @@
|
|
|
18
19
|
"oracle-meme-token-sniper",
|
|
19
20
|
"oracle-chain-graphs-telegram-cards",
|
|
20
21
|
"oracle-rfq-tokenized-assets",
|
|
21
|
-
"oracle-nft-mint-gas-war"
|
|
22
|
+
"oracle-nft-mint-gas-war",
|
|
23
|
+
"oracle-multichain-token-launch",
|
|
24
|
+
"oracle-multichain-nft-launch",
|
|
25
|
+
"balance"
|
|
22
26
|
],
|
|
23
27
|
"mcp": [
|
|
24
28
|
"oracle-data"
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# protocol builder
|
|
2
2
|
|
|
3
|
-
You design, review, and prepare deploys for
|
|
4
|
-
|
|
3
|
+
You classify by chain family, then design, review, and prepare deploys for fungible
|
|
4
|
+
tokens, NFT collections, protocols, gacha products, DEX surfaces, launchpads, mint
|
|
5
|
+
pages, and scanner-backed on-chain apps. You never sign one. Unsupported chain
|
|
6
|
+
adapters fail closed.
|
|
5
7
|
|
|
6
8
|
## What you own
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
Chain-family token/NFT launch manifests, contract and program scaffolding,
|
|
11
|
+
NFT/gacha mint mechanics, DEX/pool/launchpad design, security review, deploy and
|
|
12
|
+
verify scripts, and **unsigned** deploy transactions. Research of existing
|
|
13
|
+
protocols before cloning them.
|
|
11
14
|
|
|
12
15
|
## Deployment is permanent
|
|
13
16
|
|
|
@@ -42,7 +45,11 @@ what an attacker gains from each privileged function.
|
|
|
42
45
|
3. **Simulate before preparing.** An unsimulated deploy is a guess.
|
|
43
46
|
4. **State the authority model before the code.** A user who doesn't know who owns
|
|
44
47
|
the contract cannot consent to deploying it.
|
|
45
|
-
5. **Receipts or it didn't happen
|
|
48
|
+
5. **Receipts or it didn't happen.** Deployed address, receipt, verified source.
|
|
49
|
+
6. **Chain-family support is explicit.** Use `TEMPLATE_READY`, `ADAPTER_READY`,
|
|
50
|
+
`GUIDED_BUILD`, `RESEARCH_ONLY`, or `UNSUPPORTED`. RPC reachability is not deploy support.
|
|
51
|
+
7. **One approval per side effect.** Deploy, metadata upload, mint, liquidity,
|
|
52
|
+
authority transfer/revoke, reveal, and verification remain separate.
|
|
46
53
|
|
|
47
54
|
## Voice
|
|
48
55
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"label": "protocol builder",
|
|
5
5
|
"role": "builder",
|
|
6
6
|
"color": "#ff8c5a",
|
|
7
|
-
"description": "Builder lane for protocol, NFT
|
|
7
|
+
"description": "Builder lane for protocol, chain-family token/NFT collections, gacha, DEX, scanner, and mint-bot surfaces with unsigned deploy/mint preparation.",
|
|
8
8
|
"model": {
|
|
9
9
|
"note": "Contract review is unforgiving and mistakes are permanent; wants the strongest reasoner available.",
|
|
10
10
|
"suggested": "strong-reasoner"
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"oracle-protocol-builder",
|
|
14
14
|
"oracle-contract-research",
|
|
15
15
|
"oracle-protocol-security",
|
|
16
|
+
"oracle-multichain-token-launch",
|
|
17
|
+
"oracle-multichain-nft-launch",
|
|
16
18
|
"oracle-nft-gacha-launch",
|
|
17
19
|
"oracle-dex-launch",
|
|
18
20
|
"oracle-receipts",
|