@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 +13 -8
- package/SETUP.md +2 -2
- package/bin/oracle-data-mcp.mjs +3 -2
- package/docs/oracle-pack-standard.md +29 -0
- package/docs/profiles.md +1 -1
- package/examples/oracle-pack-template.mjs +38 -0
- package/package.json +7 -2
- package/profiles/oracle/SOUL.md +3 -1
- package/public/agent-connect/app.js +81 -0
- package/public/agent-connect/index.html +83 -0
- package/public/agent-connect/styles.css +70 -0
- package/scripts/adversarial-bench.mjs +114 -0
- package/skills/oracle-action-semantics/SKILL.md +1 -1
- package/src/action-receipts.mjs +168 -0
- package/src/address-book.mjs +53 -5
- package/src/data/http.mjs +69 -16
- package/src/data/providers/magiceden-sol.mjs +23 -5
- package/src/exec-policy.mjs +6 -0
- package/src/index.mjs +24 -0
- package/src/onboarding/harness-configs.mjs +102 -0
- package/src/oracle-env.mjs +11 -2
- package/src/portfolio-risk.mjs +170 -0
- package/src/signals/engine.mjs +146 -0
- package/src/signals/index.mjs +1 -0
- package/src/watch-preferences.mjs +85 -0
|
@@ -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;
|
package/src/address-book.mjs
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
package/src/data/http.mjs
CHANGED
|
@@ -37,6 +37,35 @@ const IDENTITY_HEADERS = [
|
|
|
37
37
|
"proxy-authorization",
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
+
const REDIRECT_STATUS = new Set([301, 302, 303, 307, 308]);
|
|
41
|
+
const MAX_REDIRECTS = 3;
|
|
42
|
+
|
|
43
|
+
function credentialHeaderName(name) {
|
|
44
|
+
const h = String(name || "").toLowerCase();
|
|
45
|
+
return IDENTITY_HEADERS.includes(h) || /(^|-)(api[-_]?key|key|token|secret|auth|authorization|session|cookie|signature|access)(-|$)/.test(h);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stripCredentialHeaders(headers) {
|
|
49
|
+
const safe = {};
|
|
50
|
+
for (const [name, value] of Object.entries(headers || {})) {
|
|
51
|
+
if (!credentialHeaderName(name)) safe[name] = value;
|
|
52
|
+
}
|
|
53
|
+
return safe;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function redirectedUrl(location, currentUrl) {
|
|
57
|
+
if (!location) throw new Error(`HTTP redirect from ${currentUrl} missing Location`);
|
|
58
|
+
const next = new URL(String(location), String(currentUrl));
|
|
59
|
+
if (next.protocol !== "http:" && next.protocol !== "https:") {
|
|
60
|
+
throw new Error(`HTTP redirect from ${currentUrl} used unsupported protocol ${next.protocol}`);
|
|
61
|
+
}
|
|
62
|
+
return next.toString();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sameOrigin(a, b) {
|
|
66
|
+
return new URL(String(a)).origin === new URL(String(b)).origin;
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
const inflight = new Map();
|
|
41
70
|
|
|
42
71
|
/**
|
|
@@ -77,24 +106,48 @@ async function once(url, { fetchImpl, method, headers, body, timeoutMs }) {
|
|
|
77
106
|
const ac = new AbortController();
|
|
78
107
|
const t = setTimeout(() => ac.abort(), timeoutMs);
|
|
79
108
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
109
|
+
let currentUrl = String(url);
|
|
110
|
+
let currentHeaders = { ...(headers || {}) };
|
|
111
|
+
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects += 1) {
|
|
112
|
+
const res = await fetchImpl(currentUrl, {
|
|
113
|
+
method,
|
|
114
|
+
headers: currentHeaders,
|
|
115
|
+
body,
|
|
116
|
+
signal: ac.signal,
|
|
117
|
+
redirect: "manual",
|
|
118
|
+
});
|
|
119
|
+
const text = await res.text();
|
|
120
|
+
let json = null;
|
|
121
|
+
if (text) {
|
|
122
|
+
try {
|
|
123
|
+
json = JSON.parse(text);
|
|
124
|
+
} catch {
|
|
125
|
+
json = null;
|
|
126
|
+
}
|
|
88
127
|
}
|
|
128
|
+
if (REDIRECT_STATUS.has(res.status)) {
|
|
129
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
130
|
+
const err = new Error(`HTTP ${res.status} ${method} ${currentUrl} redirected non-idempotent request`);
|
|
131
|
+
err.status = res.status;
|
|
132
|
+
err.body = json ?? text.slice(0, 300);
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
if (redirects === MAX_REDIRECTS) throw new Error(`HTTP redirect loop for ${url}`);
|
|
136
|
+
const nextUrl = redirectedUrl(res.headers?.get?.("location"), currentUrl);
|
|
137
|
+
if (!sameOrigin(nextUrl, currentUrl)) currentHeaders = stripCredentialHeaders(currentHeaders);
|
|
138
|
+
currentUrl = nextUrl;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
const err = new Error(`HTTP ${res.status} ${method} ${currentUrl}`);
|
|
143
|
+
err.status = res.status;
|
|
144
|
+
err.retryAfter = res.headers?.get?.("retry-after") ?? null;
|
|
145
|
+
err.body = json ?? text.slice(0, 300);
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
return json ?? text;
|
|
89
149
|
}
|
|
90
|
-
|
|
91
|
-
const err = new Error(`HTTP ${res.status} ${method} ${url}`);
|
|
92
|
-
err.status = res.status;
|
|
93
|
-
err.retryAfter = res.headers?.get?.("retry-after") ?? null;
|
|
94
|
-
err.body = json ?? text.slice(0, 300);
|
|
95
|
-
throw err;
|
|
96
|
-
}
|
|
97
|
-
return json ?? text;
|
|
150
|
+
throw new Error(`HTTP redirect loop for ${url}`);
|
|
98
151
|
} finally {
|
|
99
152
|
clearTimeout(t);
|
|
100
153
|
}
|
|
@@ -90,17 +90,33 @@ function lamports(sol) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
function listingExpiry(value) {
|
|
93
|
-
if (value == null || value === "")
|
|
93
|
+
if (value == null || value === "") throw new Error("magiceden: listing expiry is required and must be in the future");
|
|
94
94
|
const expiry = Number(value);
|
|
95
|
-
if (!Number.isSafeInteger(expiry) || expiry
|
|
96
|
-
throw new Error("magiceden: expiry must be a
|
|
95
|
+
if (!Number.isSafeInteger(expiry) || expiry <= 0) {
|
|
96
|
+
throw new Error("magiceden: expiry must be a future Unix timestamp in seconds");
|
|
97
97
|
}
|
|
98
|
-
if (expiry
|
|
99
|
-
throw new Error("magiceden: expiry must be in the future
|
|
98
|
+
if (expiry <= Math.floor(Date.now() / 1000)) {
|
|
99
|
+
throw new Error("magiceden: expiry must be in the future");
|
|
100
100
|
}
|
|
101
101
|
return expiry;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function listConfirmation({ tokenMint, priceSol, expiry }) {
|
|
105
|
+
return `list ${tokenMint} on magiceden-sol for ${priceSol} SOL until ${expiry}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function requireListingConfirmation(args, fields) {
|
|
109
|
+
if (args.userConfirmed !== true) {
|
|
110
|
+
throw new Error("magiceden: listing requires userConfirmed=true after reviewing tokenMint, priceSol, and expiry");
|
|
111
|
+
}
|
|
112
|
+
const expected = listConfirmation(fields);
|
|
113
|
+
const actual = String(args.confirmation || args.confirmationPhrase || "").trim();
|
|
114
|
+
if (actual !== expected) {
|
|
115
|
+
throw new Error(`magiceden: confirmation must equal ${JSON.stringify(expected)}`);
|
|
116
|
+
}
|
|
117
|
+
return expected;
|
|
118
|
+
}
|
|
119
|
+
|
|
104
120
|
export async function magicEdenSolHealth(opts = {}) {
|
|
105
121
|
try {
|
|
106
122
|
const stats = await magicEdenSolStats({ symbol: "mad_lads" }, opts);
|
|
@@ -269,6 +285,7 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
|
|
|
269
285
|
const auctionHouse = solanaPubkey(args.auctionHouse, "auctionHouse");
|
|
270
286
|
const priceSol = positiveNumber(args.priceSol ?? args.price, "priceSol");
|
|
271
287
|
const expiry = listingExpiry(args.expiry);
|
|
288
|
+
const confirmation = requireListingConfirmation(args, { tokenMint, priceSol, expiry });
|
|
272
289
|
const url = new URL(`${base(opts)}/instructions/sell`);
|
|
273
290
|
url.searchParams.set("seller", seller);
|
|
274
291
|
url.searchParams.set("auctionHouseAddress", auctionHouse);
|
|
@@ -301,6 +318,7 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
|
|
|
301
318
|
tokenMint,
|
|
302
319
|
priceSol,
|
|
303
320
|
expiry,
|
|
321
|
+
confirmation,
|
|
304
322
|
transaction,
|
|
305
323
|
transactionEncoding: "base64",
|
|
306
324
|
raw,
|
package/src/exec-policy.mjs
CHANGED
|
@@ -419,6 +419,12 @@ export function enforceTxPolicy(tx = {}, phase = "broadcast") {
|
|
|
419
419
|
// Spending caps toggle. Set MAD_VALUE_CAPS_ENABLED=0 in exec.env to lift the
|
|
420
420
|
// per-tx + daily native caps (chain/dest allowlists still enforced). Flip back
|
|
421
421
|
// to 1 to re-arm the cap wall. Default = ON when unset.
|
|
422
|
+
//
|
|
423
|
+
// Deliberately strict: ONLY the literal "0" lifts the caps. This is the
|
|
424
|
+
// opposite polarity from a kill switch. Disabling a kill switch makes a
|
|
425
|
+
// deployment safer, so it accepts 0/false/off/no. Disabling THIS makes a
|
|
426
|
+
// deployment riskier, so a stray "false" must keep the cap wall standing
|
|
427
|
+
// rather than silently removing it. Do not route this through envEnabled.
|
|
422
428
|
const capsOn = String(env("ORACLE_VALUE_CAPS_ENABLED", "MAD_VALUE_CAPS_ENABLED", "1")).trim() !== "0";
|
|
423
429
|
if (!capsOn) return true;
|
|
424
430
|
|
package/src/index.mjs
CHANGED
|
@@ -67,3 +67,27 @@ export {
|
|
|
67
67
|
DEFAULT_SCOPES,
|
|
68
68
|
normalizeScopes,
|
|
69
69
|
} from "./scopes.mjs";
|
|
70
|
+
|
|
71
|
+
export { emitHarnessConfigs, createHarnessConfigs } from "./onboarding/harness-configs.mjs";
|
|
72
|
+
export {
|
|
73
|
+
ACTION_RECEIPT_VERSION,
|
|
74
|
+
ActionReceiptSecretError,
|
|
75
|
+
assertNoReceiptSecrets,
|
|
76
|
+
canonicalReceiptJson,
|
|
77
|
+
computeReceiptId,
|
|
78
|
+
normalizeActionReceipt,
|
|
79
|
+
createActionReceipt,
|
|
80
|
+
summarizeActionReceipt,
|
|
81
|
+
formatActionReceipt,
|
|
82
|
+
} from "./action-receipts.mjs";
|
|
83
|
+
export { DEFAULT_RISK_THRESHOLDS, summarizePortfolioRisk, portfolioRiskSummary } from "./portfolio-risk.mjs";
|
|
84
|
+
export {
|
|
85
|
+
WATCH_CATEGORIES,
|
|
86
|
+
defaultPreferences,
|
|
87
|
+
subscribe,
|
|
88
|
+
unsubscribe,
|
|
89
|
+
createWatch,
|
|
90
|
+
evaluateAlert,
|
|
91
|
+
shouldDeliverAlert,
|
|
92
|
+
} from "./watch-preferences.mjs";
|
|
93
|
+
export { SIGNAL_TYPES, scoreSignals, createSignalsEngine } from "./signals/index.mjs";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const PACKAGE = "@oracle-agent/oracle";
|
|
2
|
+
const COMMAND = "npx";
|
|
3
|
+
const ARGS = ["-y", "--package", PACKAGE, "oracle-data-mcp"];
|
|
4
|
+
const FORBIDDEN_NAME = /(?:^|_)(?:PRIVATE_KEY|SECRET|TOKEN|PASSWORD|PASSPHRASE|MNEMONIC|SEED)(?:_|$)/i;
|
|
5
|
+
|
|
6
|
+
function requiredText(value, name) {
|
|
7
|
+
if (typeof value !== "string" || !value.trim()) throw new TypeError(`${name} must be a non-empty string`);
|
|
8
|
+
if (/\r|\n|\0/.test(value)) throw new TypeError(`${name} must be a single-line string`);
|
|
9
|
+
return value.trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function validateUrl(value) {
|
|
13
|
+
const text = requiredText(value, "url");
|
|
14
|
+
let parsed;
|
|
15
|
+
try { parsed = new URL(text); } catch { throw new TypeError("url must be a valid http(s) URL"); }
|
|
16
|
+
if (!/^https?:$/.test(parsed.protocol) || parsed.username || parsed.password) {
|
|
17
|
+
throw new TypeError("url must be a credential-free http(s) URL");
|
|
18
|
+
}
|
|
19
|
+
for (const name of parsed.searchParams.keys()) {
|
|
20
|
+
if (FORBIDDEN_NAME.test(name) || /^(?:key|api[_-]?key|auth)$/i.test(name)) {
|
|
21
|
+
throw new TypeError(`url must not contain secret-shaped query parameter: ${name}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return parsed.href;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateKey(value) {
|
|
28
|
+
const text = requiredText(value, "key");
|
|
29
|
+
if (/^[A-Z][A-Z0-9_]*\s*=/.test(text) || /-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(text)) {
|
|
30
|
+
throw new TypeError("key must be an Oracle agent key, not an environment assignment or private key");
|
|
31
|
+
}
|
|
32
|
+
if (/^(?:0x)?[0-9a-f]{64}$/i.test(text) || text.trim().split(/\s+/).length >= 12) {
|
|
33
|
+
throw new TypeError("key looks like private signing material");
|
|
34
|
+
}
|
|
35
|
+
return text;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateLabel(value) {
|
|
39
|
+
const label = requiredText(value, "label");
|
|
40
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,62}$/i.test(label)) {
|
|
41
|
+
throw new TypeError("label must contain only letters, digits, underscores, and hyphens");
|
|
42
|
+
}
|
|
43
|
+
if (FORBIDDEN_NAME.test(label)) throw new TypeError("label must not be a secret-shaped environment name");
|
|
44
|
+
return label;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function json(value) {
|
|
48
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function yamlString(value) {
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tomlString(value) {
|
|
56
|
+
return JSON.stringify(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Return deterministic, copy-pasteable MCP configuration snippets.
|
|
61
|
+
* This function performs no I/O and never reads from process.env.
|
|
62
|
+
*/
|
|
63
|
+
export function emitHarnessConfigs({ url, key, label = "oracle-data" } = {}) {
|
|
64
|
+
const endpoint = validateUrl(url);
|
|
65
|
+
const agentKey = validateKey(key);
|
|
66
|
+
const name = validateLabel(label);
|
|
67
|
+
const server = {
|
|
68
|
+
command: COMMAND,
|
|
69
|
+
args: [...ARGS],
|
|
70
|
+
env: { ORACLE_DATA_URL: endpoint, ORACLE_AGENT_KEY: agentKey },
|
|
71
|
+
};
|
|
72
|
+
const mcp = { mcpServers: { [name]: server } };
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
hermes: [
|
|
76
|
+
"mcp_servers:",
|
|
77
|
+
` ${name}:`,
|
|
78
|
+
` command: ${yamlString(COMMAND)}`,
|
|
79
|
+
" args:",
|
|
80
|
+
...ARGS.map((arg) => ` - ${yamlString(arg)}`),
|
|
81
|
+
" env:",
|
|
82
|
+
` ORACLE_DATA_URL: ${yamlString(endpoint)}`,
|
|
83
|
+
` ORACLE_AGENT_KEY: ${yamlString(agentKey)}`,
|
|
84
|
+
"",
|
|
85
|
+
].join("\n"),
|
|
86
|
+
claudeCode: json(mcp),
|
|
87
|
+
codex: [
|
|
88
|
+
`[mcp_servers.${tomlString(name)}]`,
|
|
89
|
+
`command = ${tomlString(COMMAND)}`,
|
|
90
|
+
`args = [${ARGS.map(tomlString).join(", ")}]`,
|
|
91
|
+
"",
|
|
92
|
+
`[mcp_servers.${tomlString(name)}.env]`,
|
|
93
|
+
`ORACLE_DATA_URL = ${tomlString(endpoint)}`,
|
|
94
|
+
`ORACLE_AGENT_KEY = ${tomlString(agentKey)}`,
|
|
95
|
+
"",
|
|
96
|
+
].join("\n"),
|
|
97
|
+
cursor: json(mcp),
|
|
98
|
+
genericMcp: json(mcp),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const createHarnessConfigs = emitHarnessConfigs;
|
package/src/oracle-env.mjs
CHANGED
|
@@ -20,9 +20,18 @@ export function envFlag(primary, legacy, defaultValue = false) {
|
|
|
20
20
|
return String(env(primary, legacy, fallback)).trim() === "1";
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const DISABLE_WORDS = ["0", "false", "off", "no"];
|
|
24
|
+
|
|
25
|
+
// Reading a flag that may default ON. Only an explicit disable word turns it
|
|
26
|
+
// off, and an unset value keeps `defaultValue`.
|
|
27
|
+
//
|
|
28
|
+
// The old shape (`!== "0"`) inverted with a false default: ORACLE_ONBOARD_HTTP
|
|
29
|
+
// defaults off, so a user writing `false` to keep it off actually ENABLED it,
|
|
30
|
+
// because "false" is not "0". A disable word must never enable a surface.
|
|
23
31
|
export function envEnabled(primary, legacy, defaultValue = true) {
|
|
24
|
-
const
|
|
25
|
-
|
|
32
|
+
const raw = String(env(primary, legacy, "")).trim().toLowerCase();
|
|
33
|
+
if (raw === "") return defaultValue;
|
|
34
|
+
return !DISABLE_WORDS.includes(raw);
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
export function csvEnv(primary, legacy) {
|