@upoch/guard 0.1.1 → 0.1.3

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 CHANGED
@@ -6,9 +6,15 @@ Upoch Guard saves verifiable checkpoints of an agent's work on Robinhood Chain.
6
6
  prompt or makes a bad trade, it restores the last good state, checked against the chain, or rolls back to the
7
7
  checkpoint before the mistake and keeps going. It also knows which of its actions already happened.
8
8
 
9
- This package is the `upoch` command you set Upoch up with, and the engine behind the Upoch framework plugins:
9
+ This package is the `upoch` command you set Upoch up with, an MCP server, and the engine behind the Upoch
10
+ framework plugins:
10
11
 
11
- - **ElizaOS**: [@upoch/plugin-elizaos](https://www.npmjs.com/package/@upoch/plugin-elizaos)
12
+ | Agent | One line |
13
+ |---|---|
14
+ | Claude Code | `claude mcp add upoch -- upoch mcp` |
15
+ | Cursor, Codex, Claude Desktop, any MCP client | an MCP server with the command `upoch mcp` (stdio) |
16
+ | ElizaOS | `plugins: ["@upoch/plugin-elizaos"]` in your character ([@upoch/plugin-elizaos](https://www.npmjs.com/package/@upoch/plugin-elizaos)) |
17
+ | LangChain / LangGraph | `callbacks: [new UpochCallbackHandler()]` ([@upoch/langchain](https://www.npmjs.com/package/@upoch/langchain)) |
12
18
 
13
19
  Website and debugger: https://upoch.tech. Contract (Robinhood Chain, 4663): `0x6179d9123c3c1f9e82f948101a4350cb55aa8c27`
14
20
 
@@ -22,8 +28,8 @@ upoch setup
22
28
  ```
23
29
 
24
30
  `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.
31
+ Robinhood Chain (0.001 ETH is plenty). Then it registers your agent and, if Claude Code is installed, offers to
32
+ add the MCP server to it. After that, connecting an agent is one line (table above).
27
33
 
28
34
  Prefer separate steps? Run `upoch init`, then `upoch register`.
29
35
 
@@ -42,10 +48,28 @@ Two keys are created on your machine and never leave it:
42
48
  A rollback restores the agent's state and memory. It cannot reverse transactions that already happened on-chain;
43
49
  those are listed so the agent can account for them.
44
50
 
51
+ ## MCP tools
52
+
53
+ | Tool | What it does | On-chain |
54
+ |---|---|---|
55
+ | `upoch_restore` | Restores the latest checkpoint after verifying it: state, note, completed and unresolved actions | read |
56
+ | `upoch_checkpoint` | Saves the current state as a checkpoint (`progress`, `before_action`, `after_action`, `manual`) | 1 transaction |
57
+ | `upoch_record_action` | Records an external action (planned, submitted, confirmed, failed) with its transaction hash | no (goes into the next checkpoint) |
58
+ | `upoch_rollback` | Goes back to an earlier checkpoint (e.g. before a bad trade) and continues from it; lists actions since then that were not undone on-chain | 1 transaction |
59
+ | `upoch_complete` | Saves a final checkpoint and marks the run finished | 1 transaction |
60
+ | `upoch_history` | Lists recent checkpoints | read |
61
+ | `upoch_verify` | Verifies one checkpoint without restoring it | read |
62
+ | `upoch_status` | Shows the workflow, executor funds and today's usage | read |
63
+
64
+ The server tells the agent when to use them: restore at the start, checkpoint after meaningful progress and before
65
+ consequential actions, record each action as planned and then as confirmed, roll back when something went wrong,
66
+ and complete at the end.
67
+
45
68
  ## Commands
46
69
 
47
70
  ```sh
48
71
  upoch setup # keys, funding, registration, in one go
72
+ upoch mcp # run the MCP server (stdio); your MCP client starts it
49
73
  upoch status # workflow, funds and today's usage
50
74
  upoch key # print the checkpoint key, to open checkpoints in the debugger
51
75
  upoch export-owner # print the owner private key, to import it into a wallet
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // upoch init create keys and settings (your terminal only)
5
5
  // upoch register register the agent's workflow on-chain and fund its executor (your terminal only)
6
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)
7
+ // upoch mcp run the MCP server (stdio) for Claude Code, Cursor, Codex and other MCP clients
8
8
  // upoch key print the checkpoint key, to open checkpoints in the debugger (your terminal only)
9
9
  // upoch export-owner print the owner private key, to import it into a wallet (your terminal only)
10
10
  import { spawnSync } from "node:child_process";
@@ -16,11 +16,8 @@ import { hasSetup, loadConfig, loadSecrets, networkInfo, upochHome } from "./con
16
16
  import { Guard } from "./guard.js";
17
17
  import { createServer, VERSION } from "./server.js";
18
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 };
19
+ /** Integrations listed in help and setup output. */
20
+ const LAUNCHED = { mcp: true, langchain: true };
24
21
  const CONNECT = [
25
22
  ...(LAUNCHED.mcp ? [" Claude Code claude mcp add upoch -- upoch mcp", " Cursor, Codex add an MCP server with the command: upoch mcp"] : []),
26
23
  ' ElizaOS plugins: ["@upoch/plugin-elizaos"] in your character',
package/dist/guard.d.ts CHANGED
@@ -1,7 +1,34 @@
1
- import { LocalKeySigner, type Eip1193Provider } from "@upoch/sdk";
1
+ import { LocalKeySigner, type Eip1193Provider, type StorageAdapter } from "@upoch/sdk";
2
2
  import { type NetworkInfo, type UpochConfig } from "./config.ts";
3
3
  export type CheckpointKind = "progress" | "before_action" | "after_action" | "manual";
4
4
  export type ActionStatus = "planned" | "submitted" | "confirmed" | "failed";
5
+ /** An action that ran after a rollback's target checkpoint; rolling back does not reverse it. */
6
+ export type NotUndone = {
7
+ operationId: string;
8
+ status: string;
9
+ transactionHash?: string;
10
+ };
11
+ /**
12
+ * IPFS that reads through the gateway and, when the gateway refuses or fails, through the authenticated RPC `cat`.
13
+ * Found on mainnet: Filebase's gateway intermittently answers 403 "CDN proxying is not allowed" (behind VPNs such as
14
+ * Cloudflare WARP), which would otherwise fail a restore or the verification after a commit.
15
+ */
16
+ export declare class IpfsWithFallback implements StorageAdapter {
17
+ readonly provider: "ipfs";
18
+ readonly developmentOnly = false;
19
+ private readonly gateway;
20
+ private readonly rpc;
21
+ constructor(o: {
22
+ apiUrl: string;
23
+ gatewayUrl: string;
24
+ authorization?: string;
25
+ });
26
+ upload(bytes: Uint8Array): Promise<import("@upoch/sdk").StoredObject>;
27
+ retrieve(contentId: string): Promise<Uint8Array<ArrayBufferLike>>;
28
+ isAvailable(contentId: string): Promise<boolean>;
29
+ health(): Promise<import("@upoch/sdk").StorageHealth>;
30
+ locator(contentId: string): string;
31
+ }
5
32
  /** A refusal the agent can act on (wrong input, a limit, missing funds). Distinct from unexpected failures. */
6
33
  export declare class GuardError extends Error {
7
34
  readonly code: string;
@@ -12,6 +39,8 @@ export interface GuardOptions {
12
39
  /** Overrides the RPC provider (tests). */
13
40
  provider?: Eip1193Provider;
14
41
  now?: () => Date;
42
+ /** Waits before retrying a checkpoint whose submission failed transiently (default 3 s, then 8 s). */
43
+ retryDelaysMs?: number[];
15
44
  }
16
45
  export declare class Guard {
17
46
  readonly home: string;
@@ -23,6 +52,7 @@ export declare class Guard {
23
52
  private readonly reader;
24
53
  private readonly journal;
25
54
  private readonly now;
55
+ private readonly retryDelaysMs;
26
56
  private readonly principal;
27
57
  private parts?;
28
58
  constructor(o?: GuardOptions);
@@ -69,6 +99,7 @@ export declare class Guard {
69
99
  state: unknown;
70
100
  note?: string;
71
101
  kind?: CheckpointKind;
102
+ notUndone?: NotUndone[];
72
103
  }, executionStatus?: "RUNNING" | "COMPLETED"): Promise<any>;
73
104
  /** Records an external action (a swap, a transfer, an API call) so the next checkpoint and any recovery know about it. */
74
105
  recordAction(a: {
@@ -101,11 +132,7 @@ export declare class Guard {
101
132
  rollback(o: {
102
133
  sequence: number;
103
134
  reason: string;
104
- annotate?: (state: unknown, actionsSince: {
105
- operationId: string;
106
- status: string;
107
- transactionHash?: string;
108
- }[]) => unknown;
135
+ annotate?: (state: unknown, actionsSince: NotUndone[]) => unknown;
109
136
  }): Promise<any>;
110
137
  /** The most recent checkpoints on-chain, newest first. Read-only. */
111
138
  history(o?: {
package/dist/guard.js CHANGED
@@ -10,6 +10,52 @@ const CHECKPOINT_GAS = 230000n;
10
10
  const HASH = /^0x[0-9a-fA-F]{64}$/;
11
11
  const TYPES = { progress: "SAFE_BOUNDARY", before_action: "PRE_ACTION", after_action: "POST_ACTION", manual: "MANUAL" };
12
12
  const STATUSES = { planned: "PENDING", submitted: "SUBMITTED", confirmed: "CONFIRMED", failed: "FAILED" };
13
+ /**
14
+ * IPFS that reads through the gateway and, when the gateway refuses or fails, through the authenticated RPC `cat`.
15
+ * Found on mainnet: Filebase's gateway intermittently answers 403 "CDN proxying is not allowed" (behind VPNs such as
16
+ * Cloudflare WARP), which would otherwise fail a restore or the verification after a commit.
17
+ */
18
+ export class IpfsWithFallback {
19
+ provider = "ipfs";
20
+ developmentOnly = false;
21
+ gateway;
22
+ rpc;
23
+ constructor(o) {
24
+ this.gateway = new IpfsStorageAdapter(o);
25
+ this.rpc = new IpfsStorageAdapter({ apiUrl: o.apiUrl, ...(o.authorization ? { authorization: o.authorization } : {}) });
26
+ }
27
+ upload(bytes) {
28
+ return this.rpc.upload(bytes);
29
+ }
30
+ async retrieve(contentId) {
31
+ try {
32
+ return await this.gateway.retrieve(contentId);
33
+ }
34
+ catch (gatewayError) {
35
+ try {
36
+ return await this.rpc.retrieve(contentId);
37
+ }
38
+ catch {
39
+ throw gatewayError;
40
+ }
41
+ }
42
+ }
43
+ async isAvailable(contentId) {
44
+ try {
45
+ await this.retrieve(contentId);
46
+ return true;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ health() {
53
+ return this.rpc.health();
54
+ }
55
+ locator(contentId) {
56
+ return this.gateway.locator(contentId);
57
+ }
58
+ }
13
59
  /** A refusal the agent can act on (wrong input, a limit, missing funds). Distinct from unexpected failures. */
14
60
  export class GuardError extends Error {
15
61
  code;
@@ -29,6 +75,7 @@ export class Guard {
29
75
  reader;
30
76
  journal;
31
77
  now;
78
+ retryDelaysMs;
32
79
  principal;
33
80
  parts;
34
81
  constructor(o = {}) {
@@ -42,6 +89,7 @@ export class Guard {
42
89
  this.reader = new EpochReader(this.provider, this.network.ledger);
43
90
  this.journal = new FileJournal(journalPath(this.home));
44
91
  this.now = o.now ?? (() => new Date());
92
+ this.retryDelaysMs = o.retryDelaysMs ?? [3000, 8000];
45
93
  this.principal = { kind: "agent", id: this.config.agentId };
46
94
  // Built lazily (needs a registered workflow); the secrets stay inside these closures, never on `this`.
47
95
  this.build = () => {
@@ -52,7 +100,9 @@ export class Guard {
52
100
  const s = this.config.storage;
53
101
  const storage = new ReplicatedStorage([
54
102
  s.kind === "ipfs"
55
- ? new IpfsStorageAdapter({ apiUrl: s.apiUrl, gatewayUrl: s.gatewayUrl, authorization: secrets.ipfsAuthorization })
103
+ ? s.gatewayUrl
104
+ ? new IpfsWithFallback({ apiUrl: s.apiUrl, gatewayUrl: s.gatewayUrl, ...(secrets.ipfsAuthorization ? { authorization: secrets.ipfsAuthorization } : {}) })
105
+ : new IpfsStorageAdapter({ apiUrl: s.apiUrl, authorization: secrets.ipfsAuthorization })
56
106
  : new LocalStorageAdapter(s.dir),
57
107
  ]);
58
108
  const encryption = new LocalAesGcmProvider(Buffer.from(secrets.encryptionKey, "hex"));
@@ -146,15 +196,24 @@ export class Guard {
146
196
  ...(r.transactionHash ? { transactionHash: r.transactionHash } : {}),
147
197
  ...(r.resultCommitment ? { resultHash: r.resultCommitment } : {}),
148
198
  }));
149
- const rec = await this.p.manager.create({
199
+ const create = () => this.p.manager.create({
150
200
  executionId: exec.executionId,
151
201
  checkpointSequence: exec.nextSequence,
152
202
  checkpointType: TYPES[input.kind ?? "progress"],
153
203
  state: input.state,
154
- context: input.note ? { note: input.note } : {},
204
+ context: { ...(input.note ? { note: input.note } : {}), ...(input.notUndone?.length ? { notUndone: input.notUndone } : {}) },
155
205
  actions,
156
206
  executionStatus,
157
207
  });
208
+ let rec = await create();
209
+ // Found on mainnet: a transient RPC failure fails the submission before anything is sent. Retrying is safe: each
210
+ // attempt re-reads the head, and the chain adapter checks for an identical committed checkpoint before sending.
211
+ for (const wait of this.retryDelaysMs) {
212
+ if (rec.status !== "SUBMISSION_FAILED" || rec.error?.code !== "CHAIN_SUBMISSION_FAILED")
213
+ break;
214
+ await new Promise((r) => setTimeout(r, wait));
215
+ rec = await create();
216
+ }
158
217
  const committed = rec.status === "COMMITTED" || rec.status === "VERIFIED_RECOVERY_POINT";
159
218
  if (committed) {
160
219
  const after = this.session();
@@ -221,6 +280,9 @@ export class Guard {
221
280
  saveSession(cp.executionStatus === "RUNNING" ? { ...s, executionId: cp.manifest.executionId, nextSequence: cp.manifest.checkpointSequence + 1 } : { day: s.day, checkpointsToday: s.checkpointsToday }, this.home);
222
281
  }
223
282
  const note = cp.context?.note;
283
+ // Saved by a rollback: actions that happened after the restored checkpoint, so a restart still knows them.
284
+ const notUndone = cp.context?.notUndone;
285
+ const hasNotUndone = Array.isArray(notUndone) && notUndone.length > 0;
224
286
  return bigintSafe({
225
287
  status: r.status,
226
288
  sequence: cp.sequence,
@@ -233,11 +295,14 @@ export class Guard {
233
295
  actions: cp.manifest.actions,
234
296
  completedActions: r.completed.map((c) => c.operationId),
235
297
  unresolvedActions: r.unresolved,
298
+ ...(hasNotUndone ? { notUndone } : {}),
236
299
  ...(isHead ? {} : { warning: "This is an older checkpoint, read for inspection only. To go back to it and continue from it, call upoch_rollback with this sequence." }),
237
300
  next: r.status === "HALTED_FOR_REVIEW"
238
301
  ? "Some actions have an unknown outcome. Check them (for example by transaction hash) before doing anything else, then record their real status."
239
302
  : cp.executionStatus === "RUNNING"
240
- ? "Continue from this state. Do not repeat completed actions."
303
+ ? hasNotUndone
304
+ ? "Continue from this state. It was restored by a rollback: the actions in notUndone happened after it and were NOT undone on-chain. Do not repeat them; check their effects (balances, positions) before acting."
305
+ : "Continue from this state. Do not repeat completed actions."
241
306
  : "That run had finished. A new checkpoint starts a new run.",
242
307
  });
243
308
  }
@@ -282,7 +347,7 @@ export class Guard {
282
347
  const note = `Rolled back to checkpoint #${o.sequence}: ${o.reason.trim()}`.slice(0, 500);
283
348
  const actionsSince = [...since.values()];
284
349
  const state = o.annotate ? o.annotate(structuredClone(cp.state), actionsSince) : cp.state;
285
- const committed = await this.checkpoint({ state, note, kind: "manual" });
350
+ const committed = await this.checkpoint({ state, note, kind: "manual", notUndone: actionsSince });
286
351
  if (committed.status !== "VERIFIED_RECOVERY_POINT" && committed.status !== "COMMITTED")
287
352
  return { ...committed, rolledBackTo: o.sequence, state: undefined };
288
353
  return bigintSafe({
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { Guard } from "./guard.ts";
3
- export declare const VERSION = "0.1.1";
3
+ export declare const VERSION = "0.1.3";
4
4
  export declare const INSTRUCTIONS = "Upoch Guard saves verifiable checkpoints of your work on Robinhood Chain, so a crash, a bad prompt or a bad trade can be recovered from the last good state.\n\nWhen to use it:\n- At the start of a task, call upoch_restore. If a run was interrupted, continue from the returned state and do not repeat its completed actions.\n- After meaningful progress, call upoch_checkpoint with the state you would need to continue (plan, step, positions, results so far). Not after every small step: each checkpoint is an on-chain transaction.\n- Before a consequential external action (a swap, a transfer, a deployment), call upoch_record_action with status \"planned\" and then upoch_checkpoint with kind \"before_action\". After it settles, record it again as \"confirmed\" (with its transaction hash) or \"failed\".\n- If something went wrong (a bad trade, a bad instruction, corrupted state), use upoch_history to find the last good checkpoint and upoch_rollback to go back to it and continue. Rolling back restores your state; it does not undo on-chain transactions, so check the actions it lists.\n- When the task is finished, call upoch_complete.\n\nNever put private keys, seed phrases, passwords or API tokens in the state, notes or actions: checkpoints are permanent. Upoch refuses state that looks like it contains them. Setup (keys, registration, funding) is done by the user in their own terminal with the upoch command; never run \"upoch init\", \"upoch register\", \"upoch key\" or \"upoch export-owner\" yourself.";
5
5
  export declare function createServer(guard: Guard | (() => Guard)): McpServer;
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { z } from "zod";
5
5
  import { Guard, GuardError } from "./guard.js";
6
- export const VERSION = "0.1.1";
6
+ export const VERSION = "0.1.3";
7
7
  export const INSTRUCTIONS = `Upoch Guard saves verifiable checkpoints of your work on Robinhood Chain, so a crash, a bad prompt or a bad trade can be recovered from the last good state.
8
8
 
9
9
  When to use it:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@upoch/guard",
3
- "version": "0.1.1",
4
- "description": "Upoch Guard: verifiable checkpoints, recovery and rollback for AI agents on Robinhood Chain. The upoch command and the engine behind the Upoch framework plugins.",
3
+ "version": "0.1.3",
4
+ "description": "Upoch Guard: verifiable checkpoints, recovery and rollback for AI agents on Robinhood Chain. The upoch command, an MCP server for Claude Code, Cursor and Codex, and the engine behind the Upoch plugins for ElizaOS and LangChain.",
5
5
  "keywords": [
6
6
  "upoch",
7
7
  "ai-agents",
@@ -9,7 +9,10 @@
9
9
  "recovery",
10
10
  "rollback",
11
11
  "robinhood-chain",
12
- "elizaos"
12
+ "elizaos",
13
+ "langchain",
14
+ "mcp",
15
+ "model-context-protocol"
13
16
  ],
14
17
  "homepage": "https://upoch.tech/docs",
15
18
  "license": "MIT",