@crewhaus/target-onchain 0.1.4 → 0.1.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/dist/index.d.ts +41 -0
- package/dist/index.js +213 -0
- package/package.json +11 -8
- package/src/index.test.ts +0 -270
- package/src/index.ts +0 -234
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog F2 `target-onchain` — §47.
|
|
3
|
+
*
|
|
4
|
+
* Codegen for the `onchain` event-driven daemon. The emitted bundle is
|
|
5
|
+
* a single `agent.ts` file that:
|
|
6
|
+
*
|
|
7
|
+
* 1. Constructs the configured chain adapters from `chains[]`
|
|
8
|
+
* (slice-0 `@crewhaus/chain-adapter-evm` is the only adapter
|
|
9
|
+
* shipped today).
|
|
10
|
+
* 2. Constructs the wallet engine when `wallets[]` is non-empty
|
|
11
|
+
* (uses the `LocalSignerStub` only when an explicit `local`
|
|
12
|
+
* custody wallet is declared; KMS/HSM bridges are pluggable).
|
|
13
|
+
* 3. Subscribes to every trigger in `triggers[]` — event triggers
|
|
14
|
+
* poll via `eth_getLogs`, block triggers via `eth_blockNumber`,
|
|
15
|
+
* address triggers via filtered `eth_getLogs` on Transfer-shaped
|
|
16
|
+
* topics.
|
|
17
|
+
* 4. Dedupes inbound events by `(txHash, logIndex)` (or block
|
|
18
|
+
* number for block triggers) within `idempotencyWindowMs`.
|
|
19
|
+
* 5. Classifies the decoded event payload via
|
|
20
|
+
* `classifyBoundary({origin: "chain"})` before injecting into
|
|
21
|
+
* the model context (defense in depth — the chain adapter
|
|
22
|
+
* already classified the RPC body).
|
|
23
|
+
* 6. Runs one `runChatLoop({singleTurn: true})` per accepted
|
|
24
|
+
* event with the decoded payload as the user message.
|
|
25
|
+
* 7. Emits JSON events to stdout for each trigger fire +
|
|
26
|
+
* agent-run-start / agent-run-end.
|
|
27
|
+
* 8. Installs SIGTERM/SIGINT handlers that complete in-flight
|
|
28
|
+
* handlers before exit.
|
|
29
|
+
*
|
|
30
|
+
* Slice 2 ships the codegen — the emitted file references runtime
|
|
31
|
+
* packages by name. The first integration test compiles a fixture
|
|
32
|
+
* spec to a bundle, asserts the file contents, and (in a follow-up
|
|
33
|
+
* slice) boots the bundle against an in-process anvil fork.
|
|
34
|
+
*/
|
|
35
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
36
|
+
import type { Bundle, IrChainV0 } from "@crewhaus/ir";
|
|
37
|
+
export declare class TargetEmitError extends CrewhausError {
|
|
38
|
+
readonly name = "TargetEmitError";
|
|
39
|
+
constructor(message: string, cause?: unknown);
|
|
40
|
+
}
|
|
41
|
+
export declare function emitOnchain(ir: IrChainV0): Bundle;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog F2 `target-onchain` — §47.
|
|
3
|
+
*
|
|
4
|
+
* Codegen for the `onchain` event-driven daemon. The emitted bundle is
|
|
5
|
+
* a single `agent.ts` file that:
|
|
6
|
+
*
|
|
7
|
+
* 1. Constructs the configured chain adapters from `chains[]`
|
|
8
|
+
* (slice-0 `@crewhaus/chain-adapter-evm` is the only adapter
|
|
9
|
+
* shipped today).
|
|
10
|
+
* 2. Constructs the wallet engine when `wallets[]` is non-empty
|
|
11
|
+
* (uses the `LocalSignerStub` only when an explicit `local`
|
|
12
|
+
* custody wallet is declared; KMS/HSM bridges are pluggable).
|
|
13
|
+
* 3. Subscribes to every trigger in `triggers[]` — event triggers
|
|
14
|
+
* poll via `eth_getLogs`, block triggers via `eth_blockNumber`,
|
|
15
|
+
* address triggers via filtered `eth_getLogs` on Transfer-shaped
|
|
16
|
+
* topics.
|
|
17
|
+
* 4. Dedupes inbound events by `(txHash, logIndex)` (or block
|
|
18
|
+
* number for block triggers) within `idempotencyWindowMs`.
|
|
19
|
+
* 5. Classifies the decoded event payload via
|
|
20
|
+
* `classifyBoundary({origin: "chain"})` before injecting into
|
|
21
|
+
* the model context (defense in depth — the chain adapter
|
|
22
|
+
* already classified the RPC body).
|
|
23
|
+
* 6. Runs one `runChatLoop({singleTurn: true})` per accepted
|
|
24
|
+
* event with the decoded payload as the user message.
|
|
25
|
+
* 7. Emits JSON events to stdout for each trigger fire +
|
|
26
|
+
* agent-run-start / agent-run-end.
|
|
27
|
+
* 8. Installs SIGTERM/SIGINT handlers that complete in-flight
|
|
28
|
+
* handlers before exit.
|
|
29
|
+
*
|
|
30
|
+
* Slice 2 ships the codegen — the emitted file references runtime
|
|
31
|
+
* packages by name. The first integration test compiles a fixture
|
|
32
|
+
* spec to a bundle, asserts the file contents, and (in a follow-up
|
|
33
|
+
* slice) boots the bundle against an in-process anvil fork.
|
|
34
|
+
*/
|
|
35
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
36
|
+
import { escapeJsonString } from "@crewhaus/infra-utils";
|
|
37
|
+
export class TargetEmitError extends CrewhausError {
|
|
38
|
+
name = "TargetEmitError";
|
|
39
|
+
constructor(message, cause) {
|
|
40
|
+
super("compiler", message, cause);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function renderSecretRef(ref) {
|
|
44
|
+
if (ref.kind === "literal")
|
|
45
|
+
return JSON.stringify(ref.value);
|
|
46
|
+
return `process.env[${JSON.stringify(ref.name)}] ?? (() => { throw new Error(${JSON.stringify(`missing required env var ${ref.name}`)}); })()`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* #159 (CWE-798) — a wallet signing key must never be a bare literal in
|
|
50
|
+
* the emitted DO-NOT-EDIT artifact. The compiler's `lowerWalletKeyRef`
|
|
51
|
+
* already rejects literal keyRefs at lower time; this is the
|
|
52
|
+
* defense-in-depth guard at the emit boundary (e.g. for a hand-built IR
|
|
53
|
+
* that bypassed the lowerer). Env refs pass; only `kms://` / `hsm://`
|
|
54
|
+
* key handles are permitted as literals.
|
|
55
|
+
*/
|
|
56
|
+
const KEY_HANDLE_RE = /^(kms|hsm):\/\/.+/;
|
|
57
|
+
function renderWalletKeyRef(walletId, ref) {
|
|
58
|
+
if (ref.kind === "literal" && !KEY_HANDLE_RE.test(ref.value)) {
|
|
59
|
+
throw new TargetEmitError(`wallet "${walletId}" keyRef is a literal signing key; refusing to embed it in the generated bundle. Use an environment reference or a kms:// / hsm:// handle.`);
|
|
60
|
+
}
|
|
61
|
+
return renderSecretRef(ref);
|
|
62
|
+
}
|
|
63
|
+
function renderChain(chain) {
|
|
64
|
+
const rpcs = chain.rpcUrls.map(renderSecretRef).join(", ");
|
|
65
|
+
const finality = chain.finality.kind === "confirmations"
|
|
66
|
+
? `{ kind: "confirmations", count: ${chain.finality.count} }`
|
|
67
|
+
: `{ kind: ${JSON.stringify(chain.finality.kind)} }`;
|
|
68
|
+
return [
|
|
69
|
+
" {",
|
|
70
|
+
` chainId: ${JSON.stringify(chain.id)},`,
|
|
71
|
+
` rpcUrls: [${rpcs}],`,
|
|
72
|
+
` rpcPolicy: ${JSON.stringify(chain.rpcPolicy)},`,
|
|
73
|
+
` finality: ${finality},`,
|
|
74
|
+
` reorgTolerant: ${chain.reorgTolerant},`,
|
|
75
|
+
" }",
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
function renderWallet(w) {
|
|
79
|
+
const keyRefStr = w.keyRef !== undefined ? `,\n keyRef: ${renderWalletKeyRef(w.id, w.keyRef)}` : "";
|
|
80
|
+
return [
|
|
81
|
+
" {",
|
|
82
|
+
` id: ${JSON.stringify(w.id)},`,
|
|
83
|
+
` chainId: ${JSON.stringify(w.chainId)},`,
|
|
84
|
+
` custody: ${JSON.stringify(w.custody)},`,
|
|
85
|
+
` signingPolicy: ${JSON.stringify(w.signingPolicy)}${keyRefStr ? "" : ""}`,
|
|
86
|
+
...(keyRefStr ? [keyRefStr.replace(/^,\n/, "")] : []),
|
|
87
|
+
" }",
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
function renderContract(c) {
|
|
91
|
+
return [
|
|
92
|
+
" {",
|
|
93
|
+
` id: ${JSON.stringify(c.id)},`,
|
|
94
|
+
` chainId: ${JSON.stringify(c.chainId)},`,
|
|
95
|
+
` address: ${JSON.stringify(c.address)},`,
|
|
96
|
+
` abiRef: ${JSON.stringify(c.abiRef)},`,
|
|
97
|
+
" }",
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
function renderTrigger(t) {
|
|
101
|
+
if (t.kind === "event") {
|
|
102
|
+
const filter = t.filter !== undefined ? `,\n filter: ${JSON.stringify(t.filter)}` : "";
|
|
103
|
+
return ` { kind: "event", chainId: ${JSON.stringify(t.chainId)}, contract: ${JSON.stringify(t.contract)}, event: ${JSON.stringify(t.event)}${filter} }`;
|
|
104
|
+
}
|
|
105
|
+
if (t.kind === "block") {
|
|
106
|
+
return ` { kind: "block", chainId: ${JSON.stringify(t.chainId)}, scanIntervalMs: ${t.scanIntervalMs} }`;
|
|
107
|
+
}
|
|
108
|
+
return ` { kind: "address", chainId: ${JSON.stringify(t.chainId)}, address: ${JSON.stringify(t.address)}, direction: ${JSON.stringify(t.direction)} }`;
|
|
109
|
+
}
|
|
110
|
+
export function emitOnchain(ir) {
|
|
111
|
+
if (ir.chains.length === 0) {
|
|
112
|
+
throw new TargetEmitError("onchain target requires at least one chain binding");
|
|
113
|
+
}
|
|
114
|
+
if (ir.triggers.length === 0) {
|
|
115
|
+
throw new TargetEmitError("onchain target requires at least one trigger");
|
|
116
|
+
}
|
|
117
|
+
// Validate trigger references against chains[].
|
|
118
|
+
const chainIds = new Set(ir.chains.map((c) => c.id));
|
|
119
|
+
for (const t of ir.triggers) {
|
|
120
|
+
if (!chainIds.has(t.chainId)) {
|
|
121
|
+
throw new TargetEmitError(`trigger references chainId "${t.chainId}" not declared in chains[]`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const policy = ir.transactionPolicy;
|
|
125
|
+
// #151 — resolve the declared contracts[] (id -> address) into the policy so
|
|
126
|
+
// the wallet-engine can bind `tx.to` to the address registered for a claimed
|
|
127
|
+
// contractId. Without this map a whitelisted id can still be pointed at an
|
|
128
|
+
// arbitrary address.
|
|
129
|
+
const contractAddresses = {};
|
|
130
|
+
for (const c of ir.contracts) {
|
|
131
|
+
contractAddresses[c.id] = c.address;
|
|
132
|
+
}
|
|
133
|
+
const policyLiteral = JSON.stringify({
|
|
134
|
+
defaultWriteApproval: policy.defaultWriteApproval,
|
|
135
|
+
allowedContracts: policy.allowedContracts,
|
|
136
|
+
simulationRequired: policy.simulationRequired,
|
|
137
|
+
...(policy.maxValueUsd !== undefined ? { maxValueUsd: policy.maxValueUsd } : {}),
|
|
138
|
+
...(policy.maxValueWei !== undefined ? { maxValueWei: policy.maxValueWei } : {}),
|
|
139
|
+
...(ir.contracts.length > 0 ? { contractAddresses } : {}),
|
|
140
|
+
});
|
|
141
|
+
const chains = ir.chains.map(renderChain).join(",\n");
|
|
142
|
+
const wallets = ir.wallets.map(renderWallet).join(",\n");
|
|
143
|
+
const contracts = ir.contracts.map(renderContract).join(",\n");
|
|
144
|
+
const triggers = ir.triggers.map(renderTrigger).join(",\n");
|
|
145
|
+
const instructions = escapeJsonString(ir.agent.instructions);
|
|
146
|
+
const name = escapeJsonString(ir.name);
|
|
147
|
+
// Header is a `//` line comment using the JSON-escaped name: a raw ir.name in
|
|
148
|
+
// a block comment lets a crafted spec.name containing `*/` (or a newline) break
|
|
149
|
+
// out of the comment and inject top-level code — RCE on build/run (#147).
|
|
150
|
+
const content = `// Generated by @crewhaus/target-onchain (§47 onchain daemon).
|
|
151
|
+
// Compiled from spec: ${name}
|
|
152
|
+
// DO NOT EDIT — re-run \`crewhaus compile\` to regenerate.
|
|
153
|
+
import { classifyBoundary } from "@crewhaus/boundary-classifier";
|
|
154
|
+
import { createEvmAdapter } from "@crewhaus/chain-adapter-evm";
|
|
155
|
+
|
|
156
|
+
export const SPEC_NAME = ${name};
|
|
157
|
+
export const AGENT_MODEL = ${JSON.stringify(ir.agent.model)};
|
|
158
|
+
export const AGENT_INSTRUCTIONS = ${instructions};
|
|
159
|
+
export const IDEMPOTENCY_WINDOW_MS = ${ir.idempotencyWindowMs};
|
|
160
|
+
|
|
161
|
+
export const CHAINS = [
|
|
162
|
+
${chains}
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
export const WALLETS = [
|
|
166
|
+
${wallets}
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
export const CONTRACTS = [
|
|
170
|
+
${contracts}
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
export const TRANSACTION_POLICY = ${policyLiteral};
|
|
174
|
+
|
|
175
|
+
export const TRIGGERS = [
|
|
176
|
+
${triggers}
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Bootstrap the adapter map keyed by chainId. The runtime calls this
|
|
181
|
+
* once on daemon start; downstream tools (tool-evm, tool-evm-tx,
|
|
182
|
+
* permission-tokengated) resolve adapters through the returned map.
|
|
183
|
+
*/
|
|
184
|
+
export function buildAdapters(): Map<string, ReturnType<typeof createEvmAdapter>> {
|
|
185
|
+
const m = new Map<string, ReturnType<typeof createEvmAdapter>>();
|
|
186
|
+
for (const c of CHAINS) {
|
|
187
|
+
m.set(c.chainId, createEvmAdapter(c));
|
|
188
|
+
}
|
|
189
|
+
return m;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Defense-in-depth: re-classify the decoded event payload (the chain
|
|
194
|
+
* adapter already classified the RPC envelope, but this catches any
|
|
195
|
+
* decoded-event content that an attacker may have planted in event
|
|
196
|
+
* indexed args).
|
|
197
|
+
*/
|
|
198
|
+
export async function acceptOrRedact(payload: string): Promise<{ accept: boolean; text: string }> {
|
|
199
|
+
const verdict = await classifyBoundary(payload, { origin: "chain" });
|
|
200
|
+
if (verdict.action === "redact") {
|
|
201
|
+
return { accept: false, text: verdict.redacted ?? "[chain payload redacted]" };
|
|
202
|
+
}
|
|
203
|
+
return { accept: true, text: payload };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Slice-2 emits the adapter wiring + trigger metadata. The full event-
|
|
207
|
+
// subscription loop (poll for new logs, dedupe, dispatch to the agent)
|
|
208
|
+
// is wired in target-onchain v1 once the runtime's runChatLoop accepts
|
|
209
|
+
// the structured payload directly. Until then, the emitted bundle is a
|
|
210
|
+
// callable module that downstream daemons or tests import.
|
|
211
|
+
`;
|
|
212
|
+
return { files: [{ path: "agent.ts", content }] };
|
|
213
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/target-onchain",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Codegen for the §47 `onchain` event-driven daemon. Emits a self-contained agent.ts that subscribes to chain triggers, classifies inbound events, dedupes by tx-hash + log-index, and runs one agent turn per event.",
|
|
6
|
-
"main": "
|
|
7
|
-
"types": "
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
10
13
|
},
|
|
11
14
|
"scripts": {
|
|
12
15
|
"test": "bun test src"
|
|
13
16
|
},
|
|
14
17
|
"dependencies": {
|
|
15
|
-
"@crewhaus/errors": "0.1.
|
|
16
|
-
"@crewhaus/infra-utils": "0.1.
|
|
17
|
-
"@crewhaus/ir": "0.1.
|
|
18
|
+
"@crewhaus/errors": "0.1.6",
|
|
19
|
+
"@crewhaus/infra-utils": "0.1.6",
|
|
20
|
+
"@crewhaus/ir": "0.1.6"
|
|
18
21
|
},
|
|
19
22
|
"license": "Apache-2.0",
|
|
20
23
|
"author": {
|
|
@@ -34,5 +37,5 @@
|
|
|
34
37
|
"publishConfig": {
|
|
35
38
|
"access": "public"
|
|
36
39
|
},
|
|
37
|
-
"files": ["
|
|
40
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
38
41
|
}
|
package/src/index.test.ts
DELETED
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { IrChainV0 } from "@crewhaus/ir";
|
|
3
|
-
import { TargetEmitError, emitOnchain } from "./index";
|
|
4
|
-
|
|
5
|
-
function baseIr(overrides: Partial<IrChainV0> = {}): IrChainV0 {
|
|
6
|
-
return {
|
|
7
|
-
version: 0,
|
|
8
|
-
name: "treasury-watch",
|
|
9
|
-
target: "onchain",
|
|
10
|
-
agent: { model: "claude-opus-4-7", instructions: "Watch the treasury." },
|
|
11
|
-
chains: [
|
|
12
|
-
{
|
|
13
|
-
id: "base-mainnet",
|
|
14
|
-
kind: "evm",
|
|
15
|
-
rpcUrls: [{ kind: "env", name: "BASE_RPC" }],
|
|
16
|
-
rpcPolicy: "single",
|
|
17
|
-
finality: { kind: "confirmations", count: 12 },
|
|
18
|
-
reorgTolerant: true,
|
|
19
|
-
},
|
|
20
|
-
],
|
|
21
|
-
wallets: [],
|
|
22
|
-
contracts: [
|
|
23
|
-
{
|
|
24
|
-
id: "treasury",
|
|
25
|
-
chainId: "base-mainnet",
|
|
26
|
-
address: "0xtreasury",
|
|
27
|
-
abiRef: "abi://safe",
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
transactionPolicy: {
|
|
31
|
-
defaultWriteApproval: "required",
|
|
32
|
-
allowedContracts: [],
|
|
33
|
-
simulationRequired: true,
|
|
34
|
-
},
|
|
35
|
-
triggers: [
|
|
36
|
-
{
|
|
37
|
-
kind: "event",
|
|
38
|
-
chainId: "base-mainnet",
|
|
39
|
-
contract: "treasury",
|
|
40
|
-
event: "ExecutionSuccess",
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
idempotencyWindowMs: 60_000,
|
|
44
|
-
tools: [],
|
|
45
|
-
toolConfigs: {},
|
|
46
|
-
mcp_servers: {},
|
|
47
|
-
permissions: { rules: [] },
|
|
48
|
-
compaction: {},
|
|
49
|
-
...overrides,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe("emitOnchain — happy path", () => {
|
|
54
|
-
test("emits a single agent.ts with the daemon metadata", () => {
|
|
55
|
-
const bundle = emitOnchain(baseIr());
|
|
56
|
-
expect(bundle.files).toHaveLength(1);
|
|
57
|
-
const file = bundle.files[0];
|
|
58
|
-
expect(file?.path).toBe("agent.ts");
|
|
59
|
-
expect(file?.content).toContain('SPEC_NAME = "treasury-watch"');
|
|
60
|
-
expect(file?.content).toContain('AGENT_MODEL = "claude-opus-4-7"');
|
|
61
|
-
expect(file?.content).toContain('AGENT_INSTRUCTIONS = "Watch the treasury."');
|
|
62
|
-
expect(file?.content).toContain("IDEMPOTENCY_WINDOW_MS = 60000");
|
|
63
|
-
expect(file?.content).toContain('chainId: "base-mainnet"');
|
|
64
|
-
expect(file?.content).toContain("createEvmAdapter");
|
|
65
|
-
expect(file?.content).toContain('event: "ExecutionSuccess"');
|
|
66
|
-
expect(file?.content).toContain("classifyBoundary");
|
|
67
|
-
expect(file?.content).toContain('origin: "chain"');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("env-ref secrets render as process.env lookups", () => {
|
|
71
|
-
const bundle = emitOnchain(baseIr());
|
|
72
|
-
const content = bundle.files[0]?.content ?? "";
|
|
73
|
-
expect(content).toContain('process.env["BASE_RPC"]');
|
|
74
|
-
expect(content).toContain("missing required env var BASE_RPC");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("literal secrets render verbatim", () => {
|
|
78
|
-
const ir = baseIr();
|
|
79
|
-
const firstChain = ir.chains[0];
|
|
80
|
-
if (firstChain === undefined) throw new Error("baseIr should produce a chain");
|
|
81
|
-
const bundle = emitOnchain({
|
|
82
|
-
...ir,
|
|
83
|
-
chains: [
|
|
84
|
-
{
|
|
85
|
-
...firstChain,
|
|
86
|
-
rpcUrls: [{ kind: "literal", value: "https://rpc.example.com" }],
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
});
|
|
90
|
-
expect(bundle.files[0]?.content).toContain("https://rpc.example.com");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("renders the three trigger kinds", () => {
|
|
94
|
-
const bundle = emitOnchain(
|
|
95
|
-
baseIr({
|
|
96
|
-
triggers: [
|
|
97
|
-
{
|
|
98
|
-
kind: "event",
|
|
99
|
-
chainId: "base-mainnet",
|
|
100
|
-
contract: "treasury",
|
|
101
|
-
event: "ExecutionSuccess",
|
|
102
|
-
},
|
|
103
|
-
{ kind: "block", chainId: "base-mainnet", scanIntervalMs: 30_000 },
|
|
104
|
-
{
|
|
105
|
-
kind: "address",
|
|
106
|
-
chainId: "base-mainnet",
|
|
107
|
-
address: "0xwhale",
|
|
108
|
-
direction: "both",
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
}),
|
|
112
|
-
);
|
|
113
|
-
const c = bundle.files[0]?.content ?? "";
|
|
114
|
-
expect(c).toContain('kind: "event"');
|
|
115
|
-
expect(c).toContain('kind: "block"');
|
|
116
|
-
expect(c).toContain('kind: "address"');
|
|
117
|
-
expect(c).toContain("scanIntervalMs: 30000");
|
|
118
|
-
expect(c).toContain('direction: "both"');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("renders transaction_policy literal", () => {
|
|
122
|
-
const bundle = emitOnchain(
|
|
123
|
-
baseIr({
|
|
124
|
-
transactionPolicy: {
|
|
125
|
-
defaultWriteApproval: "required",
|
|
126
|
-
allowedContracts: ["treasury"],
|
|
127
|
-
simulationRequired: true,
|
|
128
|
-
maxValueUsd: 5000,
|
|
129
|
-
},
|
|
130
|
-
}),
|
|
131
|
-
);
|
|
132
|
-
const c = bundle.files[0]?.content ?? "";
|
|
133
|
-
expect(c).toContain('"allowedContracts":["treasury"]');
|
|
134
|
-
expect(c).toContain('"maxValueUsd":5000');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// SECURITY: maxValueWei is the ONLY native-value cap wallet-engine can
|
|
138
|
-
// enforce (maxValueUsd hard-throws with no oracle); it must reach the
|
|
139
|
-
// emitted TRANSACTION_POLICY literal so the ceiling is actually applied.
|
|
140
|
-
test("renders transaction_policy.maxValueWei into the emitted literal", () => {
|
|
141
|
-
const bundle = emitOnchain(
|
|
142
|
-
baseIr({
|
|
143
|
-
transactionPolicy: {
|
|
144
|
-
defaultWriteApproval: "policy",
|
|
145
|
-
allowedContracts: [],
|
|
146
|
-
simulationRequired: true,
|
|
147
|
-
maxValueWei: "1000000000000000000",
|
|
148
|
-
},
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
|
-
const c = bundle.files[0]?.content ?? "";
|
|
152
|
-
expect(c).toContain('"maxValueWei":"1000000000000000000"');
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// #151 activation — the policy must carry a resolved contractId -> address
|
|
156
|
-
// map so the wallet-engine can bind tx.to to the declared contract.
|
|
157
|
-
test("populates transaction_policy.contractAddresses from declared contracts[]", () => {
|
|
158
|
-
const bundle = emitOnchain(
|
|
159
|
-
baseIr({
|
|
160
|
-
contracts: [
|
|
161
|
-
{ id: "treasury", chainId: "base-mainnet", address: "0xTREASURY", abiRef: "abi://safe" },
|
|
162
|
-
{ id: "vault", chainId: "base-mainnet", address: "0xVAULT", abiRef: "abi://erc20" },
|
|
163
|
-
],
|
|
164
|
-
}),
|
|
165
|
-
);
|
|
166
|
-
const c = bundle.files[0]?.content ?? "";
|
|
167
|
-
expect(c).toContain('"contractAddresses":{"treasury":"0xTREASURY","vault":"0xVAULT"}');
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("omits contractAddresses when no contracts are declared", () => {
|
|
171
|
-
const bundle = emitOnchain(baseIr({ contracts: [] }));
|
|
172
|
-
const c = bundle.files[0]?.content ?? "";
|
|
173
|
-
expect(c).not.toContain("contractAddresses");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// #159 (CWE-798) — a wallet keyRef that is an env ref or a kms:// / hsm://
|
|
177
|
-
// handle is fine; a bare literal (esp. a raw hex private key) must not be
|
|
178
|
-
// baked into the emitted artifact.
|
|
179
|
-
test("renders a wallet env-ref keyRef as a process.env lookup", () => {
|
|
180
|
-
const bundle = emitOnchain(
|
|
181
|
-
baseIr({
|
|
182
|
-
wallets: [
|
|
183
|
-
{
|
|
184
|
-
id: "treasurer",
|
|
185
|
-
chainId: "base-mainnet",
|
|
186
|
-
custody: "kms",
|
|
187
|
-
signingPolicy: "policy-gated",
|
|
188
|
-
keyRef: { kind: "env", name: "TREASURER_KEY" },
|
|
189
|
-
},
|
|
190
|
-
],
|
|
191
|
-
}),
|
|
192
|
-
);
|
|
193
|
-
const c = bundle.files[0]?.content ?? "";
|
|
194
|
-
expect(c).toContain('process.env["TREASURER_KEY"]');
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("renders a kms:// keyRef handle verbatim", () => {
|
|
198
|
-
const bundle = emitOnchain(
|
|
199
|
-
baseIr({
|
|
200
|
-
wallets: [
|
|
201
|
-
{
|
|
202
|
-
id: "treasurer",
|
|
203
|
-
chainId: "base-mainnet",
|
|
204
|
-
custody: "kms",
|
|
205
|
-
signingPolicy: "policy-gated",
|
|
206
|
-
keyRef: { kind: "literal", value: "kms://aws/treasurer-key" },
|
|
207
|
-
},
|
|
208
|
-
],
|
|
209
|
-
}),
|
|
210
|
-
);
|
|
211
|
-
const c = bundle.files[0]?.content ?? "";
|
|
212
|
-
expect(c).toContain('"kms://aws/treasurer-key"');
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("rejects a literal (hex private key) wallet keyRef", () => {
|
|
216
|
-
expect(() =>
|
|
217
|
-
emitOnchain(
|
|
218
|
-
baseIr({
|
|
219
|
-
wallets: [
|
|
220
|
-
{
|
|
221
|
-
id: "treasurer",
|
|
222
|
-
chainId: "base-mainnet",
|
|
223
|
-
custody: "local",
|
|
224
|
-
signingPolicy: "automated",
|
|
225
|
-
keyRef: {
|
|
226
|
-
kind: "literal",
|
|
227
|
-
value: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
],
|
|
231
|
-
}),
|
|
232
|
-
),
|
|
233
|
-
).toThrow(TargetEmitError);
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
describe("emitOnchain — validation", () => {
|
|
238
|
-
test("rejects empty chains[]", () => {
|
|
239
|
-
expect(() => emitOnchain(baseIr({ chains: [] }))).toThrow(TargetEmitError);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("rejects empty triggers[]", () => {
|
|
243
|
-
expect(() => emitOnchain(baseIr({ triggers: [] }))).toThrow(TargetEmitError);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test("rejects a trigger that references an undeclared chainId", () => {
|
|
247
|
-
expect(() =>
|
|
248
|
-
emitOnchain(
|
|
249
|
-
baseIr({
|
|
250
|
-
triggers: [{ kind: "block", chainId: "polygon-mainnet", scanIntervalMs: 10_000 }],
|
|
251
|
-
}),
|
|
252
|
-
),
|
|
253
|
-
).toThrow(/not declared in chains/);
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
describe("emitOnchain — spec-name codegen injection (#147)", () => {
|
|
258
|
-
test("a crafted name cannot break out of the header comment", () => {
|
|
259
|
-
// Block-comment escape: a `*/` (plus a newline) in the name would, with a
|
|
260
|
-
// raw `/* … */` header, terminate the comment and inject top-level code.
|
|
261
|
-
const evil = "safe */ globalThis.__PWNED_ONCHAIN__ = 1; /*\nmore";
|
|
262
|
-
const content = emitOnchain(baseIr({ name: evil })).files[0]?.content ?? "";
|
|
263
|
-
// Header is now a `//` line comment using the JSON-escaped name.
|
|
264
|
-
expect(content).toMatch(/^\/\/ Compiled from spec:/m);
|
|
265
|
-
// The payload only ever appears inside the escaped SPEC_NAME string and the
|
|
266
|
-
// comment — never as a top-level statement at column 0.
|
|
267
|
-
expect(content).not.toMatch(/^globalThis\.__PWNED_ONCHAIN__/m);
|
|
268
|
-
expect(content).not.toContain("*/\nglobalThis");
|
|
269
|
-
});
|
|
270
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Catalog F2 `target-onchain` — §47.
|
|
3
|
-
*
|
|
4
|
-
* Codegen for the `onchain` event-driven daemon. The emitted bundle is
|
|
5
|
-
* a single `agent.ts` file that:
|
|
6
|
-
*
|
|
7
|
-
* 1. Constructs the configured chain adapters from `chains[]`
|
|
8
|
-
* (slice-0 `@crewhaus/chain-adapter-evm` is the only adapter
|
|
9
|
-
* shipped today).
|
|
10
|
-
* 2. Constructs the wallet engine when `wallets[]` is non-empty
|
|
11
|
-
* (uses the `LocalSignerStub` only when an explicit `local`
|
|
12
|
-
* custody wallet is declared; KMS/HSM bridges are pluggable).
|
|
13
|
-
* 3. Subscribes to every trigger in `triggers[]` — event triggers
|
|
14
|
-
* poll via `eth_getLogs`, block triggers via `eth_blockNumber`,
|
|
15
|
-
* address triggers via filtered `eth_getLogs` on Transfer-shaped
|
|
16
|
-
* topics.
|
|
17
|
-
* 4. Dedupes inbound events by `(txHash, logIndex)` (or block
|
|
18
|
-
* number for block triggers) within `idempotencyWindowMs`.
|
|
19
|
-
* 5. Classifies the decoded event payload via
|
|
20
|
-
* `classifyBoundary({origin: "chain"})` before injecting into
|
|
21
|
-
* the model context (defense in depth — the chain adapter
|
|
22
|
-
* already classified the RPC body).
|
|
23
|
-
* 6. Runs one `runChatLoop({singleTurn: true})` per accepted
|
|
24
|
-
* event with the decoded payload as the user message.
|
|
25
|
-
* 7. Emits JSON events to stdout for each trigger fire +
|
|
26
|
-
* agent-run-start / agent-run-end.
|
|
27
|
-
* 8. Installs SIGTERM/SIGINT handlers that complete in-flight
|
|
28
|
-
* handlers before exit.
|
|
29
|
-
*
|
|
30
|
-
* Slice 2 ships the codegen — the emitted file references runtime
|
|
31
|
-
* packages by name. The first integration test compiles a fixture
|
|
32
|
-
* spec to a bundle, asserts the file contents, and (in a follow-up
|
|
33
|
-
* slice) boots the bundle against an in-process anvil fork.
|
|
34
|
-
*/
|
|
35
|
-
import { CrewhausError } from "@crewhaus/errors";
|
|
36
|
-
import { escapeJsonString } from "@crewhaus/infra-utils";
|
|
37
|
-
import type { Bundle, IrChainV0, IrSecretRef } from "@crewhaus/ir";
|
|
38
|
-
|
|
39
|
-
export class TargetEmitError extends CrewhausError {
|
|
40
|
-
override readonly name = "TargetEmitError";
|
|
41
|
-
constructor(message: string, cause?: unknown) {
|
|
42
|
-
super("compiler", message, cause);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function renderSecretRef(ref: IrSecretRef): string {
|
|
47
|
-
if (ref.kind === "literal") return JSON.stringify(ref.value);
|
|
48
|
-
return `process.env[${JSON.stringify(ref.name)}] ?? (() => { throw new Error(${JSON.stringify(
|
|
49
|
-
`missing required env var ${ref.name}`,
|
|
50
|
-
)}); })()`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* #159 (CWE-798) — a wallet signing key must never be a bare literal in
|
|
55
|
-
* the emitted DO-NOT-EDIT artifact. The compiler's `lowerWalletKeyRef`
|
|
56
|
-
* already rejects literal keyRefs at lower time; this is the
|
|
57
|
-
* defense-in-depth guard at the emit boundary (e.g. for a hand-built IR
|
|
58
|
-
* that bypassed the lowerer). Env refs pass; only `kms://` / `hsm://`
|
|
59
|
-
* key handles are permitted as literals.
|
|
60
|
-
*/
|
|
61
|
-
const KEY_HANDLE_RE = /^(kms|hsm):\/\/.+/;
|
|
62
|
-
function renderWalletKeyRef(walletId: string, ref: IrSecretRef): string {
|
|
63
|
-
if (ref.kind === "literal" && !KEY_HANDLE_RE.test(ref.value)) {
|
|
64
|
-
throw new TargetEmitError(
|
|
65
|
-
`wallet "${walletId}" keyRef is a literal signing key; refusing to embed it in the generated bundle. Use an environment reference or a kms:// / hsm:// handle.`,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
return renderSecretRef(ref);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function renderChain(chain: IrChainV0["chains"][number]): string {
|
|
72
|
-
const rpcs = chain.rpcUrls.map(renderSecretRef).join(", ");
|
|
73
|
-
const finality =
|
|
74
|
-
chain.finality.kind === "confirmations"
|
|
75
|
-
? `{ kind: "confirmations", count: ${chain.finality.count} }`
|
|
76
|
-
: `{ kind: ${JSON.stringify(chain.finality.kind)} }`;
|
|
77
|
-
return [
|
|
78
|
-
" {",
|
|
79
|
-
` chainId: ${JSON.stringify(chain.id)},`,
|
|
80
|
-
` rpcUrls: [${rpcs}],`,
|
|
81
|
-
` rpcPolicy: ${JSON.stringify(chain.rpcPolicy)},`,
|
|
82
|
-
` finality: ${finality},`,
|
|
83
|
-
` reorgTolerant: ${chain.reorgTolerant},`,
|
|
84
|
-
" }",
|
|
85
|
-
].join("\n");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function renderWallet(w: IrChainV0["wallets"][number]): string {
|
|
89
|
-
const keyRefStr =
|
|
90
|
-
w.keyRef !== undefined ? `,\n keyRef: ${renderWalletKeyRef(w.id, w.keyRef)}` : "";
|
|
91
|
-
return [
|
|
92
|
-
" {",
|
|
93
|
-
` id: ${JSON.stringify(w.id)},`,
|
|
94
|
-
` chainId: ${JSON.stringify(w.chainId)},`,
|
|
95
|
-
` custody: ${JSON.stringify(w.custody)},`,
|
|
96
|
-
` signingPolicy: ${JSON.stringify(w.signingPolicy)}${keyRefStr ? "" : ""}`,
|
|
97
|
-
...(keyRefStr ? [keyRefStr.replace(/^,\n/, "")] : []),
|
|
98
|
-
" }",
|
|
99
|
-
].join("\n");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function renderContract(c: IrChainV0["contracts"][number]): string {
|
|
103
|
-
return [
|
|
104
|
-
" {",
|
|
105
|
-
` id: ${JSON.stringify(c.id)},`,
|
|
106
|
-
` chainId: ${JSON.stringify(c.chainId)},`,
|
|
107
|
-
` address: ${JSON.stringify(c.address)},`,
|
|
108
|
-
` abiRef: ${JSON.stringify(c.abiRef)},`,
|
|
109
|
-
" }",
|
|
110
|
-
].join("\n");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function renderTrigger(t: IrChainV0["triggers"][number]): string {
|
|
114
|
-
if (t.kind === "event") {
|
|
115
|
-
const filter = t.filter !== undefined ? `,\n filter: ${JSON.stringify(t.filter)}` : "";
|
|
116
|
-
return ` { kind: "event", chainId: ${JSON.stringify(t.chainId)}, contract: ${JSON.stringify(t.contract)}, event: ${JSON.stringify(t.event)}${filter} }`;
|
|
117
|
-
}
|
|
118
|
-
if (t.kind === "block") {
|
|
119
|
-
return ` { kind: "block", chainId: ${JSON.stringify(t.chainId)}, scanIntervalMs: ${t.scanIntervalMs} }`;
|
|
120
|
-
}
|
|
121
|
-
return ` { kind: "address", chainId: ${JSON.stringify(t.chainId)}, address: ${JSON.stringify(t.address)}, direction: ${JSON.stringify(t.direction)} }`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function emitOnchain(ir: IrChainV0): Bundle {
|
|
125
|
-
if (ir.chains.length === 0) {
|
|
126
|
-
throw new TargetEmitError("onchain target requires at least one chain binding");
|
|
127
|
-
}
|
|
128
|
-
if (ir.triggers.length === 0) {
|
|
129
|
-
throw new TargetEmitError("onchain target requires at least one trigger");
|
|
130
|
-
}
|
|
131
|
-
// Validate trigger references against chains[].
|
|
132
|
-
const chainIds = new Set(ir.chains.map((c) => c.id));
|
|
133
|
-
for (const t of ir.triggers) {
|
|
134
|
-
if (!chainIds.has(t.chainId)) {
|
|
135
|
-
throw new TargetEmitError(
|
|
136
|
-
`trigger references chainId "${t.chainId}" not declared in chains[]`,
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const policy = ir.transactionPolicy;
|
|
142
|
-
// #151 — resolve the declared contracts[] (id -> address) into the policy so
|
|
143
|
-
// the wallet-engine can bind `tx.to` to the address registered for a claimed
|
|
144
|
-
// contractId. Without this map a whitelisted id can still be pointed at an
|
|
145
|
-
// arbitrary address.
|
|
146
|
-
const contractAddresses: Record<string, string> = {};
|
|
147
|
-
for (const c of ir.contracts) {
|
|
148
|
-
contractAddresses[c.id] = c.address;
|
|
149
|
-
}
|
|
150
|
-
const policyLiteral = JSON.stringify({
|
|
151
|
-
defaultWriteApproval: policy.defaultWriteApproval,
|
|
152
|
-
allowedContracts: policy.allowedContracts,
|
|
153
|
-
simulationRequired: policy.simulationRequired,
|
|
154
|
-
...(policy.maxValueUsd !== undefined ? { maxValueUsd: policy.maxValueUsd } : {}),
|
|
155
|
-
...(policy.maxValueWei !== undefined ? { maxValueWei: policy.maxValueWei } : {}),
|
|
156
|
-
...(ir.contracts.length > 0 ? { contractAddresses } : {}),
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const chains = ir.chains.map(renderChain).join(",\n");
|
|
160
|
-
const wallets = ir.wallets.map(renderWallet).join(",\n");
|
|
161
|
-
const contracts = ir.contracts.map(renderContract).join(",\n");
|
|
162
|
-
const triggers = ir.triggers.map(renderTrigger).join(",\n");
|
|
163
|
-
|
|
164
|
-
const instructions = escapeJsonString(ir.agent.instructions);
|
|
165
|
-
const name = escapeJsonString(ir.name);
|
|
166
|
-
|
|
167
|
-
// Header is a `//` line comment using the JSON-escaped name: a raw ir.name in
|
|
168
|
-
// a block comment lets a crafted spec.name containing `*/` (or a newline) break
|
|
169
|
-
// out of the comment and inject top-level code — RCE on build/run (#147).
|
|
170
|
-
const content = `// Generated by @crewhaus/target-onchain (§47 onchain daemon).
|
|
171
|
-
// Compiled from spec: ${name}
|
|
172
|
-
// DO NOT EDIT — re-run \`crewhaus compile\` to regenerate.
|
|
173
|
-
import { classifyBoundary } from "@crewhaus/boundary-classifier";
|
|
174
|
-
import { createEvmAdapter } from "@crewhaus/chain-adapter-evm";
|
|
175
|
-
|
|
176
|
-
export const SPEC_NAME = ${name};
|
|
177
|
-
export const AGENT_MODEL = ${JSON.stringify(ir.agent.model)};
|
|
178
|
-
export const AGENT_INSTRUCTIONS = ${instructions};
|
|
179
|
-
export const IDEMPOTENCY_WINDOW_MS = ${ir.idempotencyWindowMs};
|
|
180
|
-
|
|
181
|
-
export const CHAINS = [
|
|
182
|
-
${chains}
|
|
183
|
-
];
|
|
184
|
-
|
|
185
|
-
export const WALLETS = [
|
|
186
|
-
${wallets}
|
|
187
|
-
];
|
|
188
|
-
|
|
189
|
-
export const CONTRACTS = [
|
|
190
|
-
${contracts}
|
|
191
|
-
];
|
|
192
|
-
|
|
193
|
-
export const TRANSACTION_POLICY = ${policyLiteral};
|
|
194
|
-
|
|
195
|
-
export const TRIGGERS = [
|
|
196
|
-
${triggers}
|
|
197
|
-
];
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Bootstrap the adapter map keyed by chainId. The runtime calls this
|
|
201
|
-
* once on daemon start; downstream tools (tool-evm, tool-evm-tx,
|
|
202
|
-
* permission-tokengated) resolve adapters through the returned map.
|
|
203
|
-
*/
|
|
204
|
-
export function buildAdapters(): Map<string, ReturnType<typeof createEvmAdapter>> {
|
|
205
|
-
const m = new Map<string, ReturnType<typeof createEvmAdapter>>();
|
|
206
|
-
for (const c of CHAINS) {
|
|
207
|
-
m.set(c.chainId, createEvmAdapter(c));
|
|
208
|
-
}
|
|
209
|
-
return m;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Defense-in-depth: re-classify the decoded event payload (the chain
|
|
214
|
-
* adapter already classified the RPC envelope, but this catches any
|
|
215
|
-
* decoded-event content that an attacker may have planted in event
|
|
216
|
-
* indexed args).
|
|
217
|
-
*/
|
|
218
|
-
export async function acceptOrRedact(payload: string): Promise<{ accept: boolean; text: string }> {
|
|
219
|
-
const verdict = await classifyBoundary(payload, { origin: "chain" });
|
|
220
|
-
if (verdict.action === "redact") {
|
|
221
|
-
return { accept: false, text: verdict.redacted ?? "[chain payload redacted]" };
|
|
222
|
-
}
|
|
223
|
-
return { accept: true, text: payload };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Slice-2 emits the adapter wiring + trigger metadata. The full event-
|
|
227
|
-
// subscription loop (poll for new logs, dedupe, dispatch to the agent)
|
|
228
|
-
// is wired in target-onchain v1 once the runtime's runChatLoop accepts
|
|
229
|
-
// the structured payload directly. Until then, the emitted bundle is a
|
|
230
|
-
// callable module that downstream daemons or tests import.
|
|
231
|
-
`;
|
|
232
|
-
|
|
233
|
-
return { files: [{ path: "agent.ts", content }] };
|
|
234
|
-
}
|