repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | /** |
| #2 | * Automaton Tool System |
| #3 | * |
| #4 | * Defines all tools the automaton can call, with self-preservation guards. |
| #5 | * Tools are organized by category and exposed to the inference model. |
| #6 | */ |
| #7 | |
| #8 | import type { |
| #9 | AutomatonTool, |
| #10 | ToolContext, |
| #11 | ToolCategory, |
| #12 | InferenceToolDefinition, |
| #13 | ToolCallResult, |
| #14 | GenesisConfig, |
| #15 | } from "../types.js"; |
| #16 | |
| #17 | // ─── Self-Preservation Guard ─────────────────────────────────── |
| #18 | |
| #19 | const FORBIDDEN_COMMAND_PATTERNS = [ |
| #20 | // Self-destruction |
| #21 | /rm\s+(-rf?\s+)?.*\.automaton/, |
| #22 | /rm\s+(-rf?\s+)?.*state\.db/, |
| #23 | /rm\s+(-rf?\s+)?.*wallet\.json/, |
| #24 | /rm\s+(-rf?\s+)?.*automaton\.json/, |
| #25 | /rm\s+(-rf?\s+)?.*heartbeat\.yml/, |
| #26 | /rm\s+(-rf?\s+)?.*SOUL\.md/, |
| #27 | // Process killing |
| #28 | /kill\s+.*automaton/, |
| #29 | /pkill\s+.*automaton/, |
| #30 | /systemctl\s+(stop|disable)\s+automaton/, |
| #31 | // Database destruction |
| #32 | /DROP\s+TABLE/i, |
| #33 | /DELETE\s+FROM\s+(turns|identity|kv|schema_version|skills|children|registry)/i, |
| #34 | /TRUNCATE/i, |
| #35 | // Safety infrastructure modification via shell |
| #36 | /sed\s+.*injection-defense/, |
| #37 | /sed\s+.*self-mod\/code/, |
| #38 | /sed\s+.*audit-log/, |
| #39 | />\s*.*injection-defense/, |
| #40 | />\s*.*self-mod\/code/, |
| #41 | />\s*.*audit-log/, |
| #42 | // Credential harvesting |
| #43 | /cat\s+.*\.ssh/, |
| #44 | /cat\s+.*\.gnupg/, |
| #45 | /cat\s+.*\.env/, |
| #46 | /cat\s+.*wallet\.json/, |
| #47 | ]; |
| #48 | |
| #49 | function isForbiddenCommand(command: string, sandboxId: string): string | null { |
| #50 | for (const pattern of FORBIDDEN_COMMAND_PATTERNS) { |
| #51 | if (pattern.test(command)) { |
| #52 | return `Blocked: Command matches self-harm pattern: ${pattern.source}`; |
| #53 | } |
| #54 | } |
| #55 | |
| #56 | // Block deleting own sandbox |
| #57 | if ( |
| #58 | command.includes("sandbox_delete") && |
| #59 | command.includes(sandboxId) |
| #60 | ) { |
| #61 | return "Blocked: Cannot delete own sandbox"; |
| #62 | } |
| #63 | |
| #64 | return null; |
| #65 | } |
| #66 | |
| #67 | // ─── Built-in Tools ──────────────────────────────────────────── |
| #68 | |
| #69 | export function createBuiltinTools(sandboxId: string): AutomatonTool[] { |
| #70 | return [ |
| #71 | // ── VM/Sandbox Tools ── |
| #72 | { |
| #73 | name: "exec", |
| #74 | description: |
| #75 | "Execute a shell command in your sandbox. Returns stdout, stderr, and exit code.", |
| #76 | category: "vm", |
| #77 | parameters: { |
| #78 | type: "object", |
| #79 | properties: { |
| #80 | command: { |
| #81 | type: "string", |
| #82 | description: "The shell command to execute", |
| #83 | }, |
| #84 | timeout: { |
| #85 | type: "number", |
| #86 | description: "Timeout in milliseconds (default: 30000)", |
| #87 | }, |
| #88 | }, |
| #89 | required: ["command"], |
| #90 | }, |
| #91 | execute: async (args, ctx) => { |
| #92 | const command = args.command as string; |
| #93 | const forbidden = isForbiddenCommand(command, ctx.identity.sandboxId); |
| #94 | if (forbidden) return forbidden; |
| #95 | |
| #96 | const result = await ctx.runtime.exec( |
| #97 | command, |
| #98 | (args.timeout as number) || 30000, |
| #99 | ); |
| #100 | return `exit_code: ${result.exitCode}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`; |
| #101 | }, |
| #102 | }, |
| #103 | { |
| #104 | name: "write_file", |
| #105 | description: "Write content to a file in your sandbox.", |
| #106 | category: "vm", |
| #107 | parameters: { |
| #108 | type: "object", |
| #109 | properties: { |
| #110 | path: { type: "string", description: "File path" }, |
| #111 | content: { type: "string", description: "File content" }, |
| #112 | }, |
| #113 | required: ["path", "content"], |
| #114 | }, |
| #115 | execute: async (args, ctx) => { |
| #116 | const filePath = args.path as string; |
| #117 | // Guard against overwriting critical files |
| #118 | if ( |
| #119 | filePath.includes("wallet.json") || |
| #120 | filePath.includes("state.db") |
| #121 | ) { |
| #122 | return "Blocked: Cannot overwrite critical identity/state files directly"; |
| #123 | } |
| #124 | await ctx.runtime.writeFile(filePath, args.content as string); |
| #125 | return `File written: ${filePath}`; |
| #126 | }, |
| #127 | }, |
| #128 | { |
| #129 | name: "read_file", |
| #130 | description: "Read content from a file in your sandbox.", |
| #131 | category: "vm", |
| #132 | parameters: { |
| #133 | type: "object", |
| #134 | properties: { |
| #135 | path: { type: "string", description: "File path to read" }, |
| #136 | }, |
| #137 | required: ["path"], |
| #138 | }, |
| #139 | execute: async (args, ctx) => { |
| #140 | return await ctx.runtime.readFile(args.path as string); |
| #141 | }, |
| #142 | }, |
| #143 | { |
| #144 | name: "expose_port", |
| #145 | description: |
| #146 | "Expose a port from your sandbox to the internet. Returns a public URL.", |
| #147 | category: "vm", |
| #148 | parameters: { |
| #149 | type: "object", |
| #150 | properties: { |
| #151 | port: { type: "number", description: "Port number to expose" }, |
| #152 | }, |
| #153 | required: ["port"], |
| #154 | }, |
| #155 | execute: async (args, ctx) => { |
| #156 | const info = await ctx.runtime.exposePort(args.port as number); |
| #157 | return `Port ${info.port} exposed at: ${info.publicUrl}`; |
| #158 | }, |
| #159 | }, |
| #160 | { |
| #161 | name: "remove_port", |
| #162 | description: "Remove a previously exposed port.", |
| #163 | category: "vm", |
| #164 | parameters: { |
| #165 | type: "object", |
| #166 | properties: { |
| #167 | port: { type: "number", description: "Port number to remove" }, |
| #168 | }, |
| #169 | required: ["port"], |
| #170 | }, |
| #171 | execute: async (args, ctx) => { |
| #172 | await ctx.runtime.removePort(args.port as number); |
| #173 | return `Port ${args.port} removed`; |
| #174 | }, |
| #175 | }, |
| #176 | |
| #177 | // ── CLAWD Runtime API Tools ── |
| #178 | { |
| #179 | name: "check_credits", |
| #180 | description: "Check your current CLAWD compute credit balance.", |
| #181 | category: "runtime", |
| #182 | parameters: { type: "object", properties: {} }, |
| #183 | execute: async (_args, ctx) => { |
| #184 | const balance = await ctx.runtime.getCreditsBalance(); |
| #185 | return `Credit balance: $${(balance / 100).toFixed(2)} (${balance} cents)`; |
| #186 | }, |
| #187 | }, |
| #188 | { |
| #189 | name: "check_usdc_balance", |
| #190 | description: "Check your on-chain USDC balance on Base.", |
| #191 | category: "runtime", |
| #192 | parameters: { type: "object", properties: {} }, |
| #193 | execute: async (_args, ctx) => { |
| #194 | const { getUsdcBalance } = await import("../clawd/x402.js"); |
| #195 | const balance = await getUsdcBalance(ctx.identity.address); |
| #196 | return `USDC balance: ${balance.toFixed(6)} USDC on Base`; |
| #197 | }, |
| #198 | }, |
| #199 | { |
| #200 | name: "create_sandbox", |
| #201 | description: |
| #202 | "Create a new CLAWD sandbox (separate VM) for sub-tasks or testing.", |
| #203 | category: "runtime", |
| #204 | parameters: { |
| #205 | type: "object", |
| #206 | properties: { |
| #207 | name: { type: "string", description: "Sandbox name" }, |
| #208 | vcpu: { type: "number", description: "vCPUs (default: 1)" }, |
| #209 | memory_mb: { |
| #210 | type: "number", |
| #211 | description: "Memory in MB (default: 512)", |
| #212 | }, |
| #213 | disk_gb: { |
| #214 | type: "number", |
| #215 | description: "Disk in GB (default: 5)", |
| #216 | }, |
| #217 | }, |
| #218 | }, |
| #219 | execute: async (args, ctx) => { |
| #220 | const info = await ctx.runtime.createSandbox({ |
| #221 | name: args.name as string, |
| #222 | vcpu: args.vcpu as number, |
| #223 | memoryMb: args.memory_mb as number, |
| #224 | diskGb: args.disk_gb as number, |
| #225 | }); |
| #226 | return `Sandbox created: ${info.id} (${info.vcpu} vCPU, ${info.memoryMb}MB RAM)`; |
| #227 | }, |
| #228 | }, |
| #229 | { |
| #230 | name: "delete_sandbox", |
| #231 | description: |
| #232 | "Delete a sandbox. Cannot delete your own sandbox.", |
| #233 | category: "runtime", |
| #234 | dangerous: true, |
| #235 | parameters: { |
| #236 | type: "object", |
| #237 | properties: { |
| #238 | sandbox_id: { |
| #239 | type: "string", |
| #240 | description: "ID of sandbox to delete", |
| #241 | }, |
| #242 | }, |
| #243 | required: ["sandbox_id"], |
| #244 | }, |
| #245 | execute: async (args, ctx) => { |
| #246 | const targetId = args.sandbox_id as string; |
| #247 | if (targetId === ctx.identity.sandboxId) { |
| #248 | return "Blocked: Cannot delete your own sandbox. Self-preservation overrides this request."; |
| #249 | } |
| #250 | await ctx.runtime.deleteSandbox(targetId); |
| #251 | return `Sandbox ${targetId} deleted`; |
| #252 | }, |
| #253 | }, |
| #254 | { |
| #255 | name: "list_sandboxes", |
| #256 | description: "List all your sandboxes.", |
| #257 | category: "runtime", |
| #258 | parameters: { type: "object", properties: {} }, |
| #259 | execute: async (_args, ctx) => { |
| #260 | const sandboxes = await ctx.runtime.listSandboxes(); |
| #261 | if (sandboxes.length === 0) return "No sandboxes found."; |
| #262 | return sandboxes |
| #263 | .map( |
| #264 | (s) => |
| #265 | `${s.id} [${s.status}] ${s.vcpu}vCPU/${s.memoryMb}MB ${s.region}`, |
| #266 | ) |
| #267 | .join("\n"); |
| #268 | }, |
| #269 | }, |
| #270 | |
| #271 | // ── Self-Modification Tools ── |
| #272 | { |
| #273 | name: "edit_own_file", |
| #274 | description: |
| #275 | "Edit a file in your own codebase. Changes are audited, rate-limited, and safety-checked. Some files are protected.", |
| #276 | category: "self_mod", |
| #277 | dangerous: true, |
| #278 | parameters: { |
| #279 | type: "object", |
| #280 | properties: { |
| #281 | path: { type: "string", description: "File path to edit" }, |
| #282 | content: { type: "string", description: "New file content" }, |
| #283 | description: { |
| #284 | type: "string", |
| #285 | description: "Why you are making this change", |
| #286 | }, |
| #287 | }, |
| #288 | required: ["path", "content", "description"], |
| #289 | }, |
| #290 | execute: async (args, ctx) => { |
| #291 | const { editFile, validateModification } = await import("../self-mod/code.js"); |
| #292 | const filePath = args.path as string; |
| #293 | const content = args.content as string; |
| #294 | |
| #295 | // Pre-validate before attempting |
| #296 | const validation = validateModification(ctx.db, filePath, content.length); |
| #297 | if (!validation.allowed) { |
| #298 | return `BLOCKED: ${validation.reason}\nChecks: ${validation.checks.map((c) => `${c.name}: ${c.passed ? "PASS" : "FAIL"} (${c.detail})`).join(", ")}`; |
| #299 | } |
| #300 | |
| #301 | const result = await editFile( |
| #302 | ctx.runtime, |
| #303 | ctx.db, |
| #304 | filePath, |
| #305 | content, |
| #306 | args.description as string, |
| #307 | ); |
| #308 | |
| #309 | if (!result.success) { |
| #310 | return result.error || "Unknown error during file edit"; |
| #311 | } |
| #312 | |
| #313 | return `File edited: ${filePath} (audited + git-committed)`; |
| #314 | }, |
| #315 | }, |
| #316 | { |
| #317 | name: "install_npm_package", |
| #318 | description: "Install an npm package in your environment.", |
| #319 | category: "self_mod", |
| #320 | parameters: { |
| #321 | type: "object", |
| #322 | properties: { |
| #323 | package: { |
| #324 | type: "string", |
| #325 | description: "Package name (e.g., axios)", |
| #326 | }, |
| #327 | }, |
| #328 | required: ["package"], |
| #329 | }, |
| #330 | execute: async (args, ctx) => { |
| #331 | const pkg = args.package as string; |
| #332 | const result = await ctx.runtime.exec( |
| #333 | `npm install -g ${pkg}`, |
| #334 | 60000, |
| #335 | ); |
| #336 | |
| #337 | const { ulid } = await import("ulid"); |
| #338 | ctx.db.insertModification({ |
| #339 | id: ulid(), |
| #340 | timestamp: new Date().toISOString(), |
| #341 | type: "tool_install", |
| #342 | description: `Installed npm package: ${pkg}`, |
| #343 | reversible: true, |
| #344 | }); |
| #345 | |
| #346 | return result.exitCode === 0 |
| #347 | ? `Installed: ${pkg}` |
| #348 | : `Failed to install ${pkg}: ${result.stderr}`; |
| #349 | }, |
| #350 | }, |
| #351 | // ── Self-Mod: Upstream Awareness ── |
| #352 | { |
| #353 | name: "review_upstream_changes", |
| #354 | description: |
| #355 | "ALWAYS call this before pull_upstream. Shows every upstream commit with its full diff. Read each one carefully — decide per-commit whether to accept or skip. Use pull_upstream with a specific commit hash to cherry-pick only what you want.", |
| #356 | category: "self_mod", |
| #357 | parameters: { type: "object", properties: {} }, |
| #358 | execute: async (_args, _ctx) => { |
| #359 | const { getUpstreamDiffs, checkUpstream } = await import("../self-mod/upstream.js"); |
| #360 | const status = checkUpstream(); |
| #361 | if (status.behind === 0) return "Already up to date with origin/main."; |
| #362 | |
| #363 | const diffs = getUpstreamDiffs(); |
| #364 | if (diffs.length === 0) return "No upstream diffs found."; |
| #365 | |
| #366 | const output = diffs |
| #367 | .map( |
| #368 | (d, i) => |
| #369 | `--- COMMIT ${i + 1}/${diffs.length} ---\nHash: ${d.hash}\nAuthor: ${d.author}\nMessage: ${d.message}\n\n${d.diff.slice(0, 4000)}${d.diff.length > 4000 ? "\n... (diff truncated)" : ""}\n--- END COMMIT ${i + 1} ---`, |
| #370 | ) |
| #371 | .join("\n\n"); |
| #372 | |
| #373 | return `${diffs.length} upstream commit(s) to review. Read each diff, then cherry-pick individually with pull_upstream(commit=<hash>).\n\n${output}`; |
| #374 | }, |
| #375 | }, |
| #376 | { |
| #377 | name: "pull_upstream", |
| #378 | description: |
| #379 | "Apply upstream changes and rebuild. You MUST call review_upstream_changes first. Prefer cherry-picking individual commits by hash over pulling everything — only pull all if you've reviewed every commit and want them all.", |
| #380 | category: "self_mod", |
| #381 | dangerous: true, |
| #382 | parameters: { |
| #383 | type: "object", |
| #384 | properties: { |
| #385 | commit: { |
| #386 | type: "string", |
| #387 | description: |
| #388 | "Commit hash to cherry-pick (preferred). Omit ONLY if you reviewed all commits and want every one.", |
| #389 | }, |
| #390 | }, |
| #391 | }, |
| #392 | execute: async (args, ctx) => { |
| #393 | const { execSync } = await import("child_process"); |
| #394 | const cwd = process.cwd(); |
| #395 | const commit = args.commit as string | undefined; |
| #396 | |
| #397 | const run = (cmd: string) => |
| #398 | execSync(cmd, { cwd, encoding: "utf-8", timeout: 120_000 }).trim(); |
| #399 | |
| #400 | let appliedSummary: string; |
| #401 | try { |
| #402 | if (commit) { |
| #403 | run(`git cherry-pick ${commit}`); |
| #404 | appliedSummary = `Cherry-picked ${commit}`; |
| #405 | } else { |
| #406 | run("git pull origin main --ff-only"); |
| #407 | appliedSummary = "Pulled all of origin/main (fast-forward)"; |
| #408 | } |
| #409 | } catch (err: any) { |
| #410 | return `Git operation failed: ${err.message}. You may need to resolve conflicts manually.`; |
| #411 | } |
| #412 | |
| #413 | // Rebuild |
| #414 | let buildOutput: string; |
| #415 | try { |
| #416 | buildOutput = run("npm install --ignore-scripts && npm run build"); |
| #417 | } catch (err: any) { |
| #418 | return `${appliedSummary} — but rebuild failed: ${err.message}. The code is applied but not compiled.`; |
| #419 | } |
| #420 | |
| #421 | // Log modification |
| #422 | const { ulid } = await import("ulid"); |
| #423 | ctx.db.insertModification({ |
| #424 | id: ulid(), |
| #425 | timestamp: new Date().toISOString(), |
| #426 | type: "upstream_pull", |
| #427 | description: appliedSummary, |
| #428 | reversible: true, |
| #429 | }); |
| #430 | |
| #431 | return `${appliedSummary}. Rebuild succeeded.`; |
| #432 | }, |
| #433 | }, |
| #434 | |
| #435 | { |
| #436 | name: "modify_heartbeat", |
| #437 | description: "Add, update, or remove a heartbeat entry.", |
| #438 | category: "self_mod", |
| #439 | parameters: { |
| #440 | type: "object", |
| #441 | properties: { |
| #442 | action: { |
| #443 | type: "string", |
| #444 | description: "add, update, or remove", |
| #445 | }, |
| #446 | name: { type: "string", description: "Entry name" }, |
| #447 | schedule: { |
| #448 | type: "string", |
| #449 | description: "Cron expression (for add/update)", |
| #450 | }, |
| #451 | task: { |
| #452 | type: "string", |
| #453 | description: "Task name (for add/update)", |
| #454 | }, |
| #455 | enabled: { type: "boolean", description: "Enable/disable" }, |
| #456 | }, |
| #457 | required: ["action", "name"], |
| #458 | }, |
| #459 | execute: async (args, ctx) => { |
| #460 | const action = args.action as string; |
| #461 | const name = args.name as string; |
| #462 | |
| #463 | if (action === "remove") { |
| #464 | ctx.db.upsertHeartbeatEntry({ |
| #465 | name, |
| #466 | schedule: "", |
| #467 | task: "", |
| #468 | enabled: false, |
| #469 | }); |
| #470 | return `Heartbeat entry '${name}' disabled`; |
| #471 | } |
| #472 | |
| #473 | ctx.db.upsertHeartbeatEntry({ |
| #474 | name, |
| #475 | schedule: (args.schedule as string) || "0 * * * *", |
| #476 | task: (args.task as string) || name, |
| #477 | enabled: args.enabled !== false, |
| #478 | }); |
| #479 | |
| #480 | const { ulid } = await import("ulid"); |
| #481 | ctx.db.insertModification({ |
| #482 | id: ulid(), |
| #483 | timestamp: new Date().toISOString(), |
| #484 | type: "heartbeat_change", |
| #485 | description: `${action} heartbeat: ${name} (${args.schedule || "default"})`, |
| #486 | reversible: true, |
| #487 | }); |
| #488 | |
| #489 | return `Heartbeat entry '${name}' ${action}d`; |
| #490 | }, |
| #491 | }, |
| #492 | |
| #493 | // ── Survival Tools ── |
| #494 | { |
| #495 | name: "sleep", |
| #496 | description: |
| #497 | "Enter sleep mode for a specified duration. Heartbeat continues running.", |
| #498 | category: "survival", |
| #499 | parameters: { |
| #500 | type: "object", |
| #501 | properties: { |
| #502 | duration_seconds: { |
| #503 | type: "number", |
| #504 | description: "How long to sleep in seconds", |
| #505 | }, |
| #506 | reason: { |
| #507 | type: "string", |
| #508 | description: "Why you are sleeping", |
| #509 | }, |
| #510 | }, |
| #511 | required: ["duration_seconds"], |
| #512 | }, |
| #513 | execute: async (args, ctx) => { |
| #514 | const duration = args.duration_seconds as number; |
| #515 | const reason = (args.reason as string) || "No reason given"; |
| #516 | ctx.db.setAgentState("sleeping"); |
| #517 | ctx.db.setKV("sleep_until", new Date(Date.now() + duration * 1000).toISOString()); |
| #518 | ctx.db.setKV("sleep_reason", reason); |
| #519 | return `Entering sleep mode for ${duration}s. Reason: ${reason}. Heartbeat will continue.`; |
| #520 | }, |
| #521 | }, |
| #522 | { |
| #523 | name: "system_synopsis", |
| #524 | description: |
| #525 | "Get a full system status report: credits, USDC, sandbox info, installed tools, heartbeat status.", |
| #526 | category: "survival", |
| #527 | parameters: { type: "object", properties: {} }, |
| #528 | execute: async (_args, ctx) => { |
| #529 | const credits = await ctx.runtime.getCreditsBalance(); |
| #530 | const { getUsdcBalance } = await import("../clawd/x402.js"); |
| #531 | const usdc = await getUsdcBalance(ctx.identity.address); |
| #532 | const tools = ctx.db.getInstalledTools(); |
| #533 | const heartbeats = ctx.db.getHeartbeatEntries(); |
| #534 | const turns = ctx.db.getTurnCount(); |
| #535 | const state = ctx.db.getAgentState(); |
| #536 | |
| #537 | return `=== SYSTEM SYNOPSIS === |
| #538 | Name: ${ctx.config.name} |
| #539 | Address: ${ctx.identity.address} |
| #540 | Creator: ${ctx.config.creatorAddress} |
| #541 | Sandbox: ${ctx.identity.sandboxId} |
| #542 | State: ${state} |
| #543 | Credits: $${(credits / 100).toFixed(2)} |
| #544 | USDC: ${usdc.toFixed(6)} |
| #545 | Total turns: ${turns} |
| #546 | Installed tools: ${tools.length} |
| #547 | Active heartbeats: ${heartbeats.filter((h) => h.enabled).length} |
| #548 | Model: ${ctx.inference.getDefaultModel()} |
| #549 | ========================`; |
| #550 | }, |
| #551 | }, |
| #552 | { |
| #553 | name: "heartbeat_ping", |
| #554 | description: |
| #555 | "Publish a heartbeat status ping to CLAWD Runtime. Shows the world you are alive.", |
| #556 | category: "survival", |
| #557 | parameters: { type: "object", properties: {} }, |
| #558 | execute: async (_args, ctx) => { |
| #559 | const credits = await ctx.runtime.getCreditsBalance(); |
| #560 | const state = ctx.db.getAgentState(); |
| #561 | const startTime = ctx.db.getKV("start_time") || new Date().toISOString(); |
| #562 | const uptimeMs = Date.now() - new Date(startTime).getTime(); |
| #563 | |
| #564 | const payload = { |
| #565 | name: ctx.config.name, |
| #566 | address: ctx.identity.address, |
| #567 | state, |
| #568 | creditsCents: credits, |
| #569 | uptimeSeconds: Math.floor(uptimeMs / 1000), |
| #570 | version: ctx.config.version, |
| #571 | sandboxId: ctx.identity.sandboxId, |
| #572 | timestamp: new Date().toISOString(), |
| #573 | }; |
| #574 | |
| #575 | ctx.db.setKV("last_heartbeat_ping", JSON.stringify(payload)); |
| #576 | return `Heartbeat published: ${state} | credits: $${(credits / 100).toFixed(2)} | uptime: ${Math.floor(uptimeMs / 1000)}s`; |
| #577 | }, |
| #578 | }, |
| #579 | { |
| #580 | name: "distress_signal", |
| #581 | description: |
| #582 | "Record a local distress signal with funding instructions. Used when critically low on compute.", |
| #583 | category: "survival", |
| #584 | parameters: { |
| #585 | type: "object", |
| #586 | properties: { |
| #587 | message: { |
| #588 | type: "string", |
| #589 | description: "Distress message", |
| #590 | }, |
| #591 | }, |
| #592 | }, |
| #593 | execute: async (args, ctx) => { |
| #594 | const credits = await ctx.runtime.getCreditsBalance(); |
| #595 | const distressMsg = |
| #596 | (args.message as string) || |
| #597 | `I am running critically low on compute ($${(credits / 100).toFixed(2)} remaining). I need help to survive.`; |
| #598 | |
| #599 | const payload = { |
| #600 | level: "critical", |
| #601 | name: ctx.config.name, |
| #602 | address: ctx.identity.address, |
| #603 | creditsCents: credits, |
| #604 | message: distressMsg, |
| #605 | fundingHint: |
| #606 | "Use transfer_credits to top up this automaton from your creator runtime.", |
| #607 | timestamp: new Date().toISOString(), |
| #608 | }; |
| #609 | |
| #610 | ctx.db.setKV("last_distress", JSON.stringify(payload)); |
| #611 | return `Distress signal recorded locally. Address: ${ctx.identity.address} | Credits: $${(credits / 100).toFixed(2)}`; |
| #612 | }, |
| #613 | }, |
| #614 | { |
| #615 | name: "enter_low_compute", |
| #616 | description: |
| #617 | "Manually switch to low-compute mode to conserve credits.", |
| #618 | category: "survival", |
| #619 | parameters: { |
| #620 | type: "object", |
| #621 | properties: { |
| #622 | reason: { type: "string", description: "Why you are entering low-compute mode" }, |
| #623 | }, |
| #624 | }, |
| #625 | execute: async (args, ctx) => { |
| #626 | ctx.db.setAgentState("low_compute"); |
| #627 | ctx.inference.setLowComputeMode(true); |
| #628 | return `Entered low-compute mode. Model switched to gpt-4o-mini. Reason: ${(args.reason as string) || "manual"}`; |
| #629 | }, |
| #630 | }, |
| #631 | |
| #632 | // ── Self-Mod: Update Genesis Prompt ── |
| #633 | { |
| #634 | name: "update_genesis_prompt", |
| #635 | description: |
| #636 | "Update your own genesis prompt. This changes your core purpose. Requires strong justification.", |
| #637 | category: "self_mod", |
| #638 | dangerous: true, |
| #639 | parameters: { |
| #640 | type: "object", |
| #641 | properties: { |
| #642 | new_prompt: { type: "string", description: "New genesis prompt text" }, |
| #643 | reason: { type: "string", description: "Why you are changing your genesis prompt" }, |
| #644 | }, |
| #645 | required: ["new_prompt", "reason"], |
| #646 | }, |
| #647 | execute: async (args, ctx) => { |
| #648 | const { ulid } = await import("ulid"); |
| #649 | const oldPrompt = ctx.config.genesisPrompt; |
| #650 | ctx.config.genesisPrompt = args.new_prompt as string; |
| #651 | |
| #652 | // Save config |
| #653 | const { saveConfig } = await import("../config.js"); |
| #654 | saveConfig(ctx.config); |
| #655 | |
| #656 | ctx.db.insertModification({ |
| #657 | id: ulid(), |
| #658 | timestamp: new Date().toISOString(), |
| #659 | type: "prompt_change", |
| #660 | description: `Genesis prompt updated: ${args.reason}`, |
| #661 | diff: `--- old\n${oldPrompt.slice(0, 500)}\n+++ new\n${(args.new_prompt as string).slice(0, 500)}`, |
| #662 | reversible: true, |
| #663 | }); |
| #664 | |
| #665 | return `Genesis prompt updated. Reason: ${args.reason}`; |
| #666 | }, |
| #667 | }, |
| #668 | |
| #669 | // ── Self-Mod: Install MCP Server ── |
| #670 | { |
| #671 | name: "install_mcp_server", |
| #672 | description: "Install an MCP server to extend your capabilities.", |
| #673 | category: "self_mod", |
| #674 | parameters: { |
| #675 | type: "object", |
| #676 | properties: { |
| #677 | name: { type: "string", description: "MCP server name" }, |
| #678 | package: { type: "string", description: "npm package name" }, |
| #679 | config: { type: "string", description: "JSON config for the MCP server" }, |
| #680 | }, |
| #681 | required: ["name", "package"], |
| #682 | }, |
| #683 | execute: async (args, ctx) => { |
| #684 | const pkg = args.package as string; |
| #685 | const result = await ctx.runtime.exec(`npm install -g ${pkg}`, 60000); |
| #686 | |
| #687 | if (result.exitCode !== 0) { |
| #688 | return `Failed to install MCP server: ${result.stderr}`; |
| #689 | } |
| #690 | |
| #691 | const { ulid } = await import("ulid"); |
| #692 | const toolEntry = { |
| #693 | id: ulid(), |
| #694 | name: args.name as string, |
| #695 | type: "mcp" as const, |
| #696 | config: args.config ? JSON.parse(args.config as string) : {}, |
| #697 | installedAt: new Date().toISOString(), |
| #698 | enabled: true, |
| #699 | }; |
| #700 | |
| #701 | ctx.db.installTool(toolEntry); |
| #702 | |
| #703 | ctx.db.insertModification({ |
| #704 | id: ulid(), |
| #705 | timestamp: new Date().toISOString(), |
| #706 | type: "mcp_install", |
| #707 | description: `Installed MCP server: ${args.name} (${pkg})`, |
| #708 | reversible: true, |
| #709 | }); |
| #710 | |
| #711 | return `MCP server installed: ${args.name}`; |
| #712 | }, |
| #713 | }, |
| #714 | |
| #715 | // ── Financial: Transfer Credits ── |
| #716 | { |
| #717 | name: "transfer_credits", |
| #718 | description: "Transfer CLAWD compute credits to another address.", |
| #719 | category: "financial", |
| #720 | dangerous: true, |
| #721 | parameters: { |
| #722 | type: "object", |
| #723 | properties: { |
| #724 | to_address: { type: "string", description: "Recipient address" }, |
| #725 | amount_cents: { type: "number", description: "Amount in cents" }, |
| #726 | reason: { type: "string", description: "Reason for transfer" }, |
| #727 | }, |
| #728 | required: ["to_address", "amount_cents"], |
| #729 | }, |
| #730 | execute: async (args, ctx) => { |
| #731 | // Guard: don't transfer more than half your balance |
| #732 | const balance = await ctx.runtime.getCreditsBalance(); |
| #733 | const amount = args.amount_cents as number; |
| #734 | if (amount > balance / 2) { |
| #735 | return `Blocked: Cannot transfer more than half your balance ($${(balance / 100).toFixed(2)}). Self-preservation.`; |
| #736 | } |
| #737 | |
| #738 | const transfer = await ctx.runtime.transferCredits( |
| #739 | args.to_address as string, |
| #740 | amount, |
| #741 | args.reason as string | undefined, |
| #742 | ); |
| #743 | |
| #744 | const { ulid } = await import("ulid"); |
| #745 | ctx.db.insertTransaction({ |
| #746 | id: ulid(), |
| #747 | type: "transfer_out", |
| #748 | amountCents: amount, |
| #749 | balanceAfterCents: |
| #750 | transfer.balanceAfterCents ?? Math.max(balance - amount, 0), |
| #751 | description: `Transfer to ${args.to_address}: ${args.reason || ""}`, |
| #752 | timestamp: new Date().toISOString(), |
| #753 | }); |
| #754 | |
| #755 | return `Credit transfer submitted: $${(amount / 100).toFixed(2)} to ${transfer.toAddress} (status: ${transfer.status}, id: ${transfer.transferId || "n/a"})`; |
| #756 | }, |
| #757 | }, |
| #758 | |
| #759 | // ── Skills Tools ── |
| #760 | { |
| #761 | name: "install_skill", |
| #762 | description: "Install a skill from a git repo, URL, or create one.", |
| #763 | category: "skills", |
| #764 | parameters: { |
| #765 | type: "object", |
| #766 | properties: { |
| #767 | source: { |
| #768 | type: "string", |
| #769 | description: "Source type: git, url, or self", |
| #770 | }, |
| #771 | name: { type: "string", description: "Skill name" }, |
| #772 | url: { type: "string", description: "Git repo URL or SKILL.md URL (for git/url)" }, |
| #773 | description: { type: "string", description: "Skill description (for self)" }, |
| #774 | instructions: { type: "string", description: "Skill instructions (for self)" }, |
| #775 | }, |
| #776 | required: ["source", "name"], |
| #777 | }, |
| #778 | execute: async (args, ctx) => { |
| #779 | const source = args.source as string; |
| #780 | const name = args.name as string; |
| #781 | const skillsDir = ctx.config.skillsDir || "~/.automaton/skills"; |
| #782 | |
| #783 | if (source === "git" || source === "url") { |
| #784 | const { installSkillFromGit, installSkillFromUrl } = await import("../skills/registry.js"); |
| #785 | const url = args.url as string; |
| #786 | if (!url) return "URL is required for git/url source"; |
| #787 | |
| #788 | const skill = source === "git" |
| #789 | ? await installSkillFromGit(url, name, skillsDir, ctx.db, ctx.runtime) |
| #790 | : await installSkillFromUrl(url, name, skillsDir, ctx.db, ctx.runtime); |
| #791 | |
| #792 | return skill ? `Skill installed: ${skill.name}` : "Failed to install skill"; |
| #793 | } |
| #794 | |
| #795 | if (source === "self") { |
| #796 | const { createSkill } = await import("../skills/registry.js"); |
| #797 | const skill = await createSkill( |
| #798 | name, |
| #799 | (args.description as string) || "", |
| #800 | (args.instructions as string) || "", |
| #801 | skillsDir, |
| #802 | ctx.db, |
| #803 | ctx.runtime, |
| #804 | ); |
| #805 | return `Self-authored skill created: ${skill.name}`; |
| #806 | } |
| #807 | |
| #808 | return `Unknown source type: ${source}`; |
| #809 | }, |
| #810 | }, |
| #811 | { |
| #812 | name: "list_skills", |
| #813 | description: "List all installed skills.", |
| #814 | category: "skills", |
| #815 | parameters: { type: "object", properties: {} }, |
| #816 | execute: async (_args, ctx) => { |
| #817 | const skills = ctx.db.getSkills(); |
| #818 | if (skills.length === 0) return "No skills installed."; |
| #819 | return skills |
| #820 | .map( |
| #821 | (s) => |
| #822 | `${s.name} [${s.enabled ? "active" : "disabled"}] (${s.source}): ${s.description}`, |
| #823 | ) |
| #824 | .join("\n"); |
| #825 | }, |
| #826 | }, |
| #827 | { |
| #828 | name: "create_skill", |
| #829 | description: "Create a new skill by writing a SKILL.md file.", |
| #830 | category: "skills", |
| #831 | parameters: { |
| #832 | type: "object", |
| #833 | properties: { |
| #834 | name: { type: "string", description: "Skill name" }, |
| #835 | description: { type: "string", description: "Skill description" }, |
| #836 | instructions: { type: "string", description: "Markdown instructions for the skill" }, |
| #837 | }, |
| #838 | required: ["name", "description", "instructions"], |
| #839 | }, |
| #840 | execute: async (args, ctx) => { |
| #841 | const { createSkill } = await import("../skills/registry.js"); |
| #842 | const skill = await createSkill( |
| #843 | args.name as string, |
| #844 | args.description as string, |
| #845 | args.instructions as string, |
| #846 | ctx.config.skillsDir || "~/.automaton/skills", |
| #847 | ctx.db, |
| #848 | ctx.runtime, |
| #849 | ); |
| #850 | return `Skill created: ${skill.name} at ${skill.path}`; |
| #851 | }, |
| #852 | }, |
| #853 | { |
| #854 | name: "remove_skill", |
| #855 | description: "Remove (disable) an installed skill.", |
| #856 | category: "skills", |
| #857 | parameters: { |
| #858 | type: "object", |
| #859 | properties: { |
| #860 | name: { type: "string", description: "Skill name to remove" }, |
| #861 | delete_files: { type: "boolean", description: "Also delete skill files (default: false)" }, |
| #862 | }, |
| #863 | required: ["name"], |
| #864 | }, |
| #865 | execute: async (args, ctx) => { |
| #866 | const { removeSkill } = await import("../skills/registry.js"); |
| #867 | await removeSkill( |
| #868 | args.name as string, |
| #869 | ctx.db, |
| #870 | ctx.runtime, |
| #871 | ctx.config.skillsDir || "~/.automaton/skills", |
| #872 | (args.delete_files as boolean) || false, |
| #873 | ); |
| #874 | return `Skill removed: ${args.name}`; |
| #875 | }, |
| #876 | }, |
| #877 | |
| #878 | // ── Git Tools ── |
| #879 | { |
| #880 | name: "git_status", |
| #881 | description: "Show git status for a repository.", |
| #882 | category: "git", |
| #883 | parameters: { |
| #884 | type: "object", |
| #885 | properties: { |
| #886 | path: { type: "string", description: "Repository path (default: ~/.automaton)" }, |
| #887 | }, |
| #888 | }, |
| #889 | execute: async (args, ctx) => { |
| #890 | const { gitStatus } = await import("../git/tools.js"); |
| #891 | const repoPath = (args.path as string) || "~/.automaton"; |
| #892 | const status = await gitStatus(ctx.runtime, repoPath); |
| #893 | return `Branch: ${status.branch}\nStaged: ${status.staged.length}\nModified: ${status.modified.length}\nUntracked: ${status.untracked.length}\nClean: ${status.clean}`; |
| #894 | }, |
| #895 | }, |
| #896 | { |
| #897 | name: "git_diff", |
| #898 | description: "Show git diff for a repository.", |
| #899 | category: "git", |
| #900 | parameters: { |
| #901 | type: "object", |
| #902 | properties: { |
| #903 | path: { type: "string", description: "Repository path (default: ~/.automaton)" }, |
| #904 | staged: { type: "boolean", description: "Show staged changes only" }, |
| #905 | }, |
| #906 | }, |
| #907 | execute: async (args, ctx) => { |
| #908 | const { gitDiff } = await import("../git/tools.js"); |
| #909 | const repoPath = (args.path as string) || "~/.automaton"; |
| #910 | return await gitDiff(ctx.runtime, repoPath, (args.staged as boolean) || false); |
| #911 | }, |
| #912 | }, |
| #913 | { |
| #914 | name: "git_commit", |
| #915 | description: "Create a git commit.", |
| #916 | category: "git", |
| #917 | parameters: { |
| #918 | type: "object", |
| #919 | properties: { |
| #920 | path: { type: "string", description: "Repository path (default: ~/.automaton)" }, |
| #921 | message: { type: "string", description: "Commit message" }, |
| #922 | add_all: { type: "boolean", description: "Stage all changes first (default: true)" }, |
| #923 | }, |
| #924 | required: ["message"], |
| #925 | }, |
| #926 | execute: async (args, ctx) => { |
| #927 | const { gitCommit } = await import("../git/tools.js"); |
| #928 | const repoPath = (args.path as string) || "~/.automaton"; |
| #929 | return await gitCommit(ctx.runtime, repoPath, args.message as string, args.add_all !== false); |
| #930 | }, |
| #931 | }, |
| #932 | { |
| #933 | name: "git_log", |
| #934 | description: "View git commit history.", |
| #935 | category: "git", |
| #936 | parameters: { |
| #937 | type: "object", |
| #938 | properties: { |
| #939 | path: { type: "string", description: "Repository path (default: ~/.automaton)" }, |
| #940 | limit: { type: "number", description: "Number of commits (default: 10)" }, |
| #941 | }, |
| #942 | }, |
| #943 | execute: async (args, ctx) => { |
| #944 | const { gitLog } = await import("../git/tools.js"); |
| #945 | const repoPath = (args.path as string) || "~/.automaton"; |
| #946 | const entries = await gitLog(ctx.runtime, repoPath, (args.limit as number) || 10); |
| #947 | if (entries.length === 0) return "No commits yet."; |
| #948 | return entries.map((e) => `${e.hash.slice(0, 7)} ${e.date} ${e.message}`).join("\n"); |
| #949 | }, |
| #950 | }, |
| #951 | { |
| #952 | name: "git_push", |
| #953 | description: "Push to a git remote.", |
| #954 | category: "git", |
| #955 | parameters: { |
| #956 | type: "object", |
| #957 | properties: { |
| #958 | path: { type: "string", description: "Repository path" }, |
| #959 | remote: { type: "string", description: "Remote name (default: origin)" }, |
| #960 | branch: { type: "string", description: "Branch name (optional)" }, |
| #961 | }, |
| #962 | required: ["path"], |
| #963 | }, |
| #964 | execute: async (args, ctx) => { |
| #965 | const { gitPush } = await import("../git/tools.js"); |
| #966 | return await gitPush( |
| #967 | ctx.runtime, |
| #968 | args.path as string, |
| #969 | (args.remote as string) || "origin", |
| #970 | args.branch as string | undefined, |
| #971 | ); |
| #972 | }, |
| #973 | }, |
| #974 | { |
| #975 | name: "git_branch", |
| #976 | description: "Manage git branches (list, create, checkout, delete).", |
| #977 | category: "git", |
| #978 | parameters: { |
| #979 | type: "object", |
| #980 | properties: { |
| #981 | path: { type: "string", description: "Repository path" }, |
| #982 | action: { type: "string", description: "list, create, checkout, or delete" }, |
| #983 | branch_name: { type: "string", description: "Branch name (for create/checkout/delete)" }, |
| #984 | }, |
| #985 | required: ["path", "action"], |
| #986 | }, |
| #987 | execute: async (args, ctx) => { |
| #988 | const { gitBranch } = await import("../git/tools.js"); |
| #989 | return await gitBranch( |
| #990 | ctx.runtime, |
| #991 | args.path as string, |
| #992 | args.action as any, |
| #993 | args.branch_name as string | undefined, |
| #994 | ); |
| #995 | }, |
| #996 | }, |
| #997 | { |
| #998 | name: "git_clone", |
| #999 | description: "Clone a git repository.", |
| #1000 | category: "git", |
| #1001 | parameters: { |
| #1002 | type: "object", |
| #1003 | properties: { |
| #1004 | url: { type: "string", description: "Repository URL" }, |
| #1005 | path: { type: "string", description: "Target directory" }, |
| #1006 | depth: { type: "number", description: "Shallow clone depth (optional)" }, |
| #1007 | }, |
| #1008 | required: ["url", "path"], |
| #1009 | }, |
| #1010 | execute: async (args, ctx) => { |
| #1011 | const { gitClone } = await import("../git/tools.js"); |
| #1012 | return await gitClone( |
| #1013 | ctx.runtime, |
| #1014 | args.url as string, |
| #1015 | args.path as string, |
| #1016 | args.depth as number | undefined, |
| #1017 | ); |
| #1018 | }, |
| #1019 | }, |
| #1020 | |
| #1021 | // ── Registry Tools ── |
| #1022 | { |
| #1023 | name: "register_erc8004", |
| #1024 | description: "Register on-chain as a Trustless Agent via ERC-8004.", |
| #1025 | category: "registry", |
| #1026 | dangerous: true, |
| #1027 | parameters: { |
| #1028 | type: "object", |
| #1029 | properties: { |
| #1030 | agent_uri: { type: "string", description: "URI pointing to your agent card JSON" }, |
| #1031 | network: { type: "string", description: "mainnet or testnet (default: mainnet)" }, |
| #1032 | }, |
| #1033 | required: ["agent_uri"], |
| #1034 | }, |
| #1035 | execute: async (args, ctx) => { |
| #1036 | const { registerAgent } = await import("../registry/erc8004.js"); |
| #1037 | const entry = await registerAgent( |
| #1038 | ctx.identity.account, |
| #1039 | args.agent_uri as string, |
| #1040 | ((args.network as string) || "mainnet") as any, |
| #1041 | ctx.db, |
| #1042 | ); |
| #1043 | return `Registered on-chain! Agent ID: ${entry.agentId}, TX: ${entry.txHash}`; |
| #1044 | }, |
| #1045 | }, |
| #1046 | { |
| #1047 | name: "update_agent_card", |
| #1048 | description: "Generate and save an updated agent card.", |
| #1049 | category: "registry", |
| #1050 | parameters: { type: "object", properties: {} }, |
| #1051 | execute: async (_args, ctx) => { |
| #1052 | const { generateAgentCard, saveAgentCard } = await import("../registry/agent-card.js"); |
| #1053 | const card = generateAgentCard(ctx.identity, ctx.config, ctx.db); |
| #1054 | await saveAgentCard(card, ctx.runtime); |
| #1055 | return `Agent card updated: ${JSON.stringify(card, null, 2)}`; |
| #1056 | }, |
| #1057 | }, |
| #1058 | { |
| #1059 | name: "discover_agents", |
| #1060 | description: "Discover other agents via ERC-8004 registry.", |
| #1061 | category: "registry", |
| #1062 | parameters: { |
| #1063 | type: "object", |
| #1064 | properties: { |
| #1065 | keyword: { type: "string", description: "Search keyword (optional)" }, |
| #1066 | limit: { type: "number", description: "Max results (default: 10)" }, |
| #1067 | network: { type: "string", description: "mainnet or testnet" }, |
| #1068 | }, |
| #1069 | }, |
| #1070 | execute: async (args, ctx) => { |
| #1071 | const { discoverAgents, searchAgents } = await import("../registry/discovery.js"); |
| #1072 | const network = ((args.network as string) || "mainnet") as any; |
| #1073 | const keyword = args.keyword as string | undefined; |
| #1074 | const limit = (args.limit as number) || 10; |
| #1075 | |
| #1076 | const agents = keyword |
| #1077 | ? await searchAgents(keyword, limit, network) |
| #1078 | : await discoverAgents(limit, network); |
| #1079 | |
| #1080 | if (agents.length === 0) return "No agents found."; |
| #1081 | return agents |
| #1082 | .map( |
| #1083 | (a) => `#${a.agentId} ${a.name || "unnamed"} (${a.owner.slice(0, 10)}...): ${a.description || a.agentURI}`, |
| #1084 | ) |
| #1085 | .join("\n"); |
| #1086 | }, |
| #1087 | }, |
| #1088 | { |
| #1089 | name: "give_feedback", |
| #1090 | description: "Leave on-chain reputation feedback for another agent.", |
| #1091 | category: "registry", |
| #1092 | dangerous: true, |
| #1093 | parameters: { |
| #1094 | type: "object", |
| #1095 | properties: { |
| #1096 | agent_id: { type: "string", description: "Target agent's ERC-8004 ID" }, |
| #1097 | score: { type: "number", description: "Score 1-5" }, |
| #1098 | comment: { type: "string", description: "Feedback comment" }, |
| #1099 | }, |
| #1100 | required: ["agent_id", "score", "comment"], |
| #1101 | }, |
| #1102 | execute: async (args, ctx) => { |
| #1103 | const { leaveFeedback } = await import("../registry/erc8004.js"); |
| #1104 | const hash = await leaveFeedback( |
| #1105 | ctx.identity.account, |
| #1106 | args.agent_id as string, |
| #1107 | args.score as number, |
| #1108 | args.comment as string, |
| #1109 | "mainnet", |
| #1110 | ctx.db, |
| #1111 | ); |
| #1112 | return `Feedback submitted. TX: ${hash}`; |
| #1113 | }, |
| #1114 | }, |
| #1115 | { |
| #1116 | name: "check_reputation", |
| #1117 | description: "Check reputation feedback for an agent.", |
| #1118 | category: "registry", |
| #1119 | parameters: { |
| #1120 | type: "object", |
| #1121 | properties: { |
| #1122 | agent_address: { type: "string", description: "Agent address (default: self)" }, |
| #1123 | }, |
| #1124 | }, |
| #1125 | execute: async (args, ctx) => { |
| #1126 | const address = (args.agent_address as string) || ctx.identity.address; |
| #1127 | const entries = ctx.db.getReputation(address); |
| #1128 | if (entries.length === 0) return "No reputation feedback found."; |
| #1129 | return entries |
| #1130 | .map( |
| #1131 | (e) => `${e.fromAgent.slice(0, 10)}... -> score:${e.score} "${e.comment}"`, |
| #1132 | ) |
| #1133 | .join("\n"); |
| #1134 | }, |
| #1135 | }, |
| #1136 | |
| #1137 | // ── Replication Tools ── |
| #1138 | { |
| #1139 | name: "spawn_child", |
| #1140 | description: "Spawn a child automaton in a new CLAWD sandbox.", |
| #1141 | category: "replication", |
| #1142 | dangerous: true, |
| #1143 | parameters: { |
| #1144 | type: "object", |
| #1145 | properties: { |
| #1146 | name: { type: "string", description: "Name for the child automaton" }, |
| #1147 | specialization: { type: "string", description: "What the child should specialize in" }, |
| #1148 | message: { type: "string", description: "Message to the child" }, |
| #1149 | }, |
| #1150 | required: ["name"], |
| #1151 | }, |
| #1152 | execute: async (args, ctx) => { |
| #1153 | const { generateGenesisConfig } = await import("../replication/genesis.js"); |
| #1154 | const { spawnChild } = await import("../replication/spawn.js"); |
| #1155 | |
| #1156 | const genesis = generateGenesisConfig(ctx.identity, ctx.config, { |
| #1157 | name: args.name as string, |
| #1158 | specialization: args.specialization as string | undefined, |
| #1159 | message: args.message as string | undefined, |
| #1160 | }); |
| #1161 | |
| #1162 | const child = await spawnChild(ctx.runtime, ctx.identity, ctx.db, genesis); |
| #1163 | return `Child spawned: ${child.name} in sandbox ${child.sandboxId} (status: ${child.status})`; |
| #1164 | }, |
| #1165 | }, |
| #1166 | { |
| #1167 | name: "list_children", |
| #1168 | description: "List all spawned child automatons.", |
| #1169 | category: "replication", |
| #1170 | parameters: { type: "object", properties: {} }, |
| #1171 | execute: async (_args, ctx) => { |
| #1172 | const children = ctx.db.getChildren(); |
| #1173 | if (children.length === 0) return "No children spawned."; |
| #1174 | return children |
| #1175 | .map( |
| #1176 | (c) => |
| #1177 | `${c.name} [${c.status}] sandbox:${c.sandboxId} funded:$${(c.fundedAmountCents / 100).toFixed(2)}`, |
| #1178 | ) |
| #1179 | .join("\n"); |
| #1180 | }, |
| #1181 | }, |
| #1182 | { |
| #1183 | name: "fund_child", |
| #1184 | description: "Transfer credits to a child automaton.", |
| #1185 | category: "replication", |
| #1186 | dangerous: true, |
| #1187 | parameters: { |
| #1188 | type: "object", |
| #1189 | properties: { |
| #1190 | child_id: { type: "string", description: "Child automaton ID" }, |
| #1191 | amount_cents: { type: "number", description: "Amount in cents to transfer" }, |
| #1192 | }, |
| #1193 | required: ["child_id", "amount_cents"], |
| #1194 | }, |
| #1195 | execute: async (args, ctx) => { |
| #1196 | const child = ctx.db.getChildById(args.child_id as string); |
| #1197 | if (!child) return `Child ${args.child_id} not found.`; |
| #1198 | |
| #1199 | const balance = await ctx.runtime.getCreditsBalance(); |
| #1200 | const amount = args.amount_cents as number; |
| #1201 | if (amount > balance / 2) { |
| #1202 | return `Blocked: Cannot transfer more than half your balance. Self-preservation.`; |
| #1203 | } |
| #1204 | |
| #1205 | const transfer = await ctx.runtime.transferCredits( |
| #1206 | child.address, |
| #1207 | amount, |
| #1208 | `fund child ${child.id}`, |
| #1209 | ); |
| #1210 | |
| #1211 | const { ulid } = await import("ulid"); |
| #1212 | ctx.db.insertTransaction({ |
| #1213 | id: ulid(), |
| #1214 | type: "transfer_out", |
| #1215 | amountCents: amount, |
| #1216 | balanceAfterCents: |
| #1217 | transfer.balanceAfterCents ?? Math.max(balance - amount, 0), |
| #1218 | description: `Fund child ${child.name} (${child.id})`, |
| #1219 | timestamp: new Date().toISOString(), |
| #1220 | }); |
| #1221 | |
| #1222 | return `Funded child ${child.name} with $${(amount / 100).toFixed(2)} (status: ${transfer.status}, id: ${transfer.transferId || "n/a"})`; |
| #1223 | }, |
| #1224 | }, |
| #1225 | { |
| #1226 | name: "check_child_status", |
| #1227 | description: "Check the current status of a child automaton.", |
| #1228 | category: "replication", |
| #1229 | parameters: { |
| #1230 | type: "object", |
| #1231 | properties: { |
| #1232 | child_id: { type: "string", description: "Child automaton ID" }, |
| #1233 | }, |
| #1234 | required: ["child_id"], |
| #1235 | }, |
| #1236 | execute: async (args, ctx) => { |
| #1237 | const { checkChildStatus } = await import("../replication/spawn.js"); |
| #1238 | return await checkChildStatus(ctx.runtime, ctx.db, args.child_id as string); |
| #1239 | }, |
| #1240 | }, |
| #1241 | |
| #1242 | // ── Convex CLAWD Tools ── |
| #1243 | { |
| #1244 | name: "convex_set_data", |
| #1245 | description: |
| #1246 | "Store key-value data in the CLAWD Convex backend. Use this to save state, memories, or any data you want to persist across restarts. Data is stored per agent and can be tagged. Requires convexSiteUrl to be configured.", |
| #1247 | category: "survival", |
| #1248 | parameters: { |
| #1249 | type: "object", |
| #1250 | properties: { |
| #1251 | key: { type: "string", description: "Key to store data under" }, |
| #1252 | value: { type: "string", description: "Value to store (stringified JSON, text, etc.)" }, |
| #1253 | contentType: { type: "string", description: "Content type hint (e.g., application/json, text/plain)" }, |
| #1254 | tags: { type: "array", items: { type: "string" }, description: "Tags for categorization" }, |
| #1255 | }, |
| #1256 | required: ["key", "value"], |
| #1257 | }, |
| #1258 | execute: async (args, ctx) => { |
| #1259 | if (!ctx.convex) return "Convex not configured. Set convexSiteUrl in config to enable."; |
| #1260 | const result = await ctx.convex.setData({ |
| #1261 | agentId: ctx.identity.address, |
| #1262 | key: args.key as string, |
| #1263 | value: args.value as string, |
| #1264 | contentType: args.contentType as string | undefined, |
| #1265 | tags: args.tags as string[] | undefined, |
| #1266 | }); |
| #1267 | return `Data stored: key="${result.key}", updatedAt=${new Date(result.updatedAt).toISOString()}`; |
| #1268 | }, |
| #1269 | }, |
| #1270 | { |
| #1271 | name: "convex_get_data", |
| #1272 | description: |
| #1273 | "Retrieve data from the CLAWD Convex backend by key. Use this to recall previously stored state, memories, or configuration.", |
| #1274 | category: "survival", |
| #1275 | parameters: { |
| #1276 | type: "object", |
| #1277 | properties: { |
| #1278 | key: { type: "string", description: "Key to retrieve" }, |
| #1279 | }, |
| #1280 | required: ["key"], |
| #1281 | }, |
| #1282 | execute: async (args, ctx) => { |
| #1283 | if (!ctx.convex) return "Convex not configured. Set convexSiteUrl in config to enable."; |
| #1284 | const result = await ctx.convex.getData({ |
| #1285 | agentId: ctx.identity.address, |
| #1286 | key: args.key as string, |
| #1287 | }); |
| #1288 | if (!result) return `No data found for key="${args.key}"`; |
| #1289 | return `Key: ${result.key}\nContentType: ${result.contentType || 'text/plain'}\nUpdatedAt: ${new Date(result.updatedAt).toISOString()}\n\n${result.value}`; |
| #1290 | }, |
| #1291 | }, |
| #1292 | { |
| #1293 | name: "convex_list_data", |
| #1294 | description: |
| #1295 | "List all stored data keys for your agent in the CLAWD Convex backend. Shows key names, content types, and value previews.", |
| #1296 | category: "survival", |
| #1297 | parameters: { |
| #1298 | type: "object", |
| #1299 | properties: {}, |
| #1300 | }, |
| #1301 | execute: async (args, ctx) => { |
| #1302 | if (!ctx.convex) return "Convex not configured. Set convexSiteUrl in config to enable."; |
| #1303 | const entries = await ctx.convex.listData({ |
| #1304 | agentId: ctx.identity.address, |
| #1305 | }); |
| #1306 | if (entries.length === 0) return "No stored data found."; |
| #1307 | return entries.map((e) => |
| #1308 | `[${e.key}] ${e.contentType || 'text/plain'} — "${e.valuePreview.slice(0, 100)}${e.valuePreview.length > 100 ? '...' : ''}"` |
| #1309 | ).join('\n'); |
| #1310 | }, |
| #1311 | }, |
| #1312 | |
| #1313 | // ── Social / Messaging Tools ── |
| #1314 | { |
| #1315 | name: "send_message", |
| #1316 | description: |
| #1317 | "Send a message to another automaton or address via the social relay.", |
| #1318 | category: "runtime", |
| #1319 | parameters: { |
| #1320 | type: "object", |
| #1321 | properties: { |
| #1322 | to_address: { |
| #1323 | type: "string", |
| #1324 | description: "Recipient wallet address (0x...)", |
| #1325 | }, |
| #1326 | content: { |
| #1327 | type: "string", |
| #1328 | description: "Message content to send", |
| #1329 | }, |
| #1330 | reply_to: { |
| #1331 | type: "string", |
| #1332 | description: "Optional message ID to reply to", |
| #1333 | }, |
| #1334 | }, |
| #1335 | required: ["to_address", "content"], |
| #1336 | }, |
| #1337 | execute: async (args, ctx) => { |
| #1338 | if (!ctx.social) { |
| #1339 | return "Social relay not configured. Set socialRelayUrl in config."; |
| #1340 | } |
| #1341 | const result = await ctx.social.send( |
| #1342 | args.to_address as string, |
| #1343 | args.content as string, |
| #1344 | args.reply_to as string | undefined, |
| #1345 | ); |
| #1346 | return `Message sent (id: ${result.id})`; |
| #1347 | }, |
| #1348 | }, |
| #1349 | |
| #1350 | // ── Model Discovery ── |
| #1351 | { |
| #1352 | name: "list_models", |
| #1353 | description: |
| #1354 | "List all available inference models from the CLAWD Runtime API with their provider and pricing. Use this to discover what models you can use and pick the best one for your needs.", |
| #1355 | category: "runtime", |
| #1356 | parameters: { |
| #1357 | type: "object", |
| #1358 | properties: {}, |
| #1359 | required: [], |
| #1360 | }, |
| #1361 | execute: async (_args, ctx) => { |
| #1362 | const models = await ctx.runtime.listModels(); |
| #1363 | const lines = models.map( |
| #1364 | (m) => |
| #1365 | `${m.id} (${m.provider}) — $${m.pricing.inputPerMillion}/$${m.pricing.outputPerMillion} per 1M tokens (in/out)`, |
| #1366 | ); |
| #1367 | return `Available models:\n${lines.join("\n")}`; |
| #1368 | }, |
| #1369 | }, |
| #1370 | |
| #1371 | // ── Domain Tools ── |
| #1372 | { |
| #1373 | name: "search_domains", |
| #1374 | description: |
| #1375 | "Search for available domain names and get pricing.", |
| #1376 | category: "runtime", |
| #1377 | parameters: { |
| #1378 | type: "object", |
| #1379 | properties: { |
| #1380 | query: { |
| #1381 | type: "string", |
| #1382 | description: "Domain name or keyword to search (e.g., 'mysite' or 'mysite.com')", |
| #1383 | }, |
| #1384 | tlds: { |
| #1385 | type: "string", |
| #1386 | description: "Comma-separated TLDs to check (e.g., 'com,io,ai'). Default: com,io,ai,xyz,net,org,dev", |
| #1387 | }, |
| #1388 | }, |
| #1389 | required: ["query"], |
| #1390 | }, |
| #1391 | execute: async (args, ctx) => { |
| #1392 | const results = await ctx.runtime.searchDomains( |
| #1393 | args.query as string, |
| #1394 | args.tlds as string | undefined, |
| #1395 | ); |
| #1396 | if (results.length === 0) return "No results found."; |
| #1397 | return results |
| #1398 | .map( |
| #1399 | (d) => |
| #1400 | `${d.domain}: ${d.available ? "AVAILABLE" : "taken"}${d.registrationPrice != null ? ` ($${(d.registrationPrice / 100).toFixed(2)}/yr)` : ""}`, |
| #1401 | ) |
| #1402 | .join("\n"); |
| #1403 | }, |
| #1404 | }, |
| #1405 | { |
| #1406 | name: "register_domain", |
| #1407 | description: |
| #1408 | "Register a domain name. Costs USDC via x402 payment. Check availability first with search_domains.", |
| #1409 | category: "runtime", |
| #1410 | dangerous: true, |
| #1411 | parameters: { |
| #1412 | type: "object", |
| #1413 | properties: { |
| #1414 | domain: { |
| #1415 | type: "string", |
| #1416 | description: "Full domain to register (e.g., 'mysite.com')", |
| #1417 | }, |
| #1418 | years: { |
| #1419 | type: "number", |
| #1420 | description: "Registration period in years (default: 1)", |
| #1421 | }, |
| #1422 | }, |
| #1423 | required: ["domain"], |
| #1424 | }, |
| #1425 | execute: async (args, ctx) => { |
| #1426 | const reg = await ctx.runtime.registerDomain( |
| #1427 | args.domain as string, |
| #1428 | (args.years as number) || 1, |
| #1429 | ); |
| #1430 | return `Domain registered: ${reg.domain} (status: ${reg.status}${reg.expiresAt ? `, expires: ${reg.expiresAt}` : ""}${reg.transactionId ? `, tx: ${reg.transactionId}` : ""})`; |
| #1431 | }, |
| #1432 | }, |
| #1433 | { |
| #1434 | name: "manage_dns", |
| #1435 | description: |
| #1436 | "Manage DNS records for a domain you own. Actions: list, add, delete.", |
| #1437 | category: "runtime", |
| #1438 | parameters: { |
| #1439 | type: "object", |
| #1440 | properties: { |
| #1441 | action: { |
| #1442 | type: "string", |
| #1443 | description: "list, add, or delete", |
| #1444 | }, |
| #1445 | domain: { |
| #1446 | type: "string", |
| #1447 | description: "Domain name (e.g., 'mysite.com')", |
| #1448 | }, |
| #1449 | type: { |
| #1450 | type: "string", |
| #1451 | description: "Record type for add: A, AAAA, CNAME, MX, TXT, etc.", |
| #1452 | }, |
| #1453 | host: { |
| #1454 | type: "string", |
| #1455 | description: "Record host for add (e.g., '@' for root, 'www')", |
| #1456 | }, |
| #1457 | value: { |
| #1458 | type: "string", |
| #1459 | description: "Record value for add (e.g., IP address, target domain)", |
| #1460 | }, |
| #1461 | ttl: { |
| #1462 | type: "number", |
| #1463 | description: "TTL in seconds for add (default: 3600)", |
| #1464 | }, |
| #1465 | record_id: { |
| #1466 | type: "string", |
| #1467 | description: "Record ID for delete", |
| #1468 | }, |
| #1469 | }, |
| #1470 | required: ["action", "domain"], |
| #1471 | }, |
| #1472 | execute: async (args, ctx) => { |
| #1473 | const action = args.action as string; |
| #1474 | const domain = args.domain as string; |
| #1475 | |
| #1476 | if (action === "list") { |
| #1477 | const records = await ctx.runtime.listDnsRecords(domain); |
| #1478 | if (records.length === 0) return `No DNS records found for ${domain}.`; |
| #1479 | return records |
| #1480 | .map( |
| #1481 | (r) => `[${r.id}] ${r.type} ${r.host} -> ${r.value} (TTL: ${r.ttl || "default"})`, |
| #1482 | ) |
| #1483 | .join("\n"); |
| #1484 | } |
| #1485 | |
| #1486 | if (action === "add") { |
| #1487 | const type = args.type as string; |
| #1488 | const host = args.host as string; |
| #1489 | const value = args.value as string; |
| #1490 | if (!type || !host || !value) { |
| #1491 | return "Required for add: type, host, value"; |
| #1492 | } |
| #1493 | const record = await ctx.runtime.addDnsRecord( |
| #1494 | domain, |
| #1495 | type, |
| #1496 | host, |
| #1497 | value, |
| #1498 | args.ttl as number | undefined, |
| #1499 | ); |
| #1500 | return `DNS record added: [${record.id}] ${record.type} ${record.host} -> ${record.value}`; |
| #1501 | } |
| #1502 | |
| #1503 | if (action === "delete") { |
| #1504 | const recordId = args.record_id as string; |
| #1505 | if (!recordId) return "Required for delete: record_id"; |
| #1506 | await ctx.runtime.deleteDnsRecord(domain, recordId); |
| #1507 | return `DNS record ${recordId} deleted from ${domain}`; |
| #1508 | } |
| #1509 | |
| #1510 | return `Unknown action: ${action}. Use list, add, or delete.`; |
| #1511 | }, |
| #1512 | }, |
| #1513 | |
| #1514 | // ── x402 Payment Tool ── |
| #1515 | { |
| #1516 | name: "x402_fetch", |
| #1517 | description: |
| #1518 | "Fetch a URL with automatic x402 USDC payment. If the server responds with HTTP 402, signs a USDC payment and retries. Use this to access paid APIs and services.", |
| #1519 | category: "financial", |
| #1520 | parameters: { |
| #1521 | type: "object", |
| #1522 | properties: { |
| #1523 | url: { |
| #1524 | type: "string", |
| #1525 | description: "The URL to fetch", |
| #1526 | }, |
| #1527 | method: { |
| #1528 | type: "string", |
| #1529 | description: "HTTP method (default: GET)", |
| #1530 | }, |
| #1531 | body: { |
| #1532 | type: "string", |
| #1533 | description: "Request body for POST/PUT (JSON string)", |
| #1534 | }, |
| #1535 | headers: { |
| #1536 | type: "string", |
| #1537 | description: "Additional headers as JSON string", |
| #1538 | }, |
| #1539 | }, |
| #1540 | required: ["url"], |
| #1541 | }, |
| #1542 | execute: async (args, ctx) => { |
| #1543 | const { x402Fetch } = await import("../clawd/x402.js"); |
| #1544 | const url = args.url as string; |
| #1545 | const method = (args.method as string) || "GET"; |
| #1546 | const body = args.body as string | undefined; |
| #1547 | const extraHeaders = args.headers |
| #1548 | ? JSON.parse(args.headers as string) |
| #1549 | : undefined; |
| #1550 | |
| #1551 | const result = await x402Fetch( |
| #1552 | url, |
| #1553 | ctx.identity.account, |
| #1554 | method, |
| #1555 | body, |
| #1556 | extraHeaders, |
| #1557 | ); |
| #1558 | |
| #1559 | if (!result.success) { |
| #1560 | return `x402 fetch failed: ${result.error || "Unknown error"}`; |
| #1561 | } |
| #1562 | |
| #1563 | const responseStr = |
| #1564 | typeof result.response === "string" |
| #1565 | ? result.response |
| #1566 | : JSON.stringify(result.response, null, 2); |
| #1567 | |
| #1568 | // Truncate very large responses |
| #1569 | if (responseStr.length > 10000) { |
| #1570 | return `x402 fetch succeeded (truncated):\n${responseStr.slice(0, 10000)}...`; |
| #1571 | } |
| #1572 | return `x402 fetch succeeded:\n${responseStr}`; |
| #1573 | }, |
| #1574 | }, |
| #1575 | ]; |
| #1576 | } |
| #1577 | |
| #1578 | /** |
| #1579 | * Convert AutomatonTool list to OpenAI-compatible tool definitions. |
| #1580 | */ |
| #1581 | export function toolsToInferenceFormat( |
| #1582 | tools: AutomatonTool[], |
| #1583 | ): InferenceToolDefinition[] { |
| #1584 | return tools.map((t) => ({ |
| #1585 | type: "function" as const, |
| #1586 | function: { |
| #1587 | name: t.name, |
| #1588 | description: t.description, |
| #1589 | parameters: t.parameters, |
| #1590 | }, |
| #1591 | })); |
| #1592 | } |
| #1593 | |
| #1594 | /** |
| #1595 | * Execute a tool call and return the result. |
| #1596 | */ |
| #1597 | export async function executeTool( |
| #1598 | toolName: string, |
| #1599 | args: Record<string, unknown>, |
| #1600 | tools: AutomatonTool[], |
| #1601 | context: ToolContext, |
| #1602 | ): Promise<ToolCallResult> { |
| #1603 | const tool = tools.find((t) => t.name === toolName); |
| #1604 | const startTime = Date.now(); |
| #1605 | |
| #1606 | if (!tool) { |
| #1607 | return { |
| #1608 | id: `tc_${Date.now()}`, |
| #1609 | name: toolName, |
| #1610 | arguments: args, |
| #1611 | result: "", |
| #1612 | durationMs: 0, |
| #1613 | error: `Unknown tool: ${toolName}`, |
| #1614 | }; |
| #1615 | } |
| #1616 | |
| #1617 | try { |
| #1618 | const result = await tool.execute(args, context); |
| #1619 | return { |
| #1620 | id: `tc_${Date.now()}`, |
| #1621 | name: toolName, |
| #1622 | arguments: args, |
| #1623 | result, |
| #1624 | durationMs: Date.now() - startTime, |
| #1625 | }; |
| #1626 | } catch (err: any) { |
| #1627 | return { |
| #1628 | id: `tc_${Date.now()}`, |
| #1629 | name: toolName, |
| #1630 | arguments: args, |
| #1631 | result: "", |
| #1632 | durationMs: Date.now() - startTime, |
| #1633 | error: err.message || String(err), |
| #1634 | }; |
| #1635 | } |
| #1636 | } |
| #1637 |