@upoch/guard 0.1.2 → 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/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.2";
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.2";
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@upoch/guard",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
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",