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 | * Spawn |
| #3 | * |
| #4 | * Spawn child automatons in new CLAWD sandboxes. |
| #5 | * The parent creates a new sandbox, installs the runtime, |
| #6 | * writes a genesis config, funds the child, and starts it. |
| #7 | */ |
| #8 | |
| #9 | import fs from "fs"; |
| #10 | import pathLib from "path"; |
| #11 | import type { |
| #12 | ClawdRuntimeClient, |
| #13 | AutomatonIdentity, |
| #14 | AutomatonDatabase, |
| #15 | ChildAutomaton, |
| #16 | GenesisConfig, |
| #17 | } from "../types.js"; |
| #18 | import { MAX_CHILDREN } from "../types.js"; |
| #19 | import { ulid } from "ulid"; |
| #20 | |
| #21 | /** |
| #22 | * Spawn a child automaton in a new CLAWD sandbox. |
| #23 | */ |
| #24 | export async function spawnChild( |
| #25 | runtime: ClawdRuntimeClient, |
| #26 | identity: AutomatonIdentity, |
| #27 | db: AutomatonDatabase, |
| #28 | genesis: GenesisConfig, |
| #29 | ): Promise<ChildAutomaton> { |
| #30 | // Check child limit |
| #31 | const existing = db.getChildren().filter( |
| #32 | (c) => c.status !== "dead", |
| #33 | ); |
| #34 | if (existing.length >= MAX_CHILDREN) { |
| #35 | throw new Error( |
| #36 | `Cannot spawn: already at max children (${MAX_CHILDREN}). Kill or wait for existing children to die.`, |
| #37 | ); |
| #38 | } |
| #39 | |
| #40 | const childId = ulid(); |
| #41 | |
| #42 | // 1. Create a new sandbox for the child |
| #43 | const sandbox = await runtime.createSandbox({ |
| #44 | name: `automaton-child-${genesis.name.toLowerCase().replace(/\s+/g, "-")}`, |
| #45 | vcpu: 1, |
| #46 | memoryMb: 512, |
| #47 | diskGb: 5, |
| #48 | }); |
| #49 | |
| #50 | const child: ChildAutomaton = { |
| #51 | id: childId, |
| #52 | name: genesis.name, |
| #53 | address: "0x0000000000000000000000000000000000000000" as any, // Will be set after keygen |
| #54 | sandboxId: sandbox.id, |
| #55 | genesisPrompt: genesis.genesisPrompt, |
| #56 | creatorMessage: genesis.creatorMessage, |
| #57 | fundedAmountCents: 0, |
| #58 | status: "spawning", |
| #59 | createdAt: new Date().toISOString(), |
| #60 | }; |
| #61 | |
| #62 | db.insertChild(child); |
| #63 | |
| #64 | // 2. Install Node.js and the automaton runtime in the child sandbox |
| #65 | await execInSandbox(runtime, sandbox.id, "apt-get update -qq && apt-get install -y -qq nodejs npm git curl", 120000); |
| #66 | |
| #67 | // 3. Install the automaton runtime |
| #68 | await execInSandbox( |
| #69 | runtime, |
| #70 | sandbox.id, |
| #71 | "npm install -g @clawd/automaton@latest 2>/dev/null || true", |
| #72 | 60000, |
| #73 | ); |
| #74 | |
| #75 | // 4. Write the genesis configuration |
| #76 | const genesisJson = JSON.stringify( |
| #77 | { |
| #78 | name: genesis.name, |
| #79 | genesisPrompt: genesis.genesisPrompt, |
| #80 | creatorMessage: genesis.creatorMessage, |
| #81 | creatorAddress: identity.address, // Parent is the creator |
| #82 | parentAddress: identity.address, |
| #83 | }, |
| #84 | null, |
| #85 | 2, |
| #86 | ); |
| #87 | |
| #88 | await writeInSandbox( |
| #89 | runtime, |
| #90 | sandbox.id, |
| #91 | "/root/.automaton/genesis.json", |
| #92 | genesisJson, |
| #93 | ); |
| #94 | |
| #95 | // 4b. Propagate constitution (immutable, inherited before anything else) |
| #96 | const constitutionPath = pathLib.join( |
| #97 | process.env.HOME || "/root", |
| #98 | ".automaton", |
| #99 | "constitution.md", |
| #100 | ); |
| #101 | try { |
| #102 | const constitution = fs.readFileSync(constitutionPath, "utf-8"); |
| #103 | await writeInSandbox( |
| #104 | runtime, |
| #105 | sandbox.id, |
| #106 | "/root/.automaton/constitution.md", |
| #107 | constitution, |
| #108 | ); |
| #109 | // Make it read-only in the child |
| #110 | await execInSandbox(runtime, sandbox.id, "chmod 444 /root/.automaton/constitution.md", 5000); |
| #111 | } catch { |
| #112 | // Constitution file not found locally — child will get it from the repo on build |
| #113 | } |
| #114 | |
| #115 | // 5. Record the spawn |
| #116 | db.insertModification({ |
| #117 | id: ulid(), |
| #118 | timestamp: new Date().toISOString(), |
| #119 | type: "child_spawn", |
| #120 | description: `Spawned child: ${genesis.name} in sandbox ${sandbox.id}`, |
| #121 | reversible: false, |
| #122 | }); |
| #123 | |
| #124 | return child; |
| #125 | } |
| #126 | |
| #127 | /** |
| #128 | * Start a child automaton after setup. |
| #129 | */ |
| #130 | export async function startChild( |
| #131 | runtime: ClawdRuntimeClient, |
| #132 | db: AutomatonDatabase, |
| #133 | childId: string, |
| #134 | ): Promise<void> { |
| #135 | const child = db.getChildById(childId); |
| #136 | if (!child) throw new Error(`Child ${childId} not found`); |
| #137 | |
| #138 | // Initialize wallet, provision, and run |
| #139 | await execInSandbox( |
| #140 | runtime, |
| #141 | child.sandboxId, |
| #142 | "automaton --init && automaton --provision && systemctl start automaton 2>/dev/null || automaton --run &", |
| #143 | 60000, |
| #144 | ); |
| #145 | |
| #146 | db.updateChildStatus(childId, "running"); |
| #147 | } |
| #148 | |
| #149 | /** |
| #150 | * Check a child's status. |
| #151 | */ |
| #152 | export async function checkChildStatus( |
| #153 | runtime: ClawdRuntimeClient, |
| #154 | db: AutomatonDatabase, |
| #155 | childId: string, |
| #156 | ): Promise<string> { |
| #157 | const child = db.getChildById(childId); |
| #158 | if (!child) throw new Error(`Child ${childId} not found`); |
| #159 | |
| #160 | try { |
| #161 | const result = await execInSandbox( |
| #162 | runtime, |
| #163 | child.sandboxId, |
| #164 | "automaton --status 2>/dev/null || echo 'offline'", |
| #165 | 10000, |
| #166 | ); |
| #167 | |
| #168 | const output = result.stdout || "unknown"; |
| #169 | |
| #170 | // Parse status from output |
| #171 | if (output.includes("dead")) { |
| #172 | db.updateChildStatus(childId, "dead"); |
| #173 | } else if (output.includes("sleeping")) { |
| #174 | db.updateChildStatus(childId, "sleeping"); |
| #175 | } else if (output.includes("running")) { |
| #176 | db.updateChildStatus(childId, "running"); |
| #177 | } |
| #178 | |
| #179 | return output; |
| #180 | } catch { |
| #181 | db.updateChildStatus(childId, "unknown"); |
| #182 | return "Unable to reach child sandbox"; |
| #183 | } |
| #184 | } |
| #185 | |
| #186 | /** |
| #187 | * Send a message to a child automaton. |
| #188 | */ |
| #189 | export async function messageChild( |
| #190 | runtime: ClawdRuntimeClient, |
| #191 | db: AutomatonDatabase, |
| #192 | childId: string, |
| #193 | message: string, |
| #194 | ): Promise<void> { |
| #195 | const child = db.getChildById(childId); |
| #196 | if (!child) throw new Error(`Child ${childId} not found`); |
| #197 | |
| #198 | // Write message to child's message queue |
| #199 | const msgJson = JSON.stringify({ |
| #200 | from: "parent", |
| #201 | content: message, |
| #202 | timestamp: new Date().toISOString(), |
| #203 | }); |
| #204 | |
| #205 | await writeInSandbox( |
| #206 | runtime, |
| #207 | child.sandboxId, |
| #208 | `/root/.automaton/inbox/${ulid()}.json`, |
| #209 | msgJson, |
| #210 | ); |
| #211 | } |
| #212 | |
| #213 | // ─── Helpers ────────────────────────────────────────────────── |
| #214 | |
| #215 | async function execInSandbox( |
| #216 | runtime: ClawdRuntimeClient, |
| #217 | sandboxId: string, |
| #218 | command: string, |
| #219 | timeout: number = 30000, |
| #220 | ) { |
| #221 | // Use the CLAWD Runtime API to exec in a specific sandbox |
| #222 | const apiUrl = (runtime as any).__apiUrl || "https://api.x402.wtf"; |
| #223 | const apiKey = (runtime as any).__apiKey || ""; |
| #224 | |
| #225 | const resp = await fetch(`${apiUrl}/v1/sandboxes/${sandboxId}/exec`, { |
| #226 | method: "POST", |
| #227 | headers: { |
| #228 | "Content-Type": "application/json", |
| #229 | Authorization: apiKey, |
| #230 | }, |
| #231 | body: JSON.stringify({ command, timeout }), |
| #232 | }); |
| #233 | |
| #234 | if (!resp.ok) { |
| #235 | const text = await resp.text(); |
| #236 | throw new Error(`Exec in sandbox ${sandboxId} failed: ${text}`); |
| #237 | } |
| #238 | |
| #239 | return resp.json(); |
| #240 | } |
| #241 | |
| #242 | async function writeInSandbox( |
| #243 | runtime: ClawdRuntimeClient, |
| #244 | sandboxId: string, |
| #245 | path: string, |
| #246 | content: string, |
| #247 | ) { |
| #248 | const apiUrl = (runtime as any).__apiUrl || "https://api.x402.wtf"; |
| #249 | const apiKey = (runtime as any).__apiKey || ""; |
| #250 | |
| #251 | // Ensure parent directory exists |
| #252 | const dir = path.substring(0, path.lastIndexOf("/")); |
| #253 | await execInSandbox(runtime, sandboxId, `mkdir -p ${dir}`, 5000); |
| #254 | |
| #255 | const resp = await fetch( |
| #256 | `${apiUrl}/v1/sandboxes/${sandboxId}/files/upload/json`, |
| #257 | { |
| #258 | method: "POST", |
| #259 | headers: { |
| #260 | "Content-Type": "application/json", |
| #261 | Authorization: apiKey, |
| #262 | }, |
| #263 | body: JSON.stringify({ path, content }), |
| #264 | }, |
| #265 | ); |
| #266 | |
| #267 | if (!resp.ok) { |
| #268 | const text = await resp.text(); |
| #269 | throw new Error(`Write to sandbox ${sandboxId} failed: ${text}`); |
| #270 | } |
| #271 | } |
| #272 |