feat: add @elizaos/plugin-openttt — Proof-of-Time temporal attestation for AI agents#6645
feat: add @elizaos/plugin-openttt — Proof-of-Time temporal attestation for AI agents#6645Heime-Jorgen wants to merge 5 commits intoelizaOS:developfrom
Conversation
…n (v3) - Real Ed25519 signing via openttt SDK (PotSigner.signPot / verifyPotSignature) - SHA-256 potHash as stable cache key for GENERATE_POT/VERIFY_POT correlation - potHash-keyed + agentId last-pointer cache (no message.id mismatch) - GENERATE_POT, VERIFY_POT, QUERY_POT actions - TIME_PROVIDER (4-source: NIST, Apple, Google, Cloudflare) - POT_COVERAGE_EVALUATOR for trade message coverage check - @elizaos/core 2.0.x compatible handler signatures
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR introduces Changes
Sequence Diagram(s)sequenceDiagram
participant Agent
participant generatePot as generatePot Action
participant timeProvider as Time Provider
participant TimeSources as Time Sources<br/>(NIST/Apple/Google/CF)
participant PotSigner as PotSigner
participant Cache as PoT Cache
participant verifyPot as verifyPot Action
Agent->>generatePot: Request PoT token
generatePot->>timeProvider: getVerifiedTime()
timeProvider->>TimeSources: Concurrent HEAD requests
TimeSources-->>timeProvider: Date headers
timeProvider->>timeProvider: Compute consensus & deviation
timeProvider-->>generatePot: VerifiedTime (timestamp, sources, consensus)
generatePot->>generatePot: Generate nonce & potHash
generatePot->>PotSigner: Sign potHash (Ed25519)
PotSigner-->>generatePot: Signature
generatePot->>Cache: Store PoTToken (TTL: 5 min)
generatePot-->>Agent: success + pot data
Agent->>verifyPot: Verify PoT token
verifyPot->>Cache: Retrieve cached PoT
Cache-->>verifyPot: PoTToken
verifyPot->>PotSigner: Verify signature
PotSigner-->>verifyPot: Signature valid
verifyPot->>timeProvider: getVerifiedTime() for age check
timeProvider-->>verifyPot: Current time
verifyPot->>verifyPot: Check age ≤ 5 min
verifyPot-->>Agent: VerifyResult (valid: true, age_ms, details)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
| const potKey = `openttt:pot:${message.id}`; | ||
| const cached = potCacheGet(potKey); |
There was a problem hiding this comment.
Evaluator always warns — wrong cache key
The evaluator looks up openttt:pot:${message.id}, but generatePot never stores a PoT under that key. It stores under openttt:pot:${pot.potHash} and openttt:last:${agentId}. Because message.id is never used as a cache key in generatePot.ts, this lookup will always return null and the evaluator will unconditionally print the "No Proof-of-Time found" warning, even when a PoT was just generated. The PR description even explicitly calls out "SHA-256 potHash as stable cache key (avoids message.id mismatch)" — the evaluator was never updated to match.
The fix is to fall back to the openttt:last:{agentId} pointer (as verifyPot and queryPot already do):
handler: async (
_runtime: IAgentRuntime,
message: Memory,
_state?: State
): Promise<ActionResult | void | undefined> => {
// First try to find PoT via last-generated pointer for this agent
const agentId = (_runtime.agentId ?? "unknown") as string;
const lastHash = potCacheGet(`openttt:last:${agentId}`);
const cached = lastHash ? potCacheGet(`openttt:pot:${lastHash}`) : null;| // Module-level signer: ephemeral Ed25519 keypair per agent process. | ||
| // A fresh keypair is generated on startup; the public key ships with every PoT | ||
| // so any counterparty can verify the signature without a PKI. | ||
| const _signer = new PotSigner(); |
There was a problem hiding this comment.
Ephemeral keypair invalidates all persisted PoTs on restart
_signer = new PotSigner() generates a fresh Ed25519 keypair at module load time. If the agent process restarts — even briefly — all previously issued PoTs become unverifiable because PotSigner.verifyPotSignature will be called with the old signature against a brand-new public key. This directly undermines the core value proposition: a counterparty storing a PoT hash cannot verify it after any service restart.
Consider either:
- Persisting the signing keypair to
runtime.getSetting("OPENTTT_PRIVATE_KEY")/ the agent's secret store so the same key survives restarts. - Or embedding the public key inside
PotSignatureso that verification is self-contained and works with any signer instance (if theopentttSDK already does this, add a comment clarifying it).
| setInterval(() => { | ||
| const now = Date.now(); | ||
| for (const [key, entry] of potCache.entries()) { | ||
| if (now > entry.expiresAt) potCache.delete(key); | ||
| } | ||
| }, 60_000); |
There was a problem hiding this comment.
setInterval without .unref() prevents graceful process exit
The cleanup interval is never unref'd, so Node.js will keep the event loop alive indefinitely just for this timer. Call .unref() so the process can exit cleanly when no other work is pending:
| setInterval(() => { | |
| const now = Date.now(); | |
| for (const [key, entry] of potCache.entries()) { | |
| if (now > entry.expiresAt) potCache.delete(key); | |
| } | |
| }, 60_000); | |
| const _cleanupInterval = setInterval(() => { | |
| const now = Date.now(); | |
| for (const [key, entry] of potCache.entries()) { | |
| if (now > entry.expiresAt) potCache.delete(key); | |
| } | |
| }, 60_000).unref(); |
| "peerDependencies": { | ||
| "@elizaos/core": ">=0.1.0" | ||
| }, |
There was a problem hiding this comment.
peerDependencies version range is too broad
>=0.1.0 implies compatibility with every version of @elizaos/core starting from the very beginning, but the handler signatures, ActionResult, ProviderResult, and other types used in this plugin target the 2.x API surface. Older versions lack these types and would produce cryptic build errors. The range should be tightened:
| "peerDependencies": { | |
| "@elizaos/core": ">=0.1.0" | |
| }, | |
| "peerDependencies": { | |
| "@elizaos/core": ">=2.0.0" | |
| }, |
| { | ||
| "name": "@elizaos/plugin-openttt", | ||
| "version": "0.1.0", | ||
| "description": "OpenTTT Proof-of-Time plugin for ElizaOS — temporal attestation for AI agent transactions", | ||
| "main": "dist/index.js", | ||
| "types": "dist/index.d.ts", | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "test": "jest" | ||
| }, | ||
| "keywords": [ | ||
| "elizaos", | ||
| "plugin", | ||
| "proof-of-time", | ||
| "temporal-attestation", | ||
| "openttt" | ||
| ], | ||
| "license": "MIT", | ||
| "peerDependencies": { | ||
| "@elizaos/core": ">=0.1.0" | ||
| }, | ||
| "dependencies": { | ||
| "openttt": "^0.2.6" | ||
| }, | ||
| "devDependencies": { | ||
| "@elizaos/core": "^2.0.0-alpha.86", | ||
| "@types/jest": "^29.0.0", | ||
| "jest": "^29.0.0", | ||
| "ts-jest": "^29.0.0", | ||
| "typescript": "^5.0.0" | ||
| }, | ||
| "overrides": { | ||
| "@types/node": "^25.0.6" | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing standard package.json fields
The package is missing several fields required for correct module resolution and publishing in the monorepo:
"type": "module"— the source uses ESM (.jsextension imports,ESNextmodule target intsconfig.json), so this field is needed so Node.js resolvesdist/index.jsas ESM."exports"map — modern bundlers and Node.js useexportsfor package entry points."files"— without this,npm publishwill include all files (includingsrc/,tsconfig.json, etc.).
Additionally, the README.md states openttt@0.1.3 but package.json declares "openttt": "^0.2.6". These should be kept in sync to avoid confusing users.
Evaluator was looking up openttt:pot:${message.id} which was never
stored. generatePot stores under openttt:pot:${potHash} with a
last-pointer at openttt:last:${agentId}.
Now resolves: agentId last-pointer → potHash → PoT record.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
packages/plugin-openttt/src/actions/generatePot.ts (1)
23-29: Considerunref()on cleanup interval for graceful shutdown.The
setIntervalkeeps the Node.js event loop alive, potentially preventing the process from exiting gracefully when the agent stops.Proposed fix
// Periodic cleanup: evict expired entries every minute -setInterval(() => { +const cleanupInterval = setInterval(() => { const now = Date.now(); for (const [key, entry] of potCache.entries()) { if (now > entry.expiresAt) potCache.delete(key); } }, 60_000); +cleanupInterval.unref();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugin-openttt/src/actions/generatePot.ts` around lines 23 - 29, The periodic cleanup uses setInterval(...) which keeps the Node event loop alive; capture the returned timer from setInterval in generatePot.ts (the variable holding the interval for the potCache cleanup) and call timer.unref() so the interval won't prevent graceful shutdown; locate the setInterval that iterates potCache.entries(), assign it to a const (e.g., cleanupTimer) and call cleanupTimer.unref() after creation to allow the process to exit when no other work remains.packages/plugin-openttt/README.md (1)
43-53: Add language specifier to fenced code blocks.Static analysis flags these conversation examples as missing language specifiers. Use
textorplaintextfor non-code content.Proposed fix
-``` +```text User: Generate a proof of time before I submit this swap🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugin-openttt/README.md` around lines 43 - 53, The fenced conversation example in README.md is missing a language specifier which triggers static analysis; update the triple-backtick fence(s) around the conversation snippet (the block starting with "User: Generate a proof of time before I submit this swap") to include a language token such as text or plaintext (e.g., change ``` to ```text) so the example is recognized as non-code/plain text; ensure all similar fenced blocks in the file use the same specifier.packages/plugin-openttt/src/actions/verifyPot.ts (1)
94-163: Consolidate repeated failure-return pathsThe failure branches repeat callback +
ActionResultconstruction many times. This is easy to drift and hard to maintain; extract a local helper (e.g.,fail(result)) and reuse it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugin-openttt/src/actions/verifyPot.ts` around lines 94 - 163, Multiple failure branches in verifyPot are duplicating the same pattern (build VerifyResult, optionally await callback({...}), then return { success:false,... }); refactor by adding a local helper function (e.g., fail(result: VerifyResult)) inside the verifyPot scope that performs the callback invocation when callback is present and returns the ActionResult shape expected; then replace each repeated block (the "no pot" branch, missing signature branch, sigValid false branch, expired age_ms > POT_MAX_AGE_MS branch, and future timestamp age_ms < 0 branch) to call fail(result) instead of inlining the callback + return logic. Ensure the helper preserves awaiting the callback and returns exactly { success: false, error: result.reason, data: { result } } so existing callers and behavior (including use of pot, age_ms, and current_time fields) remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/plugin-openttt/package.json`:
- Around line 1-10: The package.json for the `@elizaos/plugin-openttt` package is
missing the top-level "type": "module" field causing Node to treat emitted
ESNext .js files as CommonJS; update the package.json (the object containing
"name": "@elizaos/plugin-openttt", "version", "main": "dist/index.js", "types":
"dist/index.d.ts", etc.) to include "type": "module" at the top level so Node
will load the dist/*.js files as ESM (ensure it's added alongside the existing
fields, not nested).
In `@packages/plugin-openttt/README.md`:
- Around line 82-87: Update the README example so the documented action list for
openTTTPlugin matches the registered actions: include queryPot alongside
generatePot and verifyPot in the openTTTPlugin.actions comment (i.e., change the
comment showing [generatePot, verifyPot] to [generatePot, verifyPot, queryPot])
so the API section accurately reflects the registered actions.
In `@packages/plugin-openttt/src/actions/verifyPot.ts`:
- Around line 67-70: The SHA-256 pot hash extraction is too permissive: replace
the loose regex (currently matching 32+ hex chars via textMatch/potHashFromText
in verifyPot.ts) with an exact 64-hex-character match (use a regex that enforces
64 hex chars) and normalize the resulting value to lowercase for consistent
cache keys; also normalize potHashArg (options?.pot_hash) to lowercase when
using it so both sources produce the same canonical lowercase 64-char hash
string.
- Around line 108-130: The current signature check calls
PotSigner.verifyPotSignature(pot.potHash, pot.signature) but later uses mutable
fields pot.timestamp, pot.sources, and pot.agent_id for expiry/drift decisions,
allowing tampering; update verifyPot logic to bind the signature to the actual
payload fields by re-computing the canonical payload hash from the full token
fields (at least pot.potHash plus pot.timestamp, pot.sources, pot.agent_id — or
serialize the canonical payload used at signing) and verify the signature
against that recomputed hash via PotSigner.verifyPotSignature (or a new
PotSigner.verifySignedPayload) before performing age/checks (getVerifiedTime(),
age_ms). Ensure the verification path rejects tokens where the signed payload
does not include those mutable fields.
In `@packages/plugin-openttt/src/evaluators/potEvaluator.ts`:
- Around line 39-41: The evaluator uses potCacheGet with key
`openttt:pot:${message.id}` which never matches the key generated by generatePot
(`openttt:pot:${pot.potHash}`), so it always misses PoT tokens; update the
lookup in potEvaluator to follow the same resolution used by verifyPot/queryPot:
read the pointer `openttt:last:${agentId}` (resolve agentId from the message or
context), use that pointer value (potHash) to build the real key
`openttt:pot:${potHash}`, then call potCacheGet with that key (keeping existing
potCacheGet usage and error handling). Ensure you reference the same pointer
resolution logic as in verifyPot/queryPot and remove reliance on message.id.
---
Nitpick comments:
In `@packages/plugin-openttt/README.md`:
- Around line 43-53: The fenced conversation example in README.md is missing a
language specifier which triggers static analysis; update the triple-backtick
fence(s) around the conversation snippet (the block starting with "User:
Generate a proof of time before I submit this swap") to include a language token
such as text or plaintext (e.g., change ``` to ```text) so the example is
recognized as non-code/plain text; ensure all similar fenced blocks in the file
use the same specifier.
In `@packages/plugin-openttt/src/actions/generatePot.ts`:
- Around line 23-29: The periodic cleanup uses setInterval(...) which keeps the
Node event loop alive; capture the returned timer from setInterval in
generatePot.ts (the variable holding the interval for the potCache cleanup) and
call timer.unref() so the interval won't prevent graceful shutdown; locate the
setInterval that iterates potCache.entries(), assign it to a const (e.g.,
cleanupTimer) and call cleanupTimer.unref() after creation to allow the process
to exit when no other work remains.
In `@packages/plugin-openttt/src/actions/verifyPot.ts`:
- Around line 94-163: Multiple failure branches in verifyPot are duplicating the
same pattern (build VerifyResult, optionally await callback({...}), then return
{ success:false,... }); refactor by adding a local helper function (e.g.,
fail(result: VerifyResult)) inside the verifyPot scope that performs the
callback invocation when callback is present and returns the ActionResult shape
expected; then replace each repeated block (the "no pot" branch, missing
signature branch, sigValid false branch, expired age_ms > POT_MAX_AGE_MS branch,
and future timestamp age_ms < 0 branch) to call fail(result) instead of inlining
the callback + return logic. Ensure the helper preserves awaiting the callback
and returns exactly { success: false, error: result.reason, data: { result } }
so existing callers and behavior (including use of pot, age_ms, and current_time
fields) remain unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4c529a98-ae17-48f9-9193-c6f40046ebcd
📒 Files selected for processing (9)
packages/plugin-openttt/README.mdpackages/plugin-openttt/package.jsonpackages/plugin-openttt/src/actions/generatePot.tspackages/plugin-openttt/src/actions/queryPot.tspackages/plugin-openttt/src/actions/verifyPot.tspackages/plugin-openttt/src/evaluators/potEvaluator.tspackages/plugin-openttt/src/index.tspackages/plugin-openttt/src/providers/timeProvider.tspackages/plugin-openttt/tsconfig.json
| { | ||
| "name": "@elizaos/plugin-openttt", | ||
| "version": "0.1.0", | ||
| "description": "OpenTTT Proof-of-Time plugin for ElizaOS — temporal attestation for AI agent transactions", | ||
| "main": "dist/index.js", | ||
| "types": "dist/index.d.ts", | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "test": "jest" | ||
| }, |
There was a problem hiding this comment.
Missing "type": "module" for ESM package.
The tsconfig outputs ESNext modules, but the package.json lacks "type": "module". This can cause Node.js to misinterpret .js files as CommonJS.
Proposed fix
{
"name": "@elizaos/plugin-openttt",
"version": "0.1.0",
+ "type": "module",
"description": "OpenTTT Proof-of-Time plugin for ElizaOS — temporal attestation for AI agent transactions",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugin-openttt/package.json` around lines 1 - 10, The package.json
for the `@elizaos/plugin-openttt` package is missing the top-level "type":
"module" field causing Node to treat emitted ESNext .js files as CommonJS;
update the package.json (the object containing "name":
"@elizaos/plugin-openttt", "version", "main": "dist/index.js", "types":
"dist/index.d.ts", etc.) to include "type": "module" at the top level so Node
will load the dist/*.js files as ESM (ensure it's added alongside the existing
fields, not nested).
| ```typescript | ||
| import { openTTTPlugin } from "@elizaos/plugin-openttt"; | ||
| // openTTTPlugin.actions → [generatePot, verifyPot] | ||
| // openTTTPlugin.providers → [timeProvider] | ||
| // openTTTPlugin.evaluators → [potEvaluator] | ||
| ``` |
There was a problem hiding this comment.
Documentation shows incomplete action list.
The API section shows only [generatePot, verifyPot] but queryPot is also registered. Update for accuracy.
Proposed fix
```typescript
import { openTTTPlugin } from "@elizaos/plugin-openttt";
-// openTTTPlugin.actions → [generatePot, verifyPot]
+// openTTTPlugin.actions → [generatePot, verifyPot, queryPot]
// openTTTPlugin.providers → [timeProvider]
// openTTTPlugin.evaluators → [potEvaluator]</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
Verify each finding against the current code and only fix it if needed.
In @packages/plugin-openttt/README.md around lines 82 - 87, Update the README
example so the documented action list for openTTTPlugin matches the registered
actions: include queryPot alongside generatePot and verifyPot in the
openTTTPlugin.actions comment (i.e., change the comment showing [generatePot,
verifyPot] to [generatePot, verifyPot, queryPot]) so the API section accurately
reflects the registered actions.
</details>
<!-- fingerprinting:phantom:poseidon:ocelot -->
<!-- This is an auto-generated comment by CodeRabbit -->
| const potHashArg = options?.pot_hash as string | undefined; | ||
| // Try to extract potHash from message text (e.g. "verify pot abc123") | ||
| const textMatch = (message.content?.text as string ?? "").match(/\b([0-9a-f]{32,})\b/i); | ||
| const potHashFromText = textMatch?.[1]; |
There was a problem hiding this comment.
Use strict SHA-256 hash parsing and normalize case
At Line 69, \b([0-9a-f]{32,})\b is too permissive and may capture unrelated hex text. Since potHash is SHA-256 hex, parse exactly 64 hex chars and normalize to lowercase for cache-key consistency.
Suggested fix
- const potHashArg = options?.pot_hash as string | undefined;
+ const potHashArg =
+ typeof options?.pot_hash === "string" ? options.pot_hash.toLowerCase() : undefined;
...
- const textMatch = (message.content?.text as string ?? "").match(/\b([0-9a-f]{32,})\b/i);
- const potHashFromText = textMatch?.[1];
+ const textMatch = (message.content?.text as string ?? "").match(/\b([0-9a-f]{64})\b/i);
+ const potHashFromText = textMatch?.[1]?.toLowerCase();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const potHashArg = options?.pot_hash as string | undefined; | |
| // Try to extract potHash from message text (e.g. "verify pot abc123") | |
| const textMatch = (message.content?.text as string ?? "").match(/\b([0-9a-f]{32,})\b/i); | |
| const potHashFromText = textMatch?.[1]; | |
| const potHashArg = | |
| typeof options?.pot_hash === "string" ? options.pot_hash.toLowerCase() : undefined; | |
| // Try to extract potHash from message text (e.g. "verify pot abc123") | |
| const textMatch = (message.content?.text as string ?? "").match(/\b([0-9a-f]{64})\b/i); | |
| const potHashFromText = textMatch?.[1]?.toLowerCase(); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugin-openttt/src/actions/verifyPot.ts` around lines 67 - 70, The
SHA-256 pot hash extraction is too permissive: replace the loose regex
(currently matching 32+ hex chars via textMatch/potHashFromText in verifyPot.ts)
with an exact 64-hex-character match (use a regex that enforces 64 hex chars)
and normalize the resulting value to lowercase for consistent cache keys; also
normalize potHashArg (options?.pot_hash) to lowercase when using it so both
sources produce the same canonical lowercase 64-char hash string.
| // Verify Ed25519 signature — reject tampered tokens | ||
| if (!pot.signature) { | ||
| const result: VerifyResult = { | ||
| valid: false, | ||
| reason: "PoT token has no signature. Token was not generated by a trusted issuer.", | ||
| }; | ||
| if (callback) await callback({ text: `Verification FAILED: ${result.reason}`, content: { result } }); | ||
| return { success: false, error: result.reason, data: { result } }; | ||
| } | ||
| const sigValid = PotSigner.verifyPotSignature(pot.potHash, pot.signature); | ||
| if (!sigValid) { | ||
| const result: VerifyResult = { | ||
| valid: false, | ||
| reason: "Ed25519 signature verification failed. Token may have been tampered with.", | ||
| }; | ||
| if (callback) await callback({ text: `Verification FAILED: ${result.reason}`, content: { result } }); | ||
| return { success: false, error: result.reason, data: { result } }; | ||
| } | ||
|
|
||
| // Check age | ||
| const vt = await getVerifiedTime(); | ||
| const age_ms = vt.timestamp - pot.timestamp; | ||
|
|
There was a problem hiding this comment.
Bind verified signature to token payload fields
At Line 117, verification only checks pot.potHash against pot.signature, but expiry and drift decisions use mutable fields (pot.timestamp, pot.sources, pot.agent_id). A tampered token can keep a valid signed hash while altering those fields and still pass policy checks.
Suggested fix
+import { createHash } from "node:crypto";
...
+ // Validate required fields before policy checks
+ if (
+ typeof pot.potHash !== "string" ||
+ typeof pot.timestamp !== "number" ||
+ !Number.isFinite(pot.timestamp) ||
+ typeof pot.agent_id !== "string" ||
+ !Array.isArray(pot.sources)
+ ) {
+ const result: VerifyResult = {
+ valid: false,
+ reason: "PoT token payload is malformed.",
+ };
+ if (callback) await callback({ text: `Verification FAILED: ${result.reason}`, content: { result } });
+ return { success: false, error: result.reason, data: { result } };
+ }
+
+ // Recompute canonical hash from signed fields (must match issuance format)
+ const canonical = `${pot.agent_id}:${pot.timestamp}:${pot.sources.join(",")}:${pot.nonce}`;
+ const expectedHash = createHash("sha256").update(canonical).digest("hex");
+ if (expectedHash !== pot.potHash) {
+ const result: VerifyResult = {
+ valid: false,
+ reason: "PoT hash does not match token payload. Token may be tampered.",
+ };
+ if (callback) await callback({ text: `Verification FAILED: ${result.reason}`, content: { result } });
+ return { success: false, error: result.reason, data: { result } };
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugin-openttt/src/actions/verifyPot.ts` around lines 108 - 130, The
current signature check calls PotSigner.verifyPotSignature(pot.potHash,
pot.signature) but later uses mutable fields pot.timestamp, pot.sources, and
pot.agent_id for expiry/drift decisions, allowing tampering; update verifyPot
logic to bind the signature to the actual payload fields by re-computing the
canonical payload hash from the full token fields (at least pot.potHash plus
pot.timestamp, pot.sources, pot.agent_id — or serialize the canonical payload
used at signing) and verify the signature against that recomputed hash via
PotSigner.verifyPotSignature (or a new PotSigner.verifySignedPayload) before
performing age/checks (getVerifiedTime(), age_ms). Ensure the verification path
rejects tokens where the signed payload does not include those mutable fields.
| // Check if a PoT was generated for this message using module-level cache | ||
| const potKey = `openttt:pot:${message.id}`; | ||
| const cached = potCacheGet(potKey); |
There was a problem hiding this comment.
Critical: Cache key mismatch — evaluator will never find PoT tokens.
generatePot stores tokens with key openttt:pot:${pot.potHash} (see generatePot.ts:127), but this evaluator looks up openttt:pot:${message.id}. These keys never match, so the evaluator always emits false "missing PoT" warnings.
Use the same resolution pattern as verifyPot and queryPot: look up via openttt:last:${agentId} pointer.
Proposed fix
handler: async (
- _runtime: IAgentRuntime,
+ runtime: IAgentRuntime,
message: Memory,
_state?: State
): Promise<ActionResult | void | undefined> => {
- // Check if a PoT was generated for this message using module-level cache
- const potKey = `openttt:pot:${message.id}`;
- const cached = potCacheGet(potKey);
+ // Resolve PoT via last-generated pointer (same pattern as verifyPot/queryPot)
+ const agentId = (runtime.agentId ?? "unknown") as string;
+ const lastHash = potCacheGet(`openttt:last:${agentId}`);
+ const cached = lastHash ? potCacheGet(`openttt:pot:${lastHash}`) : null;
if (cached) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugin-openttt/src/evaluators/potEvaluator.ts` around lines 39 - 41,
The evaluator uses potCacheGet with key `openttt:pot:${message.id}` which never
matches the key generated by generatePot (`openttt:pot:${pot.potHash}`), so it
always misses PoT tokens; update the lookup in potEvaluator to follow the same
resolution used by verifyPot/queryPot: read the pointer
`openttt:last:${agentId}` (resolve agentId from the message or context), use
that pointer value (potHash) to build the real key `openttt:pot:${potHash}`,
then call potCacheGet with that key (keeping existing potCacheGet usage and
error handling). Ensure you reference the same pointer resolution logic as in
verifyPot/queryPot and remove reliance on message.id.
…package.json cleanup - generatePot: lazy-init PotSigner via getOrCreateSigner(runtime) reads OPENTTT_PRIVATE_KEY from runtime settings for cross-restart continuity; verifyPotSignature is self-contained via issuerPubKey - setInterval: add .unref() to prevent keeping event loop alive on idle - package.json: type:module, exports map, files field, peerDeps >=2.0.0 remove @types/node direct dep (conflicts with monorepo root override) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…queryPot verifyPot.ts: - Recompute potHash from fields (agent_id:timestamp:sources:nonce) before signature check — detects mutable-field tampering while signature stays valid - Hash regex tightened to exactly 64 hex chars (SHA-256) with toLowerCase() - Add createHash import from node:crypto README.md: - Add queryPot to openTTTPlugin.actions list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…alize potHash, fix README - generatePot.ts: assign setInterval to cleanupInterval then call .unref() explicitly - verifyPot.ts: normalize potHashArg to lowercase via .toLowerCase() to handle mixed-case hashes - README.md: add 'text' language specifier to three bare fenced code blocks (conversation examples) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Hey @shaw @lalalune 👋 Plugin is ready for review! @elizaos/plugin-openttt adds Proof-of-Time (PoT) temporal attestation for AI agent transactions — think of it as a temporal integrity layer: before executing a trade or action, an agent can generate a cryptographically signed timestamp from 4 independent sources (NIST, Apple, Google, Cloudflare), then verify it was issued within the last 5 minutes. ✅ Already merged into awesome-mcp-servers (Finance/DeFi category) Would love your feedback when you get a chance! 🙏 |
Summary
Adds
@elizaos/plugin-openttt— a Proof-of-Time (PoT) plugin that attaches tamper-evident temporal attestations to AI agent transactions using the OpenTTT protocol.Why this matters for ElizaOS agents: Every agent trade or transaction carries a timestamp — but timestamps can be forged. This plugin queries four independent time sources (NIST, Apple, Google, Cloudflare), computes a consensus timestamp, and signs it with Ed25519 — producing a PoT token any counterparty can verify on-chain.
What's included
GENERATE_POTaction — generate a PoT before submitting a tradeVERIFY_POTaction — verify a previously issued PoTQUERY_POTaction — retrieve PoT details by hashTIME_PROVIDERprovider — injects current verified time into agent contextPOT_COVERAGE_EVALUATORevaluator — warns if a trade message lacks PoT coverageTechnical notes
opentttSDK (PotSigner.signPot/verifyPotSignature)potHashas stable cache key (avoidsmessage.idmismatch)potHash-keyed +agentIdlast-pointer cache@elizaos/core2.0.x handler signaturesRelated
openttt@0.1.3Test plan
npm install @elizaos/plugin-opentttin an ElizaOS projectopenTTTPlugininAgentRuntimeGENERATE_POTfires automaticallyVERIFY_POTon the returnedpotHash→ verification passesGreptile Summary
This PR introduces
@elizaos/plugin-openttt, a new ElizaOS plugin that attaches Proof-of-Time (PoT) attestations to agent transactions by querying four independent HTTP time sources (NIST, Apple, Google, Cloudflare), computing a consensus timestamp, and signing it with Ed25519. The plugin adds three actions (GENERATE_POT,VERIFY_POT,QUERY_POT), aTIME_PROVIDERprovider, and aPOT_COVERAGE_EVALUATORevaluator.Key issues found:
potEvaluator: The evaluator'shandlerlooks upopenttt:pot:${message.id}, butgeneratePotstores tokens underopenttt:pot:${pot.potHash}. Sincemessage.idis never used as a cache key anywhere, the evaluator will always fail to find a PoT and unconditionally print the coverage warning, rendering it non-functional. The other three actions (verifyPot,queryPot) correctly use theopenttt:last:{agentId}pointer fallback — the evaluator simply needs to do the same._signer = new PotSigner()regenerates a fresh keypair on every process start. Any PoTs issued before a restart become unverifiable, which significantly undermines the tamper-evident attestation guarantee. The keypair should be derived from a persistent secret or thePotSignaturetype should embed the public key for self-contained verification (worth clarifying in code comments).setIntervalmissing.unref(): The module-level cleanup timer prevents the Node.js process from exiting gracefully when no other work is pending.package.jsonissues:peerDependenciesrange>=0.1.0is far too broad for a 2.x API consumer; missing"type": "module","exports"map, and"files"field;opentttversion in README (0.1.3) disagrees with the declared dependency (^0.2.6). No test files are included despitejestbeing configured as a devDependency.Confidence Score: 2/5
potEvaluatoralways fires its warning regardless of whether a PoT was generated (wrong cache key), making the advertised coverage-check feature entirely non-functional. The ephemeral-signer design means PoTs cannot be verified after any process restart, which is a significant reliability concern for an attestation system. Multiplepackage.jsonissues would cause resolution or publishing problems in the monorepo. These require concrete fixes before the plugin delivers on its stated goals.packages/plugin-openttt/src/evaluators/potEvaluator.ts(broken cache key),packages/plugin-openttt/src/actions/generatePot.ts(ephemeral signer, unref'd timer),packages/plugin-openttt/package.json(missing fields, broad peer range)Important Files Changed
openttt:pot:${message.id}but PoTs are stored underopenttt:pot:${pot.potHash}— the evaluator will unconditionally warn even when a PoT was just generated, making it entirely non-functional.setIntervalcleanup timer is missing.unref(), preventing graceful process exit."type": "module","exports"map, and"files"field.peerDependenciesrange>=0.1.0is far too broad for a 2.x API consumer. README version (openttt@0.1.3) disagrees with declared dependency (^0.2.6).Sequence Diagram
sequenceDiagram participant User participant Agent as ElizaOS Agent participant GP as GENERATE_POT participant TP as TimeProvider participant Sources as NIST/Apple/Google/Cloudflare participant Cache as potCache (module-level Map) participant VP as VERIFY_POT participant PE as potEvaluator User->>Agent: "Generate a proof of time before this trade" Agent->>GP: handler() GP->>TP: getVerifiedTime() TP->>Sources: HEAD request (parallel, 3s timeout) Sources-->>TP: Date headers TP-->>GP: VerifiedTime { timestamp, sources, consensus, deviation_ms } GP->>GP: randomBytes(16) → nonce GP->>GP: SHA-256(agentId:timestamp:sources:nonce) → potHash GP->>GP: _signer.signPot(potHash) → PotSignature GP->>Cache: set openttt:pot:{potHash} GP->>Cache: set openttt:last:{agentId} GP-->>Agent: PoTToken + response text User->>Agent: "Verify the proof of time on my last transaction" Agent->>VP: handler() VP->>Cache: get openttt:last:{agentId} → potHash VP->>Cache: get openttt:pot:{potHash} → PoTToken VP->>VP: PotSigner.verifyPotSignature(potHash, signature) VP->>TP: getVerifiedTime() → current time VP->>VP: check age < 5 min VP-->>Agent: VerifyResult { valid, reason, age_ms } Note over PE,Cache: BUG: evaluator looks up openttt:pot:{message.id}<br/>but cache key is openttt:pot:{potHash} — always misses Agent->>PE: handler(message) PE->>Cache: get openttt:pot:{message.id} ← wrong key Cache-->>PE: null (always) PE-->>Agent: ⚠ warns every timeReviews (1): Last reviewed commit: "feat: add @elizaos/plugin-openttt — Proo..." | Re-trigger Greptile
Summary by CodeRabbit
New Features
Documentation