@upoch/guard 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Upoch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,72 @@
1
- # Temporary Holding Version
1
+ # @upoch/guard: Upoch Guard
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Give your AI agent a memory and an undo button.
4
+
5
+ Upoch Guard saves verifiable checkpoints of an agent's work on Robinhood Chain. If the agent crashes, gets a bad
6
+ prompt or makes a bad trade, it restores the last good state, checked against the chain, or rolls back to the
7
+ checkpoint before the mistake and keeps going. It also knows which of its actions already happened.
8
+
9
+ This package is the `upoch` command you set Upoch up with, and the engine behind the Upoch framework plugins:
10
+
11
+ - **ElizaOS**: [@upoch/plugin-elizaos](https://www.npmjs.com/package/@upoch/plugin-elizaos)
12
+
13
+ Website and debugger: https://upoch.tech. Contract (Robinhood Chain, 4663): `0x6179d9123c3c1f9e82f948101a4350cb55aa8c27`
14
+
15
+ ## Setup
16
+
17
+ Needs Node.js 24 or later. In your own terminal:
18
+
19
+ ```sh
20
+ npm install -g @upoch/guard
21
+ upoch setup
22
+ ```
23
+
24
+ `upoch setup` creates your keys in `~/.upoch`, shows the owner address, and waits while you send it a little ETH on
25
+ Robinhood Chain (0.001 ETH is plenty). Then it registers your agent. After that, connecting an agent is one line,
26
+ for example `plugins: ["@upoch/plugin-elizaos"]` in an ElizaOS character.
27
+
28
+ Prefer separate steps? Run `upoch init`, then `upoch register`.
29
+
30
+ ## How it works
31
+
32
+ Each checkpoint is encrypted on your machine and stored on IPFS. Its fingerprint is committed to the Upoch contract,
33
+ and then the checkpoint is read back and verified. Only someone with your checkpoint key can read it, and anyone
34
+ can check that it has not been changed. Every checkpoint opens in the
35
+ [Time-Travel Debugger](https://upoch.tech/docs/debugger).
36
+
37
+ Two keys are created on your machine and never leave it:
38
+
39
+ - **Owner**: registers the agent and approves rollbacks. You fund it once.
40
+ - **Executor**: the agent's key. It can only submit checkpoints, so the agent never holds the owner key.
41
+
42
+ A rollback restores the agent's state and memory. It cannot reverse transactions that already happened on-chain;
43
+ those are listed so the agent can account for them.
44
+
45
+ ## Commands
46
+
47
+ ```sh
48
+ upoch setup # keys, funding, registration, in one go
49
+ upoch status # workflow, funds and today's usage
50
+ upoch key # print the checkpoint key, to open checkpoints in the debugger
51
+ upoch export-owner # print the owner private key, to import it into a wallet
52
+ ```
53
+
54
+ Settings live in `UPOCH_HOME` (default `~/.upoch`).
55
+
56
+ ## Safety
57
+
58
+ - **Secrets are refused.** State, notes or actions that look like private keys, seed phrases, passwords or API
59
+ tokens are rejected before anything is stored, because checkpoints are permanent.
60
+ - **Daily limit.** At most 100 checkpoints per UTC day by default (`maxCheckpointsPerDay` in `~/.upoch/config.json`).
61
+ - **Terminal-only commands.** `upoch setup`, `init`, `register`, `key` and `export-owner` run only in an interactive
62
+ terminal, so an agent cannot run them.
63
+ - **Keys are stored unencrypted** in `~/.upoch/secrets.json` (owner-only permissions on macOS and Linux). Keep only
64
+ a small amount of ETH on these keys, and back up the file: without it, nobody, including Upoch, can open your
65
+ checkpoints.
66
+ - Upoch Guard signs and sends only checkpoint transactions from the executor and, during setup, the registration
67
+ and funding transactions from the owner.
68
+ - The Upoch contracts have not been audited.
69
+
70
+ ## License
71
+
72
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ // upoch: the Upoch Guard command line.
3
+ // upoch setup init + wait for funding + register + connect, in one go (your terminal only)
4
+ // upoch init create keys and settings (your terminal only)
5
+ // upoch register register the agent's workflow on-chain and fund its executor (your terminal only)
6
+ // upoch status show the workflow, funds and usage
7
+ // upoch mcp run the MCP server (stdio) for Claude Code, Cursor, Codex and other MCP clients (see LAUNCHED)
8
+ // upoch key print the checkpoint key, to open checkpoints in the debugger (your terminal only)
9
+ // upoch export-owner print the owner private key, to import it into a wallet (your terminal only)
10
+ import { spawnSync } from "node:child_process";
11
+ import { stdin, stdout } from "node:process";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { formatEther } from "viem";
14
+ import { privateKeyToAccount } from "viem/accounts";
15
+ import { hasSetup, loadConfig, loadSecrets, networkInfo, upochHome } from "./config.js";
16
+ import { Guard } from "./guard.js";
17
+ import { createServer, VERSION } from "./server.js";
18
+ import { initialize, register, registrationPlan } from "./setup.js";
19
+ /**
20
+ * Integrations announced so far. The MCP server (`upoch mcp`) and LangChain already work; help and setup output list
21
+ * them only once they launch. Flip these at launch.
22
+ */
23
+ const LAUNCHED = { mcp: false, langchain: false };
24
+ const CONNECT = [
25
+ ...(LAUNCHED.mcp ? [" Claude Code claude mcp add upoch -- upoch mcp", " Cursor, Codex add an MCP server with the command: upoch mcp"] : []),
26
+ ' ElizaOS plugins: ["@upoch/plugin-elizaos"] in your character',
27
+ ...(LAUNCHED.langchain ? [" LangChain callbacks: [new UpochCallbackHandler()] from @upoch/langchain"] : []),
28
+ ].join("\n");
29
+ const HELP = `Upoch Guard ${VERSION}: verifiable checkpoints, recovery and rollback for AI agents on Robinhood Chain.
30
+
31
+ Setup, in your own terminal:
32
+ upoch setup everything in one go: keys, funding, registration
33
+ or step by step:
34
+ upoch init create keys and settings in ${upochHome()}
35
+ upoch register register your agent on-chain and fund its executor
36
+
37
+ Then connect your agent with one line:
38
+ ${CONNECT}
39
+
40
+ Other commands:
41
+ upoch status workflow, funds and today's usage
42
+ ${LAUNCHED.mcp ? " upoch mcp run the MCP server (stdio)\n" : ""} upoch key print the checkpoint key (to open checkpoints in the debugger)
43
+ upoch export-owner print the owner private key (to import it into a wallet)
44
+
45
+ Settings live in UPOCH_HOME (default ~/.upoch). Keys never leave this machine.`;
46
+ function ask(q, hidden = false) {
47
+ return new Promise((resolve) => {
48
+ stdout.write(q);
49
+ let buf = "";
50
+ const raw = stdin.isRaw;
51
+ if (stdin.isTTY)
52
+ stdin.setRawMode(true);
53
+ stdin.resume();
54
+ stdin.setEncoding("utf8");
55
+ const onData = (s) => {
56
+ for (const ch of s) {
57
+ if (ch === "\r" || ch === "\n") {
58
+ stdin.off("data", onData);
59
+ if (stdin.isTTY)
60
+ stdin.setRawMode(raw);
61
+ stdin.pause();
62
+ stdout.write("\n");
63
+ return resolve(buf.trim());
64
+ }
65
+ if (ch === "\u0003")
66
+ process.exit(130);
67
+ if (ch === "\u007f" || ch === "\b")
68
+ buf = buf.slice(0, -1);
69
+ else {
70
+ buf += ch;
71
+ if (!hidden)
72
+ stdout.write(ch);
73
+ }
74
+ }
75
+ };
76
+ stdin.on("data", onData);
77
+ });
78
+ }
79
+ /** Commands that create, move or reveal keys run only in an interactive terminal, so an agent cannot run them. */
80
+ function requireTerminal(cmd) {
81
+ if (!stdin.isTTY || !stdout.isTTY) {
82
+ console.error(`"upoch ${cmd}" must be run by you in an interactive terminal, not by an agent or a script.`);
83
+ process.exit(1);
84
+ }
85
+ }
86
+ /** A numbered choice. With `required`, an empty answer asks again instead of taking option 1. */
87
+ async function choose(q, options, required = false) {
88
+ console.log(q);
89
+ options.forEach(([, label], i) => console.log(` ${i + 1}. ${label}`));
90
+ for (;;) {
91
+ const a = await ask(required ? `Type 1-${options.length}: ` : `Choose 1-${options.length} [1]: `);
92
+ if (a === "" && required)
93
+ continue;
94
+ const i = a === "" ? 0 : Number(a) - 1;
95
+ if (options[i])
96
+ return options[i][0];
97
+ }
98
+ }
99
+ async function cmdInit() {
100
+ requireTerminal("init");
101
+ if (hasSetup())
102
+ throw new Error(`Upoch is already set up in ${upochHome()}.`);
103
+ console.log(`Setting up Upoch Guard in ${upochHome()}\n`);
104
+ const network = await choose("Network:", [["mainnet", "Robinhood Chain mainnet (real ETH)"], ["testnet", "Robinhood Chain testnet (free test ETH, for trying it out)"]], true);
105
+ console.log(network === "mainnet" ? " -> MAINNET: registration and checkpoints use real ETH." : " -> testnet.");
106
+ const agentName = (await ask("Agent name (public, on-chain) [My agent]: ")) || "My agent";
107
+ const where = await choose("Where should encrypted checkpoints be stored?", [
108
+ ["filebase", "IPFS through Filebase (free account at filebase.com; needs an IPFS RPC API token)"],
109
+ ["kubo", "Another IPFS pinning service or node with a Kubo RPC API"],
110
+ ["local", "A folder on this computer (testing only: the debugger cannot read it)"],
111
+ ]);
112
+ let storage;
113
+ let ipfsAuthorization;
114
+ if (where === "filebase") {
115
+ const token = await ask("Filebase IPFS RPC API token (hidden): ", true);
116
+ if (!token)
117
+ throw new Error("a token is required");
118
+ storage = { kind: "ipfs", apiUrl: "https://rpc.filebase.io", gatewayUrl: "https://ipfs.filebase.io" };
119
+ ipfsAuthorization = `Bearer ${token}`;
120
+ }
121
+ else if (where === "kubo") {
122
+ const apiUrl = await ask("Kubo RPC API URL (e.g. http://127.0.0.1:5001): ");
123
+ const gatewayUrl = (await ask("Gateway URL for reading (optional): ")) || undefined;
124
+ const auth = await ask("Authorization header value (optional, hidden): ", true);
125
+ storage = { kind: "ipfs", apiUrl, ...(gatewayUrl ? { gatewayUrl } : {}) };
126
+ ipfsAuthorization = auth || undefined;
127
+ }
128
+ else {
129
+ const dir = (await ask(`Folder [${upochHome()}/checkpoints]: `)) || `${upochHome()}/checkpoints`;
130
+ storage = { kind: "local", dir };
131
+ }
132
+ const r = initialize({ network, agentName, storage, ipfsAuthorization });
133
+ console.log(`
134
+ Done. Keys are in ${r.home}/secrets.json. Back that file up: without it, nobody (including Upoch) can open your checkpoints.
135
+
136
+ Owner ${r.owner} registers the agent and approves rollbacks
137
+ Executor ${r.executor} the agent's key; it only sends checkpoints`);
138
+ if (!chained)
139
+ console.log(`\nNext: send a little ETH on ${network === "mainnet" ? "Robinhood Chain" : "Robinhood Chain testnet"} to the owner address (0.001 ETH is plenty), then run:\n upoch register`);
140
+ }
141
+ /** Set while `upoch setup` runs init and register in one go. */
142
+ let chained = false;
143
+ function fundArg(args) {
144
+ const i = args.indexOf("--fund");
145
+ return i >= 0 && args[i + 1] ? args[i + 1] : "0.0003";
146
+ }
147
+ /** init, wait for the owner to be funded, register, connect: everything in one command. */
148
+ async function cmdSetup(args) {
149
+ requireTerminal("setup");
150
+ chained = true;
151
+ if (!hasSetup())
152
+ await cmdInit();
153
+ if (!loadConfig().workflowId) {
154
+ const fundEth = fundArg(args);
155
+ let p = await registrationPlan({ fundEth });
156
+ if (!p.enough) {
157
+ console.log(`\nSend at least ${formatEther(p.cost - p.balance)} ETH on ${p.network} to the owner (0.001 ETH is plenty):\n\n ${p.owner}\n\nWaiting for it to arrive (Ctrl+C to stop; run "upoch setup" again to continue later)…`);
158
+ while (!p.enough) {
159
+ await new Promise((r) => setTimeout(r, 5000));
160
+ p = await registrationPlan({ fundEth });
161
+ }
162
+ console.log(`Received: the owner now has ${formatEther(p.balance)} ETH.\n`);
163
+ }
164
+ await cmdRegister(args);
165
+ }
166
+ else {
167
+ console.log(`Already registered as workflow #${loadConfig().workflowId}.`);
168
+ }
169
+ await connectClients();
170
+ }
171
+ /** Offers to add the MCP server to Claude Code when its CLI is installed, and prints the line for everything else. */
172
+ async function connectClients() {
173
+ const claude = LAUNCHED.mcp ? spawnSync("claude", ["--version"], { stdio: "ignore", shell: process.platform === "win32" }) : null;
174
+ if (claude?.status === 0 && (await ask("\nAdd Upoch to Claude Code now? [Y/n]: ")).toLowerCase() !== "n") {
175
+ const r = spawnSync("claude", ["mcp", "add", "upoch", "--scope", "user", "--", "upoch", "mcp"], { stdio: "inherit", shell: process.platform === "win32" });
176
+ console.log(r.status === 0 ? "Added. Restart Claude Code to load it." : "Could not add it automatically; run the line below.");
177
+ }
178
+ console.log(`\nConnect your agent with one line:\n${CONNECT}`);
179
+ }
180
+ async function cmdRegister(args) {
181
+ requireTerminal("register");
182
+ const fundEth = fundArg(args);
183
+ const p = await registrationPlan({ fundEth });
184
+ const mainnet = loadConfig().network === "mainnet";
185
+ console.log(`Register "${loadConfig().agentName}" on ${p.network}${mainnet ? " MAINNET (real ETH)" : ""}:`);
186
+ console.log(` 1. The owner ${p.owner} registers a workflow with the executor ${p.executor}`);
187
+ console.log(` 2. The owner sends ${formatEther(p.fund)} ETH to the executor for checkpoint gas`);
188
+ console.log(` Owner balance ${formatEther(p.balance)} ETH; this needs about ${formatEther(p.cost)} ETH (gas ${Number(p.gasPrice) / 1e9} gwei).`);
189
+ if (!p.enough)
190
+ throw new Error(`Not enough ETH. Send at least ${formatEther(p.cost - p.balance)} more ETH to ${p.owner} and run this again.`);
191
+ if ((await ask('Type "yes" to send: ')).toLowerCase() !== "yes")
192
+ return console.log("Nothing sent.");
193
+ const r = await register({ fundEth });
194
+ console.log(`\nRegistered workflow #${r.workflowId} (${r.registerTx}).`);
195
+ if (r.fundTx)
196
+ console.log(`Executor funded (${r.fundTx}).`);
197
+ if (r.debugger)
198
+ console.log(`Debugger: ${r.debugger}`);
199
+ if (!chained)
200
+ console.log(`\nConnect your agent with one line:\n${CONNECT}`);
201
+ }
202
+ async function cmdStatus() {
203
+ const s = await new Guard().status();
204
+ console.log(JSON.stringify(s, null, 2));
205
+ }
206
+ async function cmdMcp() {
207
+ const server = createServer(() => new Guard());
208
+ await server.connect(new StdioServerTransport());
209
+ }
210
+ function cmdKey() {
211
+ requireTerminal("key");
212
+ const s = loadSecrets();
213
+ const c = loadConfig();
214
+ console.log(`Checkpoint key for "${c.agentName}" (keep it private; anyone with it can read your checkpoints):\n\n ${s.encryptionKey}\n`);
215
+ const url = c.workflowId ? networkInfo(c).debuggerUrl?.(c.workflowId) : undefined;
216
+ if (url)
217
+ console.log(`Paste it in the debugger to open checkpoints: ${url}`);
218
+ }
219
+ async function cmdExportOwner() {
220
+ requireTerminal("export-owner");
221
+ const s = loadSecrets();
222
+ console.log(`This prints the private key of the owner ${privateKeyToAccount(s.ownerKey).address}. Anyone who sees it controls the workflow.`);
223
+ if ((await ask('Type "show" to print it: ')).toLowerCase() !== "show")
224
+ return;
225
+ console.log(`\n ${s.ownerKey}\n`);
226
+ }
227
+ async function main() {
228
+ const [cmd, ...args] = process.argv.slice(2);
229
+ switch (cmd) {
230
+ case "setup":
231
+ return cmdSetup(args);
232
+ case "init":
233
+ return cmdInit();
234
+ case "register":
235
+ return cmdRegister(args);
236
+ case "status":
237
+ return cmdStatus();
238
+ case "mcp":
239
+ return cmdMcp();
240
+ case "key":
241
+ return cmdKey();
242
+ case "export-owner":
243
+ return cmdExportOwner();
244
+ case "--version":
245
+ case "-v":
246
+ return console.log(VERSION);
247
+ default:
248
+ console.log(HELP);
249
+ if (cmd && cmd !== "help" && cmd !== "--help" && cmd !== "-h")
250
+ process.exitCode = 1;
251
+ }
252
+ }
253
+ main().catch((e) => {
254
+ console.error(e instanceof Error ? e.message : String(e));
255
+ process.exitCode = 1;
256
+ });
@@ -0,0 +1,60 @@
1
+ export type NetworkName = "mainnet" | "testnet" | "custom";
2
+ export interface StorageConfig {
3
+ kind: "ipfs" | "local";
4
+ /** IPFS (Kubo RPC API), e.g. https://rpc.filebase.io. */
5
+ apiUrl?: string;
6
+ gatewayUrl?: string;
7
+ /** Local folder (development only: the debugger on upoch.tech cannot read it). */
8
+ dir?: string;
9
+ }
10
+ export interface UpochConfig {
11
+ network: NetworkName;
12
+ /** Only for "custom" (a local chain in tests); mainnet and testnet use the published deployments. */
13
+ custom?: {
14
+ rpcUrl: string;
15
+ ledger: `0x${string}`;
16
+ chainId: number;
17
+ startBlock?: string;
18
+ };
19
+ /** Set by `upoch register`. */
20
+ workflowId?: string;
21
+ agentId: string;
22
+ agentName: string;
23
+ storage: StorageConfig;
24
+ /** Each checkpoint is an on-chain transaction; this caps how many the agent can send per UTC day. */
25
+ maxCheckpointsPerDay: number;
26
+ }
27
+ export interface UpochSecrets {
28
+ ownerKey: `0x${string}`;
29
+ executorKey: `0x${string}`;
30
+ /** 32 bytes, hex, no 0x. Opens checkpoints in the debugger. */
31
+ encryptionKey: string;
32
+ /** Authorization header value for the IPFS API, e.g. "Bearer …". */
33
+ ipfsAuthorization?: string;
34
+ }
35
+ export interface Session {
36
+ executionId?: string;
37
+ nextSequence?: number;
38
+ day?: string;
39
+ checkpointsToday?: number;
40
+ }
41
+ export interface NetworkInfo {
42
+ name: string;
43
+ chainId: number;
44
+ ledger: `0x${string}`;
45
+ rpcUrl: string;
46
+ startBlock: bigint;
47
+ /** Public debugger page for a workflow, when one exists for this network. */
48
+ debuggerUrl?: (workflowId: string) => string;
49
+ }
50
+ export declare function upochHome(): string;
51
+ export declare function loadConfig(home?: string): UpochConfig;
52
+ export declare function saveConfig(c: UpochConfig, home?: string): void;
53
+ export declare function loadSecrets(home?: string): UpochSecrets;
54
+ export declare function saveSecrets(s: UpochSecrets, home?: string): void;
55
+ export declare function newSecrets(ipfsAuthorization?: string): UpochSecrets;
56
+ export declare function loadSession(home?: string): Session;
57
+ export declare function saveSession(s: Session, home?: string): void;
58
+ export declare const journalPath: (home?: string) => string;
59
+ export declare function hasSetup(home?: string): boolean;
60
+ export declare function networkInfo(c: UpochConfig): NetworkInfo;
package/dist/config.js ADDED
@@ -0,0 +1,70 @@
1
+ // Local configuration for Upoch Guard. Everything lives in one folder (default ~/.upoch, or UPOCH_HOME):
2
+ // config.json network, workflow, storage and limits (no secrets)
3
+ // secrets.json owner and executor keys, the checkpoint encryption key, the IPFS token (file mode 600)
4
+ // session.json the open execution and today's checkpoint count
5
+ // journal.jsonl the action journal
6
+ import { randomBytes } from "node:crypto";
7
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import path from "node:path";
10
+ import { ROBINHOOD_MAINNET, ROBINHOOD_TESTNET } from "@upoch/sdk";
11
+ import { generatePrivateKey } from "viem/accounts";
12
+ export function upochHome() {
13
+ return process.env.UPOCH_HOME || path.join(homedir(), ".upoch");
14
+ }
15
+ const file = (home, name) => path.join(home, name);
16
+ function readJson(p) {
17
+ if (!existsSync(p))
18
+ return undefined;
19
+ return JSON.parse(readFileSync(p, "utf8"));
20
+ }
21
+ /** Atomic write; secrets are written owner-only (enforced on POSIX; on Windows the folder's ACL applies). */
22
+ function writeJson(p, v, secret = false) {
23
+ const tmp = `${p}.${randomBytes(4).toString("hex")}.tmp`;
24
+ writeFileSync(tmp, JSON.stringify(v, null, 2) + "\n", { mode: secret ? 0o600 : 0o644 });
25
+ renameSync(tmp, p);
26
+ if (secret)
27
+ chmodSync(p, 0o600);
28
+ }
29
+ export function loadConfig(home = upochHome()) {
30
+ const c = readJson(file(home, "config.json"));
31
+ if (!c)
32
+ throw new Error(`Upoch is not set up yet. Run "upoch init" in your own terminal (looked in ${home}).`);
33
+ return c;
34
+ }
35
+ export function saveConfig(c, home = upochHome()) {
36
+ mkdirSync(home, { recursive: true });
37
+ writeJson(file(home, "config.json"), c);
38
+ }
39
+ export function loadSecrets(home = upochHome()) {
40
+ const s = readJson(file(home, "secrets.json"));
41
+ if (!s)
42
+ throw new Error(`No keys found in ${home}. Run "upoch init" in your own terminal.`);
43
+ return s;
44
+ }
45
+ export function saveSecrets(s, home = upochHome()) {
46
+ mkdirSync(home, { recursive: true });
47
+ writeJson(file(home, "secrets.json"), s, true);
48
+ }
49
+ export function newSecrets(ipfsAuthorization) {
50
+ return { ownerKey: generatePrivateKey(), executorKey: generatePrivateKey(), encryptionKey: randomBytes(32).toString("hex"), ...(ipfsAuthorization ? { ipfsAuthorization } : {}) };
51
+ }
52
+ export function loadSession(home = upochHome()) {
53
+ return readJson(file(home, "session.json")) ?? {};
54
+ }
55
+ export function saveSession(s, home = upochHome()) {
56
+ writeJson(file(home, "session.json"), s);
57
+ }
58
+ export const journalPath = (home = upochHome()) => file(home, "journal.jsonl");
59
+ export function hasSetup(home = upochHome()) {
60
+ return existsSync(file(home, "config.json")) && existsSync(file(home, "secrets.json"));
61
+ }
62
+ export function networkInfo(c) {
63
+ if (c.network === "mainnet")
64
+ return { ...ROBINHOOD_MAINNET, debuggerUrl: (id) => `https://upoch.tech/app/agents/${id}/debugger` };
65
+ if (c.network === "testnet")
66
+ return { ...ROBINHOOD_TESTNET };
67
+ if (!c.custom)
68
+ throw new Error('config.json: network "custom" needs a "custom" section');
69
+ return { name: "Custom chain", chainId: c.custom.chainId, ledger: c.custom.ledger.toLowerCase(), rpcUrl: c.custom.rpcUrl, startBlock: BigInt(c.custom.startBlock ?? "0") };
70
+ }
@@ -0,0 +1,123 @@
1
+ import { LocalKeySigner, type Eip1193Provider } from "@upoch/sdk";
2
+ import { type NetworkInfo, type UpochConfig } from "./config.ts";
3
+ export type CheckpointKind = "progress" | "before_action" | "after_action" | "manual";
4
+ export type ActionStatus = "planned" | "submitted" | "confirmed" | "failed";
5
+ /** A refusal the agent can act on (wrong input, a limit, missing funds). Distinct from unexpected failures. */
6
+ export declare class GuardError extends Error {
7
+ readonly code: string;
8
+ constructor(code: string, message: string);
9
+ }
10
+ export interface GuardOptions {
11
+ home?: string;
12
+ /** Overrides the RPC provider (tests). */
13
+ provider?: Eip1193Provider;
14
+ now?: () => Date;
15
+ }
16
+ export declare class Guard {
17
+ readonly home: string;
18
+ readonly config: UpochConfig;
19
+ readonly network: NetworkInfo;
20
+ readonly executor: LocalKeySigner;
21
+ readonly ownerAddress: `0x${string}`;
22
+ private readonly provider;
23
+ private readonly reader;
24
+ private readonly journal;
25
+ private readonly now;
26
+ private readonly principal;
27
+ private parts?;
28
+ constructor(o?: GuardOptions);
29
+ private readonly build;
30
+ private get p();
31
+ private get workflowId();
32
+ private session;
33
+ private today;
34
+ private openExecution;
35
+ /** Workflow, executor funds, the open execution and today's usage. Read-only. */
36
+ status(): Promise<{
37
+ workflow: null;
38
+ next: string;
39
+ network: string;
40
+ chainId: number;
41
+ contract: `0x${string}`;
42
+ agentId: string;
43
+ executor: `0x${string}`;
44
+ executorBalanceEth: string;
45
+ checkpointsToday: string;
46
+ openExecution: string | null;
47
+ storage: string;
48
+ } | {
49
+ debugger?: string | undefined;
50
+ workflow: {
51
+ id: string;
52
+ status: import("@upoch/sdk").WorkflowStatus;
53
+ checkpoints: string;
54
+ headSequence: string | null;
55
+ headHash: `0x${string}` | null;
56
+ };
57
+ network: string;
58
+ chainId: number;
59
+ contract: `0x${string}`;
60
+ agentId: string;
61
+ executor: `0x${string}`;
62
+ executorBalanceEth: string;
63
+ checkpointsToday: string;
64
+ openExecution: string | null;
65
+ storage: string;
66
+ }>;
67
+ /** Commits a verified checkpoint of `state`. Refuses state that looks like it holds secrets. */
68
+ checkpoint(input: {
69
+ state: unknown;
70
+ note?: string;
71
+ kind?: CheckpointKind;
72
+ }, executionStatus?: "RUNNING" | "COMPLETED"): Promise<any>;
73
+ /** Records an external action (a swap, a transfer, an API call) so the next checkpoint and any recovery know about it. */
74
+ recordAction(a: {
75
+ operationId: string;
76
+ type: string;
77
+ status: ActionStatus;
78
+ transactionHash?: string;
79
+ input?: unknown;
80
+ result?: unknown;
81
+ }): Promise<{
82
+ recorded: boolean;
83
+ operationId: string;
84
+ status: ActionStatus;
85
+ executionId: string;
86
+ note: string;
87
+ }>;
88
+ /**
89
+ * Restores the latest verified checkpoint (or `sequence`): checks it against the chain, decrypts it and
90
+ * reconciles recorded actions. Continues that execution when it was still running.
91
+ */
92
+ restore(o?: {
93
+ sequence?: number;
94
+ }): Promise<any>;
95
+ /**
96
+ * Rolls the agent back to an earlier checkpoint and continues from it: verifies that checkpoint, then commits its
97
+ * state again as a new MANUAL checkpoint ("rolled back to #n"), so every later restore starts from it. The run
98
+ * continues as a new execution. On-chain effects since then (trades, transfers) are not undone; they are listed so
99
+ * the agent can account for them.
100
+ */
101
+ rollback(o: {
102
+ sequence: number;
103
+ reason: string;
104
+ annotate?: (state: unknown, actionsSince: {
105
+ operationId: string;
106
+ status: string;
107
+ transactionHash?: string;
108
+ }[]) => unknown;
109
+ }): Promise<any>;
110
+ /** The most recent checkpoints on-chain, newest first. Read-only. */
111
+ history(o?: {
112
+ limit?: number;
113
+ }): Promise<any>;
114
+ /** Independently verifies one checkpoint without restoring it. Read-only. */
115
+ verify(o: {
116
+ sequence: number;
117
+ }): Promise<any>;
118
+ /** Final checkpoint marking the run finished; the next checkpoint starts a new execution. */
119
+ complete(input: {
120
+ state: unknown;
121
+ note?: string;
122
+ }): Promise<any>;
123
+ }