@oracle-agent/oracle 0.3.1 → 0.3.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 +32 -7
- package/SETUP.md +14 -0
- package/package.json +1 -1
- package/public/index.html +12 -0
- package/src/agent-auth.mjs +15 -0
- package/src/index.mjs +1 -0
- package/src/prepare-envelope.mjs +66 -10
package/README.md
CHANGED
|
@@ -18,18 +18,43 @@ alongside it — same split we run ourselves.
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Start in 5 steps
|
|
22
|
+
|
|
23
|
+
Latest: **oracle `0.3.1`** · **operator `0.6.1`**
|
|
22
24
|
|
|
23
25
|
```bash
|
|
24
|
-
#
|
|
25
|
-
npm i @oracle-agent/oracle
|
|
26
|
+
# 1) install both packages
|
|
27
|
+
npm i @oracle-agent/oracle@0.3.1 @oracle-agent/operator@0.6.1
|
|
28
|
+
|
|
29
|
+
# 2) put keys ONLY on your machine (example: HL/EVM hex key file)
|
|
30
|
+
mkdir -p ~/.config/oracle/keys && chmod 700 ~/.config/oracle/keys
|
|
31
|
+
# write key, then: npx oracle-vault encrypt ~/.config/oracle/keys/evm.key
|
|
32
|
+
export HL_KEY_FILE=~/.config/oracle/keys/evm.key.vault.json
|
|
33
|
+
export ORACLE_VAULT_PASSPHRASE='...' # signer shell only — never in the agent chat
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
// 3) prepare (no keys)
|
|
38
|
+
import { data } from "@oracle-agent/oracle";
|
|
39
|
+
const prepared = await data.call("hl-perps", "prepareOrder", { /* coin, side, size... */ });
|
|
40
|
+
|
|
41
|
+
// 4) keys local → operator signs (this is the sign step — no MetaMask)
|
|
42
|
+
import { hlSignAndSubmit } from "@oracle-agent/operator";
|
|
43
|
+
await hlSignAndSubmit(prepared, {
|
|
44
|
+
keyFile: process.env.HL_KEY_FILE,
|
|
45
|
+
userInitiated: true, // app confirmed the action; or use autonomous/daemon below
|
|
46
|
+
});
|
|
47
|
+
```
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
```bash
|
|
50
|
+
# 5) unattended desk (no click each trade) — HL/Poly capped
|
|
51
|
+
export ORACLE_AUTONOMOUS_TRADING=1
|
|
52
|
+
# or: npx oracle-signer
|
|
29
53
|
```
|
|
30
54
|
|
|
31
|
-
|
|
32
|
-
|
|
55
|
+
Full vault / multi-chain / daemon detail → operator [SETUP](https://github.com/demi-hl/oracle-operator/blob/main/SETUP.md).
|
|
56
|
+
|
|
57
|
+
---
|
|
33
58
|
|
|
34
59
|
## What Oracle is
|
|
35
60
|
|
package/SETUP.md
CHANGED
|
@@ -293,3 +293,17 @@ npm run protocol:gate -- safe-erc20
|
|
|
293
293
|
|
|
294
294
|
`prepareTemplateDeploy` will not stamp an unsigned deploy unless `forge test` passes.
|
|
295
295
|
**Not a paid security-firm audit** — see each template `SECURITY.md`.
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
## Optional keyed prepare stamps (hardening)
|
|
299
|
+
|
|
300
|
+
Integrity hash is always on. For a shared HMAC between prepare process and signer:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
# same secret on prepare host and signer host (not in the agent chat)
|
|
304
|
+
export ORACLE_STAMP_HMAC_SECRET='long-random-secret'
|
|
305
|
+
# signer-only: refuse envelopes without MAC
|
|
306
|
+
export ORACLE_STAMP_REQUIRE_MAC=1
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Default remains hash-only (public GO path). HMAC is opt-in hardening.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oracle-agent/oracle",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"/>
|
|
5
|
+
<meta http-equiv="refresh" content="0; url=./oracle-splash/"/>
|
|
6
|
+
<link rel="canonical" href="./oracle-splash/"/>
|
|
7
|
+
<title>Oracle</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<p><a href="./oracle-splash/">Oracle splash</a></p>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/src/agent-auth.mjs
CHANGED
|
@@ -146,6 +146,21 @@ export function verifyCapability(tag, { agentAddress, chainId = null, action = n
|
|
|
146
146
|
* @returns {Promise<[string,string,string,string]>}
|
|
147
147
|
*/
|
|
148
148
|
export async function mintCapability(ownerSigner, agentAddress, conditions = "") {
|
|
149
|
+
// Hardening: never accept a raw key string/hex. Real ethers Wallet/Signer objects are OK
|
|
150
|
+
// (they may expose .privateKey internally — that is the wallet, not a mistaken string).
|
|
151
|
+
if (typeof ownerSigner === "string" || typeof ownerSigner === "number") {
|
|
152
|
+
throw new Error("mintCapability refuses raw key material — pass an ethers Signer from the owner wallet");
|
|
153
|
+
}
|
|
154
|
+
if (!ownerSigner || typeof ownerSigner.signMessage !== "function" || typeof ownerSigner.getAddress !== "function") {
|
|
155
|
+
throw new Error("mintCapability: ownerSigner must implement getAddress() and signMessage()");
|
|
156
|
+
}
|
|
157
|
+
// Bare POJO that only carries a key field (no prototype signer) — refuse.
|
|
158
|
+
const proto = Object.getPrototypeOf(ownerSigner);
|
|
159
|
+
if (proto === Object.prototype || proto === null) {
|
|
160
|
+
if (ownerSigner.privateKey || ownerSigner.mnemonic) {
|
|
161
|
+
throw new Error("mintCapability refuses plain objects that only carry privateKey/mnemonic");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
149
164
|
parseConditions(conditions); // validate before signing
|
|
150
165
|
const owner = await ownerSigner.getAddress();
|
|
151
166
|
if (owner.toLowerCase() === String(agentAddress).toLowerCase()) {
|
package/src/index.mjs
CHANGED
package/src/prepare-envelope.mjs
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
// Canonical prepare envelope shared by Oracle prepare helpers and the local operator.
|
|
2
|
-
// The versioned checksum binds payload and freshness metadata. It detects mutation
|
|
3
|
-
//
|
|
2
|
+
// The versioned checksum binds payload and freshness metadata. It detects mutation.
|
|
3
|
+
// Optional HMAC (ORACLE_STAMP_HMAC_SECRET) adds keyed integrity when configured.
|
|
4
|
+
// Signer-owned policy, not this public checksum alone, authorizes execution.
|
|
4
5
|
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
6
7
|
|
|
7
8
|
export const PREPARE_VERSION = 2;
|
|
8
9
|
/** Default max age for a prepared envelope at sign time (ms). */
|
|
9
10
|
export const PREPARE_MAX_AGE_MS = 5 * 60 * 1000;
|
|
10
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Hard ceiling for maxAgeMs. Caller/agent cannot widen past this.
|
|
13
|
+
* Override upward only via signer-process env ORACLE_PREPARE_HARD_MAX_AGE_MS.
|
|
14
|
+
*/
|
|
11
15
|
export const PREPARE_HARD_MAX_AGE_MS = (() => {
|
|
12
16
|
const n = Number(process.env.ORACLE_PREPARE_HARD_MAX_AGE_MS);
|
|
13
|
-
return Number.isFinite(n) && n > 0 ? n : 30 * 60 * 1000;
|
|
17
|
+
return Number.isFinite(n) && n > 0 ? n : 30 * 60 * 1000; // 30 minutes
|
|
14
18
|
})();
|
|
15
19
|
|
|
20
|
+
/** Resolve max age: default 5m, never above hard max (even if caller passes MAX_SAFE_INTEGER). */
|
|
16
21
|
export function resolvePrepareMaxAgeMs(requested) {
|
|
17
|
-
const base =
|
|
22
|
+
const base =
|
|
23
|
+
requested == null || requested === ""
|
|
24
|
+
? PREPARE_MAX_AGE_MS
|
|
25
|
+
: Number(requested);
|
|
18
26
|
if (!Number.isFinite(base) || base < 0) return PREPARE_MAX_AGE_MS;
|
|
19
27
|
return Math.min(base, PREPARE_HARD_MAX_AGE_MS);
|
|
20
28
|
}
|
|
@@ -26,11 +34,21 @@ function stableStringify(value) {
|
|
|
26
34
|
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
|
|
27
35
|
}
|
|
28
36
|
|
|
37
|
+
function stampSecret(env = process.env) {
|
|
38
|
+
const s = String(env.ORACLE_STAMP_HMAC_SECRET || "").trim();
|
|
39
|
+
return s.length >= 16 ? s : "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function requireMac(env = process.env) {
|
|
43
|
+
return String(env.ORACLE_STAMP_REQUIRE_MAC || "").trim() === "1";
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
/** Fields that bind the envelope (must not include the stamp fields themselves). */
|
|
30
47
|
export function prepareBodyForHash(prepared) {
|
|
31
48
|
const {
|
|
32
49
|
oraclePrepared,
|
|
33
50
|
prepareHash,
|
|
51
|
+
prepareMac,
|
|
34
52
|
note,
|
|
35
53
|
...body
|
|
36
54
|
} = prepared || {};
|
|
@@ -42,11 +60,27 @@ export function computePrepareHash(prepared) {
|
|
|
42
60
|
return createHash("sha256").update(stableStringify(body)).digest("hex");
|
|
43
61
|
}
|
|
44
62
|
|
|
63
|
+
export function computePrepareMac(prepareHash, secret = stampSecret()) {
|
|
64
|
+
if (!secret) return "";
|
|
65
|
+
return createHmac("sha256", secret).update(String(prepareHash)).digest("hex");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function safeEqualHex(a, b) {
|
|
69
|
+
try {
|
|
70
|
+
const ba = Buffer.from(String(a), "hex");
|
|
71
|
+
const bb = Buffer.from(String(b), "hex");
|
|
72
|
+
if (ba.length === 0 || ba.length !== bb.length) return false;
|
|
73
|
+
return timingSafeEqual(ba, bb);
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
45
79
|
/**
|
|
46
80
|
* Stamp a prepare result. Call at the end of every prepare helper.
|
|
47
81
|
* @param {object} payload provider-specific prepare fields (action/tx/psbt/...)
|
|
48
82
|
*/
|
|
49
|
-
export function stampPrepared(payload, { provider, kind } = {}) {
|
|
83
|
+
export function stampPrepared(payload, { provider, kind, env = process.env } = {}) {
|
|
50
84
|
if (!payload || typeof payload !== "object") throw new Error("stampPrepared: payload object required");
|
|
51
85
|
const base = {
|
|
52
86
|
...payload,
|
|
@@ -64,11 +98,15 @@ export function stampPrepared(payload, { provider, kind } = {}) {
|
|
|
64
98
|
const prepareVersion = PREPARE_VERSION;
|
|
65
99
|
const withMeta = { ...base, preparedAt, expiresAt, prepareVersion };
|
|
66
100
|
const prepareHash = computePrepareHash(withMeta);
|
|
67
|
-
|
|
101
|
+
const secret = stampSecret(env);
|
|
102
|
+
const prepareMac = secret ? computePrepareMac(prepareHash, secret) : undefined;
|
|
103
|
+
const out = {
|
|
68
104
|
...withMeta,
|
|
69
105
|
oraclePrepared: true,
|
|
70
106
|
prepareHash,
|
|
71
107
|
};
|
|
108
|
+
if (prepareMac) out.prepareMac = prepareMac;
|
|
109
|
+
return out;
|
|
72
110
|
}
|
|
73
111
|
|
|
74
112
|
/**
|
|
@@ -81,13 +119,14 @@ export function assertPreparedEnvelope(prepared, {
|
|
|
81
119
|
providers = null,
|
|
82
120
|
kinds = null,
|
|
83
121
|
label = "operator",
|
|
122
|
+
env = process.env,
|
|
84
123
|
} = {}) {
|
|
85
124
|
if (!prepared || typeof prepared !== "object") {
|
|
86
125
|
throw new Error(`${label}: prepared envelope object required`);
|
|
87
126
|
}
|
|
88
127
|
if (prepared.oraclePrepared !== true) {
|
|
89
128
|
throw new Error(
|
|
90
|
-
`${label}: prepared integrity envelope missing — use an @oracle-agent/oracle prepare result
|
|
129
|
+
`${label}: prepared integrity envelope missing — use an @oracle-agent/oracle prepare result`,
|
|
91
130
|
);
|
|
92
131
|
}
|
|
93
132
|
if (Number(prepared.prepareVersion) !== PREPARE_VERSION) {
|
|
@@ -97,6 +136,21 @@ export function assertPreparedEnvelope(prepared, {
|
|
|
97
136
|
if (String(prepared.prepareHash || "") !== expected) {
|
|
98
137
|
throw new Error(`${label}: prepareHash mismatch — payload was altered after prepare`);
|
|
99
138
|
}
|
|
139
|
+
|
|
140
|
+
const secret = stampSecret(env);
|
|
141
|
+
const mac = String(prepared.prepareMac || "");
|
|
142
|
+
if (requireMac(env) || mac) {
|
|
143
|
+
if (!secret) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`${label}: prepareMac present or ORACLE_STAMP_REQUIRE_MAC=1 but ORACLE_STAMP_HMAC_SECRET missing in signer`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const expectedMac = computePrepareMac(expected, secret);
|
|
149
|
+
if (!mac || !safeEqualHex(mac, expectedMac)) {
|
|
150
|
+
throw new Error(`${label}: prepareMac mismatch — stamp not from trusted preparer`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
100
154
|
const preparedAt = Number(prepared.preparedAt);
|
|
101
155
|
const expiresAt = Number(prepared.expiresAt);
|
|
102
156
|
const effectiveMax = resolvePrepareMaxAgeMs(maxAgeMs);
|
|
@@ -112,7 +166,9 @@ export function assertPreparedEnvelope(prepared, {
|
|
|
112
166
|
age > effectiveMax ||
|
|
113
167
|
nowMs > expiresAt
|
|
114
168
|
) {
|
|
115
|
-
throw new Error(
|
|
169
|
+
throw new Error(
|
|
170
|
+
`${label}: prepare envelope expired or clock-skewed (ageMs=${age}, max=${effectiveMax})`,
|
|
171
|
+
);
|
|
116
172
|
}
|
|
117
173
|
if (providers) {
|
|
118
174
|
const allow = new Set(providers);
|