@finchagentic/mcp 4.1.0 → 4.2.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.
- package/README.md +14 -14
- package/dist/agent-loop.js +75 -72
- package/dist/annotations.js +14 -13
- package/dist/convex.js +15 -18
- package/dist/index.js +4 -5
- package/dist/llm.js +12 -1
- package/dist/local-memory-file.js +14 -1
- package/dist/local-memory.js +13 -0
- package/dist/output-schemas.js +71 -17
- package/dist/resources.js +6 -11
- package/dist/server.js +27 -13
- package/dist/token-gate.js +2 -2
- package/dist/tool-filter.js +13 -4
- package/dist/tools/agents.js +62 -390
- package/dist/tools/base-mcp.js +3 -15
- package/dist/tools/base.js +34 -20
- package/dist/tools/defi.js +10 -30
- package/dist/tools/equity.js +9 -1
- package/dist/tools/insight.js +2 -2
- package/dist/tools/memory.js +44 -80
- package/dist/tools/monitor.js +6 -6
- package/dist/tools/os.js +0 -1
- package/dist/tools/packets.js +2 -2
- package/dist/tools/rh-mcp.js +13 -2
- package/dist/tools/rh-orders.js +201 -123
- package/dist/tools/scanner.js +30 -0
- package/dist/tools/stake.js +329 -0
- package/dist/tools/vault.js +99 -21
- package/dist/wallet.js +130 -19
- package/package.json +2 -2
- package/dist/tools/framework.js +0 -150
package/dist/wallet.js
CHANGED
|
@@ -66,19 +66,54 @@ exports.MEV_PROTECT_ENABLED = !!process.env.FINCH_BROADCAST_RPC;
|
|
|
66
66
|
exports.BASE_CHAIN_ID = 8453;
|
|
67
67
|
const WALLET_DIR = path.join(os.homedir(), ".finch");
|
|
68
68
|
const WALLET_FILE = path.join(WALLET_DIR, "wallet.json");
|
|
69
|
+
// Per-install random secret folded into the no-passphrase key derivation
|
|
70
|
+
// (see getMachineKey). Generated once via crypto.randomBytes and stored
|
|
71
|
+
// 0600 next to the wallet - real entropy, unlike hostname/platform/arch
|
|
72
|
+
// which are guessable/public and give an attacker who copies the wallet
|
|
73
|
+
// file everything they need to also derive the key.
|
|
74
|
+
const LOCAL_SECRET_FILE = path.join(WALLET_DIR, ".local-secret");
|
|
69
75
|
let _cachedWallet = null;
|
|
70
76
|
function clearWalletCache() { _cachedWallet = null; }
|
|
77
|
+
function getOrCreateLocalSecret() {
|
|
78
|
+
try {
|
|
79
|
+
const existing = fs.readFileSync(LOCAL_SECRET_FILE, "utf8").trim();
|
|
80
|
+
if (existing)
|
|
81
|
+
return existing;
|
|
82
|
+
}
|
|
83
|
+
catch { /* doesn't exist yet, or unreadable - (re)create below */ }
|
|
84
|
+
const secret = crypto.randomBytes(32).toString("hex");
|
|
85
|
+
if (!fs.existsSync(WALLET_DIR))
|
|
86
|
+
fs.mkdirSync(WALLET_DIR, { recursive: true });
|
|
87
|
+
fs.writeFileSync(LOCAL_SECRET_FILE, secret, { mode: 0o600 });
|
|
88
|
+
return secret;
|
|
89
|
+
}
|
|
71
90
|
function getMachineKey() {
|
|
72
91
|
// A passphrase is meant to make the wallet portable (move the encrypted
|
|
73
92
|
// file + set the same passphrase elsewhere and it still decrypts) - so when
|
|
74
93
|
// one is set, derive the key from ONLY the passphrase, no machine binding.
|
|
75
|
-
// Without a passphrase, fall back to machine info as convenience-only
|
|
76
|
-
// encryption (prevents casual reads, not security against an attacker who
|
|
77
|
-
// has both the file and the system info).
|
|
78
94
|
const passphrase = process.env.FINCH_WALLET_PASSPHRASE ?? "";
|
|
79
95
|
if (passphrase) {
|
|
80
96
|
return crypto.createHash("sha256").update(passphrase).digest("hex").slice(0, 32);
|
|
81
97
|
}
|
|
98
|
+
// Without a passphrase, this is convenience-only encryption - it still
|
|
99
|
+
// can't stop an attacker who obtains BOTH files (the encrypted wallet and
|
|
100
|
+
// this local secret), the same as any locally-stored key material. What it
|
|
101
|
+
// does stop is the weaker, more common case this used to be vulnerable to:
|
|
102
|
+
// hostname/platform/arch alone are public/guessable, so a copy of just the
|
|
103
|
+
// wallet file (backup sync, stolen disk, malware scraping known paths) used
|
|
104
|
+
// to be enough to brute-force the key offline. Folding in a random,
|
|
105
|
+
// file-local secret means the wallet file alone is no longer sufficient.
|
|
106
|
+
return crypto
|
|
107
|
+
.createHash("sha256")
|
|
108
|
+
.update(getOrCreateLocalSecret() + os.hostname() + os.platform() + os.arch())
|
|
109
|
+
.digest("hex")
|
|
110
|
+
.slice(0, 32);
|
|
111
|
+
}
|
|
112
|
+
/** Pre-entropy-fix no-passphrase key: machine info only, no local secret.
|
|
113
|
+
* Kept solely so wallets encrypted before this fix still open; migrated to
|
|
114
|
+
* the new scheme in place on first successful decrypt, same pattern as
|
|
115
|
+
* getLegacyMachineKey below. */
|
|
116
|
+
function getLegacyMachineOnlyKey() {
|
|
82
117
|
return crypto
|
|
83
118
|
.createHash("sha256")
|
|
84
119
|
.update(os.hostname() + os.platform() + os.arch())
|
|
@@ -91,9 +126,15 @@ function getMachineKey() {
|
|
|
91
126
|
* decrypted on the exact machine that created it - never portable. Kept
|
|
92
127
|
* solely so those existing wallets still open; getOrCreateWallet migrates
|
|
93
128
|
* them to the portable scheme in place on first successful decrypt.
|
|
129
|
+
*
|
|
130
|
+
* `passphrase` is a parameter, not read from env, because the whole point of
|
|
131
|
+
* this fallback is testing what the file was ACTUALLY encrypted with - which
|
|
132
|
+
* may not be today's FINCH_WALLET_PASSPHRASE. The most common case this
|
|
133
|
+
* exists for: a user who never set a passphrase before (so the file was
|
|
134
|
+
* encrypted with "" + machine info) setting one for the FIRST time just now -
|
|
135
|
+
* at that moment env has the new passphrase, but the file predates it.
|
|
94
136
|
*/
|
|
95
|
-
function getLegacyMachineKey() {
|
|
96
|
-
const passphrase = process.env.FINCH_WALLET_PASSPHRASE ?? "";
|
|
137
|
+
function getLegacyMachineKey(passphrase) {
|
|
97
138
|
return crypto
|
|
98
139
|
.createHash("sha256")
|
|
99
140
|
.update(passphrase + os.hostname() + os.platform() + os.arch())
|
|
@@ -106,13 +147,34 @@ function warnIfNoPassphrase() {
|
|
|
106
147
|
return;
|
|
107
148
|
_passphraseWarned = true;
|
|
108
149
|
// stderr only - stdout is reserved for MCP JSON-RPC framing when running as a server.
|
|
109
|
-
process.stderr.write("\n⚠️ FINCH_WALLET_PASSPHRASE is not set. Your
|
|
110
|
-
`(${WALLET_FILE}) is encrypted with a key derived
|
|
111
|
-
|
|
112
|
-
"file (backup sync, stolen disk,
|
|
113
|
-
"strong secret
|
|
150
|
+
process.stderr.write("\n⚠️ FINCH_WALLET_PASSPHRASE is not set. Your local wallet " +
|
|
151
|
+
`(${WALLET_FILE}, used on both Base and Robinhood Chain) is encrypted with a key derived from a random ` +
|
|
152
|
+
`per-install secret (${LOCAL_SECRET_FILE}) plus this machine's hostname/platform/arch. That stops the wallet ` +
|
|
153
|
+
"file alone from being crackable, but anyone who copies BOTH files together (backup sync, stolen disk, " +
|
|
154
|
+
"malware) still gets the wallet. Set FINCH_WALLET_PASSPHRASE to a strong secret you keep out of that backup " +
|
|
155
|
+
"for real protection. This wallet holds real funds.\n\n");
|
|
114
156
|
}
|
|
157
|
+
let _walletCreationPromise = null;
|
|
115
158
|
async function getOrCreateWallet() {
|
|
159
|
+
if (_cachedWallet)
|
|
160
|
+
return _cachedWallet;
|
|
161
|
+
// In-process mutex: two concurrent first-run callers (before _cachedWallet
|
|
162
|
+
// is set) must not each independently generate + write their own random
|
|
163
|
+
// wallet - only one write can ever survive on disk, and the loser would go
|
|
164
|
+
// on signing in-memory with a keypair that no longer matches what's
|
|
165
|
+
// persisted, silently switching the user's wallet identity mid-session.
|
|
166
|
+
// Chain all concurrent first-run callers through one shared promise.
|
|
167
|
+
if (_walletCreationPromise)
|
|
168
|
+
return _walletCreationPromise;
|
|
169
|
+
_walletCreationPromise = loadOrCreateWallet();
|
|
170
|
+
try {
|
|
171
|
+
return await _walletCreationPromise;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
_walletCreationPromise = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function loadOrCreateWallet() {
|
|
116
178
|
if (_cachedWallet)
|
|
117
179
|
return _cachedWallet;
|
|
118
180
|
warnIfNoPassphrase();
|
|
@@ -129,22 +191,55 @@ async function getOrCreateWallet() {
|
|
|
129
191
|
// machine (it still needs that machine's hostname/platform/arch) - it
|
|
130
192
|
// can't rescue a wallet file copied to a new machine from before this
|
|
131
193
|
// fix; there was no passphrase-only secret saved anywhere to recover.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
194
|
+
//
|
|
195
|
+
// Try two legacy candidates: today's passphrase (in case it was already
|
|
196
|
+
// set when this file was encrypted) and "" (the common case - a user
|
|
197
|
+
// setting FINCH_WALLET_PASSPHRASE for the first time, whose existing
|
|
198
|
+
// file predates having any passphrase at all).
|
|
199
|
+
const legacyCandidates = [...new Set([process.env.FINCH_WALLET_PASSPHRASE ?? "", ""])];
|
|
200
|
+
let legacyWallet = null;
|
|
201
|
+
for (const candidate of legacyCandidates) {
|
|
202
|
+
try {
|
|
203
|
+
legacyWallet = await ethers_1.ethers.Wallet.fromEncryptedJson(encrypted, getLegacyMachineKey(candidate));
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
catch { /* try next candidate */ }
|
|
207
|
+
}
|
|
208
|
+
if (legacyWallet) {
|
|
209
|
+
_cachedWallet = legacyWallet;
|
|
135
210
|
// Migrate in place to the portable scheme now that we've proven we
|
|
136
211
|
// hold the right key, so this only ever needs to happen once.
|
|
137
212
|
try {
|
|
138
|
-
const migrated = await
|
|
213
|
+
const migrated = await legacyWallet.encrypt(getMachineKey());
|
|
139
214
|
fs.writeFileSync(WALLET_FILE, migrated, { mode: 0o600 });
|
|
140
215
|
process.stderr.write(`\nMigrated ${WALLET_FILE} to the portable passphrase scheme.\n\n`);
|
|
141
216
|
}
|
|
142
217
|
catch {
|
|
143
218
|
/* migration is best-effort - the legacy key still works next run either way */
|
|
144
219
|
}
|
|
145
|
-
return
|
|
220
|
+
return legacyWallet;
|
|
221
|
+
}
|
|
222
|
+
// Third tier: no passphrase ever set, and this wallet predates the fix
|
|
223
|
+
// that folds a random local secret into the no-passphrase key (it was
|
|
224
|
+
// encrypted with hostname/platform/arch alone). Try that exact old
|
|
225
|
+
// derivation before giving up.
|
|
226
|
+
if (!process.env.FINCH_WALLET_PASSPHRASE) {
|
|
227
|
+
try {
|
|
228
|
+
const oldNoPassWallet = await ethers_1.ethers.Wallet.fromEncryptedJson(encrypted, getLegacyMachineOnlyKey());
|
|
229
|
+
_cachedWallet = oldNoPassWallet;
|
|
230
|
+
try {
|
|
231
|
+
const migrated = await oldNoPassWallet.encrypt(getMachineKey());
|
|
232
|
+
fs.writeFileSync(WALLET_FILE, migrated, { mode: 0o600 });
|
|
233
|
+
process.stderr.write(`\nMigrated ${WALLET_FILE} to the higher-entropy no-passphrase scheme.\n\n`);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
/* migration is best-effort - the legacy key still works next run either way */
|
|
237
|
+
}
|
|
238
|
+
return oldNoPassWallet;
|
|
239
|
+
}
|
|
240
|
+
catch { /* not this scheme either - fall through to the hard failure below */ }
|
|
146
241
|
}
|
|
147
|
-
|
|
242
|
+
{
|
|
148
243
|
// A wallet file already exists but couldn't be decrypted under either
|
|
149
244
|
// scheme - this almost always means FINCH_WALLET_PASSPHRASE doesn't
|
|
150
245
|
// match what encrypted it, or this is a different machine and no
|
|
@@ -166,9 +261,25 @@ async function getOrCreateWallet() {
|
|
|
166
261
|
if (!fs.existsSync(WALLET_DIR))
|
|
167
262
|
fs.mkdirSync(WALLET_DIR, { recursive: true });
|
|
168
263
|
const encrypted = await wallet.encrypt(getMachineKey());
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
264
|
+
try {
|
|
265
|
+
// Exclusive create ("wx") - guards the cross-process version of the race
|
|
266
|
+
// the in-process mutex above already closes: two separate `finch`
|
|
267
|
+
// invocations racing on the very first run, before this file exists.
|
|
268
|
+
fs.writeFileSync(WALLET_FILE, encrypted, { mode: 0o600, flag: "wx" });
|
|
269
|
+
_cachedWallet = wallet;
|
|
270
|
+
return wallet;
|
|
271
|
+
}
|
|
272
|
+
catch (writeErr) {
|
|
273
|
+
if (writeErr?.code !== "EEXIST")
|
|
274
|
+
throw writeErr;
|
|
275
|
+
// Another process won the race and created the file first. Use theirs -
|
|
276
|
+
// never sign with the wallet we generated in memory once it's clear it
|
|
277
|
+
// isn't the one actually persisted to disk.
|
|
278
|
+
const encryptedExisting = fs.readFileSync(WALLET_FILE, "utf8");
|
|
279
|
+
const existingWallet = await ethers_1.ethers.Wallet.fromEncryptedJson(encryptedExisting, getMachineKey());
|
|
280
|
+
_cachedWallet = existingWallet;
|
|
281
|
+
return existingWallet;
|
|
282
|
+
}
|
|
172
283
|
}
|
|
173
284
|
async function signRequest(toolName) {
|
|
174
285
|
const wallet = await getOrCreateWallet();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@finchagentic/mcp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "The runtime layer for Agentic AI. Persistent memory, autonomous agents, and workflows that survive every session.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"preferGlobal": true,
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "tsc",
|
|
12
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
13
13
|
"dev": "ts-node src/index.ts",
|
|
14
14
|
"start": "node dist/index.js",
|
|
15
15
|
"test": "vitest run",
|
package/dist/tools/framework.js
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FRAMEWORK_TOOLS = void 0;
|
|
4
|
-
exports.handleFrameworkTool = handleFrameworkTool;
|
|
5
|
-
const convex_js_1 = require("../convex.js");
|
|
6
|
-
exports.FRAMEWORK_TOOLS = [
|
|
7
|
-
{
|
|
8
|
-
name: "list_playbooks",
|
|
9
|
-
description: "List available Noel Framework playbooks - predefined multi-step workflows. " +
|
|
10
|
-
"Includes 4 system playbooks (Daily Market Scan, DCA Setup, Portfolio Rebalance Check, " +
|
|
11
|
-
"Research Sweep) plus any you've created. Each step is Sentinel-gated.",
|
|
12
|
-
inputSchema: { type: "object", properties: {}, required: [] },
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
name: "run_playbook",
|
|
16
|
-
description: "Execute a Noel Framework playbook. Each step runs through Sentinel before the " +
|
|
17
|
-
"matching tool executes it. Steps map directly to finch tools (market, vault, agent, " +
|
|
18
|
-
"memory, automation). Playbook halts immediately if Sentinel blocks a step.",
|
|
19
|
-
inputSchema: {
|
|
20
|
-
type: "object",
|
|
21
|
-
properties: {
|
|
22
|
-
playbook_name: {
|
|
23
|
-
type: "string",
|
|
24
|
-
description: "Exact name of the playbook. Use list_playbooks to see available ones.",
|
|
25
|
-
},
|
|
26
|
-
task_description: {
|
|
27
|
-
type: "string",
|
|
28
|
-
description: "Optional context passed as overrideParams to the playbook run.",
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
required: ["playbook_name"],
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: "get_finch_ledger",
|
|
36
|
-
description: "Get the Noel Framework audit trail - every Sentinel gate decision " +
|
|
37
|
-
"(approved / blocked / warned), which checks ran, duration, and reason. " +
|
|
38
|
-
"Full transparency on what agents are and aren't allowed to do.",
|
|
39
|
-
inputSchema: { type: "object", properties: {}, required: [] },
|
|
40
|
-
},
|
|
41
|
-
];
|
|
42
|
-
async function handleFrameworkTool(name, args) {
|
|
43
|
-
const a = (args ?? {});
|
|
44
|
-
switch (name) {
|
|
45
|
-
// ── list_playbooks ──────────────────────────────────────────────────────
|
|
46
|
-
case "list_playbooks": {
|
|
47
|
-
const result = await (0, convex_js_1.callConvex)("/framework/playbooks", "GET", undefined, "list_playbooks");
|
|
48
|
-
const pbs = result.playbooks ?? [];
|
|
49
|
-
if (pbs.length === 0) {
|
|
50
|
-
return { content: [{ type: "text", text: "No playbooks found." }] };
|
|
51
|
-
}
|
|
52
|
-
const list = pbs
|
|
53
|
-
.map((p) => {
|
|
54
|
-
const steps = (() => {
|
|
55
|
-
try {
|
|
56
|
-
return JSON.parse(p.steps).length;
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return "?";
|
|
60
|
-
}
|
|
61
|
-
})();
|
|
62
|
-
return `• **${p.name}**${p.isPublic ? " 🌐" : " 👤"} - ${steps} steps\n ${p.description}\n Used ${p.usageCount} times`;
|
|
63
|
-
})
|
|
64
|
-
.join("\n\n");
|
|
65
|
-
return { content: [{ type: "text", text: `**Available Playbooks**\n\n${list}` }] };
|
|
66
|
-
}
|
|
67
|
-
// ── run_playbook ────────────────────────────────────────────────────────
|
|
68
|
-
case "run_playbook": {
|
|
69
|
-
if (!a.playbook_name) {
|
|
70
|
-
return { content: [{ type: "text", text: "playbook_name is required" }], isError: true };
|
|
71
|
-
}
|
|
72
|
-
// Resolve playbook ID by name
|
|
73
|
-
const pbList = await (0, convex_js_1.callConvex)("/framework/playbooks", "GET", undefined, "run_playbook");
|
|
74
|
-
const playbook = (pbList.playbooks ?? []).find((p) => p.name.toLowerCase() === String(a.playbook_name).toLowerCase());
|
|
75
|
-
if (!playbook) {
|
|
76
|
-
return {
|
|
77
|
-
content: [{
|
|
78
|
-
type: "text",
|
|
79
|
-
text: `Playbook "${a.playbook_name}" not found. Use list_playbooks to see available ones.`,
|
|
80
|
-
}],
|
|
81
|
-
isError: true,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
const result = await (0, convex_js_1.callConvex)("/framework/playbook/run", "POST", {
|
|
85
|
-
playbookId: playbook._id,
|
|
86
|
-
overrideParams: a.task_description,
|
|
87
|
-
}, "run_playbook");
|
|
88
|
-
if (result.error) {
|
|
89
|
-
return { content: [{ type: "text", text: `Run failed: ${result.error}` }], isError: true };
|
|
90
|
-
}
|
|
91
|
-
if (result.blocked) {
|
|
92
|
-
return {
|
|
93
|
-
content: [{
|
|
94
|
-
type: "text",
|
|
95
|
-
text: [
|
|
96
|
-
`🛡️ **Sentinel blocked playbook at step ${result.step}**`,
|
|
97
|
-
``,
|
|
98
|
-
`**Tool:** ${result.tool}`,
|
|
99
|
-
`**Reason:** ${result.reason}`,
|
|
100
|
-
``,
|
|
101
|
-
`This is a mechanical safety gate. The action violates the agent's permission boundary.`,
|
|
102
|
-
`Completed steps before block: ${result.results?.length ?? 0}`,
|
|
103
|
-
].join("\n"),
|
|
104
|
-
}],
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
const steps = result.results ?? [];
|
|
108
|
-
const succeeded = steps.filter(r => r.success).length;
|
|
109
|
-
const stepLines = steps.map((r) => `${r.success ? "✅" : "❌"} Step ${r.step} [${r.role}]: ${r.tool}${r.error ? ` - ${r.error}` : ""}`);
|
|
110
|
-
return {
|
|
111
|
-
content: [{
|
|
112
|
-
type: "text",
|
|
113
|
-
text: [
|
|
114
|
-
`✅ **Playbook "${a.playbook_name}" completed**`,
|
|
115
|
-
``,
|
|
116
|
-
`${succeeded}/${steps.length} steps successful`,
|
|
117
|
-
`Run ID: \`${result.runId}\``,
|
|
118
|
-
``,
|
|
119
|
-
...stepLines,
|
|
120
|
-
].join("\n"),
|
|
121
|
-
}],
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
// ── get_finch_ledger ─────────────────────────────────────────────────────
|
|
125
|
-
case "get_finch_ledger": {
|
|
126
|
-
const result = await (0, convex_js_1.callConvex)("/swarm/ledger", "GET", undefined, "get_finch_ledger");
|
|
127
|
-
const entries = result.entries ?? [];
|
|
128
|
-
if (entries.length === 0) {
|
|
129
|
-
return {
|
|
130
|
-
content: [{
|
|
131
|
-
type: "text",
|
|
132
|
-
text: "No ledger entries yet. Run a playbook to see Sentinel decisions.",
|
|
133
|
-
}],
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
const lines = entries.map((e) => {
|
|
137
|
-
const icon = e.decision === "approved" ? "✅" : e.decision === "blocked" ? "🚫" : "⚠️";
|
|
138
|
-
return `${icon} **${e.agentId}** → \`${e.action}\`\n ${e.reason} (${e.durationMs}ms)`;
|
|
139
|
-
});
|
|
140
|
-
return {
|
|
141
|
-
content: [{
|
|
142
|
-
type: "text",
|
|
143
|
-
text: `**Noel Ledger** (last ${entries.length} decisions)\n\n${lines.join("\n\n")}`,
|
|
144
|
-
}],
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
default:
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
}
|