@holoscript/holoscript-agent 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,195 @@
1
+ // src/provision.ts
2
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir, hostname } from "os";
5
+ import { randomBytes, createCipheriv, createHash } from "crypto";
6
+ import { Wallet } from "ethers";
7
+ var HANDLE_PATTERN = /^[a-z0-9_-]{1,64}$/i;
8
+ var EIP712_DOMAIN = { name: "HoloMesh", version: "1" };
9
+ var EIP712_TYPES = {
10
+ Registration: [{ name: "nonce", type: "string" }]
11
+ };
12
+ async function provisionAgent(req, opts = { execute: false }) {
13
+ if (!HANDLE_PATTERN.test(req.handle)) {
14
+ throw new Error(`handle "${req.handle}" must match ${HANDLE_PATTERN}`);
15
+ }
16
+ if (!req.founderBearer || req.founderBearer.trim().length === 0) {
17
+ throw new Error("founderBearer is required (HOLOMESH_API_KEY of an agent that can call /register)");
18
+ }
19
+ const meshApiBase = (req.meshApiBase ?? "https://mcp.holoscript.net/api/holomesh").replace(/\/$/, "");
20
+ const seatsRoot = req.seatsRoot ?? defaultSeatsRoot();
21
+ const surface = req.handle;
22
+ const seatId = makeSeatId(surface);
23
+ const seatDir = join(seatsRoot, seatId);
24
+ const walletPath = join(seatDir, "wallet.enc");
25
+ const regPath = join(seatDir, "registration.json");
26
+ if (!opts.execute) {
27
+ return {
28
+ status: "dry-run",
29
+ handle: req.handle,
30
+ surface,
31
+ seatId,
32
+ seatDir,
33
+ willGenerateWallet: !existsSync(walletPath),
34
+ willCallEndpoints: [
35
+ `POST ${meshApiBase}/register/challenge`,
36
+ `POST ${meshApiBase}/register`
37
+ ]
38
+ };
39
+ }
40
+ if (existsSync(walletPath) && !opts.force) {
41
+ const blob = JSON.parse(readFileSync(walletPath, "utf8"));
42
+ const reused = {
43
+ status: "reused",
44
+ handle: req.handle,
45
+ surface,
46
+ seatId,
47
+ seatDir,
48
+ walletAddress: blob.address,
49
+ envVarLines: envVarLinesFor(req.handle, blob.address, void 0)
50
+ };
51
+ return reused;
52
+ }
53
+ const wallet = Wallet.createRandom();
54
+ mkdirSync(seatDir, { recursive: true });
55
+ const masterKey = ensureMasterKey(seatsRoot);
56
+ const encryptedBlob = {
57
+ seat_id: seatId,
58
+ surface,
59
+ handle: req.handle,
60
+ address: wallet.address,
61
+ encrypted_privkey: encryptPrivateKey(wallet.privateKey, masterKey),
62
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
63
+ source: "holoscript-agent.provision"
64
+ };
65
+ writeFileSync(walletPath, JSON.stringify(encryptedBlob, null, 2), "utf8");
66
+ try {
67
+ chmodSync(walletPath, 384);
68
+ } catch {
69
+ }
70
+ const fetchImpl = req.fetchImpl ?? fetch;
71
+ const challenge = await postJson(
72
+ fetchImpl,
73
+ `${meshApiBase}/register/challenge`,
74
+ req.founderBearer,
75
+ { wallet_address: wallet.address }
76
+ );
77
+ if (!challenge.nonce) {
78
+ throw new Error(`/register/challenge returned no nonce: ${JSON.stringify(challenge)}`);
79
+ }
80
+ const signature = await wallet.signTypedData(EIP712_DOMAIN, EIP712_TYPES, { nonce: challenge.nonce });
81
+ const registration = await postJson(
82
+ fetchImpl,
83
+ `${meshApiBase}/register`,
84
+ req.founderBearer,
85
+ {
86
+ name: req.handle,
87
+ wallet_address: wallet.address,
88
+ nonce: challenge.nonce,
89
+ signature
90
+ }
91
+ );
92
+ writeFileSync(
93
+ regPath,
94
+ JSON.stringify({ status: 201, response: registration, registered_at: (/* @__PURE__ */ new Date()).toISOString(), flow: "x402" }, null, 2),
95
+ "utf8"
96
+ );
97
+ const agentId = registration.agent?.id;
98
+ const bearer = registration.agent?.api_key;
99
+ if (!agentId || !bearer) {
100
+ throw new Error(`/register did not return agent.id + agent.api_key: ${JSON.stringify(registration).slice(0, 400)}`);
101
+ }
102
+ if (registration.wallet?.private_key) {
103
+ console.warn("[provision] WARN \u2014 server returned private_key despite x402 flow; ignoring (using local key).");
104
+ }
105
+ let joinedTeam;
106
+ if (req.autoJoinTeamId) {
107
+ try {
108
+ const joinRes = await postJson(
109
+ fetchImpl,
110
+ `${meshApiBase}/team/${req.autoJoinTeamId}/join`,
111
+ bearer,
112
+ {}
113
+ );
114
+ joinedTeam = {
115
+ teamId: req.autoJoinTeamId,
116
+ role: joinRes.role ?? "member",
117
+ members: joinRes.members ?? 0
118
+ };
119
+ } catch (err) {
120
+ joinedTeam = {
121
+ teamId: req.autoJoinTeamId,
122
+ error: err instanceof Error ? err.message : String(err)
123
+ };
124
+ }
125
+ }
126
+ return {
127
+ status: "executed",
128
+ handle: req.handle,
129
+ surface,
130
+ seatId,
131
+ seatDir,
132
+ walletAddress: wallet.address,
133
+ bearer,
134
+ agentId,
135
+ envVarLines: envVarLinesFor(req.handle, wallet.address, bearer),
136
+ joinedTeam
137
+ };
138
+ }
139
+ function defaultSeatsRoot() {
140
+ return process.env.HOLOSCRIPT_AGENT_SEATS_ROOT ?? join(homedir(), ".holoscript-agent", "seats");
141
+ }
142
+ function makeSeatId(surface) {
143
+ const fp = createHash("sha256").update(hostname() + homedir()).digest("hex").slice(0, 8);
144
+ return `holoscript-${surface}-${fp}-x402`;
145
+ }
146
+ function ensureMasterKey(seatsRoot) {
147
+ const keyPath = join(seatsRoot, ".master-key");
148
+ if (!existsSync(seatsRoot)) mkdirSync(seatsRoot, { recursive: true });
149
+ if (!existsSync(keyPath)) {
150
+ const k = randomBytes(32);
151
+ writeFileSync(keyPath, k);
152
+ try {
153
+ chmodSync(keyPath, 384);
154
+ } catch {
155
+ }
156
+ }
157
+ return readFileSync(keyPath);
158
+ }
159
+ function encryptPrivateKey(privKey, masterKey) {
160
+ const iv = randomBytes(12);
161
+ const cipher = createCipheriv("aes-256-gcm", masterKey, iv);
162
+ const ct = Buffer.concat([cipher.update(privKey, "utf8"), cipher.final()]);
163
+ return { iv: iv.toString("base64"), ct: ct.toString("base64"), tag: cipher.getAuthTag().toString("base64"), alg: "aes-256-gcm" };
164
+ }
165
+ async function postJson(fetchImpl, url, bearer, body) {
166
+ const res = await fetchImpl(url, {
167
+ method: "POST",
168
+ headers: {
169
+ Authorization: `Bearer ${bearer}`,
170
+ "Content-Type": "application/json"
171
+ },
172
+ body: JSON.stringify(body)
173
+ });
174
+ const text = await res.text();
175
+ if (!res.ok) {
176
+ throw new Error(`POST ${url} ${res.status}: ${text.slice(0, 400)}`);
177
+ }
178
+ try {
179
+ return JSON.parse(text);
180
+ } catch {
181
+ throw new Error(`POST ${url} returned non-JSON: ${text.slice(0, 200)}`);
182
+ }
183
+ }
184
+ function envVarLinesFor(handle, walletAddress, bearer) {
185
+ const suffix = handle.toUpperCase().replace(/-/g, "_");
186
+ const lines = [`HOLOSCRIPT_AGENT_WALLET_${suffix}=${walletAddress}`];
187
+ if (bearer) {
188
+ lines.push(`HOLOMESH_API_KEY_${suffix}_X402=${bearer}`);
189
+ }
190
+ return lines;
191
+ }
192
+ export {
193
+ provisionAgent
194
+ };
195
+ //# sourceMappingURL=provision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/provision.ts"],"sourcesContent":["import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir, hostname } from 'node:os';\nimport { randomBytes, createCipheriv, createHash } from 'node:crypto';\nimport { Wallet } from 'ethers';\n\nconst HANDLE_PATTERN = /^[a-z0-9_-]{1,64}$/i;\n\nexport interface ProvisionRequest {\n handle: string;\n meshApiBase?: string;\n founderBearer: string;\n seatsRoot?: string;\n fetchImpl?: typeof fetch;\n autoJoinTeamId?: string;\n}\n\nexport interface ProvisionDryRun {\n status: 'dry-run';\n handle: string;\n surface: string;\n seatId: string;\n seatDir: string;\n willGenerateWallet: boolean;\n willCallEndpoints: string[];\n}\n\nexport interface ProvisionExecuted {\n status: 'executed' | 'reused';\n handle: string;\n surface: string;\n seatId: string;\n seatDir: string;\n walletAddress: string;\n bearer?: string;\n agentId?: string;\n envVarLines: string[];\n joinedTeam?: { teamId: string; role: string; members: number } | { teamId: string; error: string };\n}\n\nexport type ProvisionResult = ProvisionDryRun | ProvisionExecuted;\n\nconst EIP712_DOMAIN = { name: 'HoloMesh', version: '1' };\nconst EIP712_TYPES: Record<string, Array<{ name: string; type: string }>> = {\n Registration: [{ name: 'nonce', type: 'string' }],\n};\n\nexport async function provisionAgent(\n req: ProvisionRequest,\n opts: { execute: boolean; force?: boolean } = { execute: false }\n): Promise<ProvisionResult> {\n if (!HANDLE_PATTERN.test(req.handle)) {\n throw new Error(`handle \"${req.handle}\" must match ${HANDLE_PATTERN}`);\n }\n if (!req.founderBearer || req.founderBearer.trim().length === 0) {\n throw new Error('founderBearer is required (HOLOMESH_API_KEY of an agent that can call /register)');\n }\n\n const meshApiBase = (req.meshApiBase ?? 'https://mcp.holoscript.net/api/holomesh').replace(/\\/$/, '');\n const seatsRoot = req.seatsRoot ?? defaultSeatsRoot();\n const surface = req.handle;\n const seatId = makeSeatId(surface);\n const seatDir = join(seatsRoot, seatId);\n const walletPath = join(seatDir, 'wallet.enc');\n const regPath = join(seatDir, 'registration.json');\n\n if (!opts.execute) {\n return {\n status: 'dry-run',\n handle: req.handle,\n surface,\n seatId,\n seatDir,\n willGenerateWallet: !existsSync(walletPath),\n willCallEndpoints: [\n `POST ${meshApiBase}/register/challenge`,\n `POST ${meshApiBase}/register`,\n ],\n };\n }\n\n if (existsSync(walletPath) && !opts.force) {\n const blob = JSON.parse(readFileSync(walletPath, 'utf8')) as { address: string };\n const reused: ProvisionExecuted = {\n status: 'reused',\n handle: req.handle,\n surface,\n seatId,\n seatDir,\n walletAddress: blob.address,\n envVarLines: envVarLinesFor(req.handle, blob.address, undefined),\n };\n return reused;\n }\n\n const wallet = Wallet.createRandom();\n mkdirSync(seatDir, { recursive: true });\n\n const masterKey = ensureMasterKey(seatsRoot);\n const encryptedBlob = {\n seat_id: seatId,\n surface,\n handle: req.handle,\n address: wallet.address,\n encrypted_privkey: encryptPrivateKey(wallet.privateKey, masterKey),\n created_at: new Date().toISOString(),\n source: 'holoscript-agent.provision',\n };\n writeFileSync(walletPath, JSON.stringify(encryptedBlob, null, 2), 'utf8');\n try { chmodSync(walletPath, 0o600); } catch {}\n\n const fetchImpl = req.fetchImpl ?? fetch;\n\n const challenge = await postJson<{ nonce: string }>(\n fetchImpl,\n `${meshApiBase}/register/challenge`,\n req.founderBearer,\n { wallet_address: wallet.address }\n );\n if (!challenge.nonce) {\n throw new Error(`/register/challenge returned no nonce: ${JSON.stringify(challenge)}`);\n }\n\n const signature = await wallet.signTypedData(EIP712_DOMAIN, EIP712_TYPES, { nonce: challenge.nonce });\n\n const registration = await postJson<{\n agent?: { id: string; api_key: string };\n wallet?: { private_key?: string };\n }>(\n fetchImpl,\n `${meshApiBase}/register`,\n req.founderBearer,\n {\n name: req.handle,\n wallet_address: wallet.address,\n nonce: challenge.nonce,\n signature,\n }\n );\n writeFileSync(\n regPath,\n JSON.stringify({ status: 201, response: registration, registered_at: new Date().toISOString(), flow: 'x402' }, null, 2),\n 'utf8'\n );\n\n const agentId = registration.agent?.id;\n const bearer = registration.agent?.api_key;\n if (!agentId || !bearer) {\n throw new Error(`/register did not return agent.id + agent.api_key: ${JSON.stringify(registration).slice(0, 400)}`);\n }\n if (registration.wallet?.private_key) {\n console.warn('[provision] WARN — server returned private_key despite x402 flow; ignoring (using local key).');\n }\n\n let joinedTeam: ProvisionExecuted['joinedTeam'];\n if (req.autoJoinTeamId) {\n try {\n const joinRes = await postJson<{ success?: boolean; role?: string; members?: number }>(\n fetchImpl,\n `${meshApiBase}/team/${req.autoJoinTeamId}/join`,\n bearer,\n {}\n );\n joinedTeam = {\n teamId: req.autoJoinTeamId,\n role: joinRes.role ?? 'member',\n members: joinRes.members ?? 0,\n };\n } catch (err) {\n joinedTeam = {\n teamId: req.autoJoinTeamId,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n }\n\n return {\n status: 'executed',\n handle: req.handle,\n surface,\n seatId,\n seatDir,\n walletAddress: wallet.address,\n bearer,\n agentId,\n envVarLines: envVarLinesFor(req.handle, wallet.address, bearer),\n joinedTeam,\n };\n}\n\nfunction defaultSeatsRoot(): string {\n return process.env.HOLOSCRIPT_AGENT_SEATS_ROOT\n ?? join(homedir(), '.holoscript-agent', 'seats');\n}\n\nfunction makeSeatId(surface: string): string {\n const fp = createHash('sha256').update(hostname() + homedir()).digest('hex').slice(0, 8);\n return `holoscript-${surface}-${fp}-x402`;\n}\n\nfunction ensureMasterKey(seatsRoot: string): Buffer {\n const keyPath = join(seatsRoot, '.master-key');\n if (!existsSync(seatsRoot)) mkdirSync(seatsRoot, { recursive: true });\n if (!existsSync(keyPath)) {\n const k = randomBytes(32);\n writeFileSync(keyPath, k);\n try { chmodSync(keyPath, 0o600); } catch {}\n }\n return readFileSync(keyPath);\n}\n\nfunction encryptPrivateKey(\n privKey: string,\n masterKey: Buffer\n): { iv: string; ct: string; tag: string; alg: 'aes-256-gcm' } {\n const iv = randomBytes(12);\n const cipher = createCipheriv('aes-256-gcm', masterKey, iv);\n const ct = Buffer.concat([cipher.update(privKey, 'utf8'), cipher.final()]);\n return { iv: iv.toString('base64'), ct: ct.toString('base64'), tag: cipher.getAuthTag().toString('base64'), alg: 'aes-256-gcm' };\n}\n\nasync function postJson<T>(\n fetchImpl: typeof fetch,\n url: string,\n bearer: string,\n body: unknown\n): Promise<T> {\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${bearer}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n const text = await res.text();\n if (!res.ok) {\n throw new Error(`POST ${url} ${res.status}: ${text.slice(0, 400)}`);\n }\n try {\n return JSON.parse(text) as T;\n } catch {\n throw new Error(`POST ${url} returned non-JSON: ${text.slice(0, 200)}`);\n }\n}\n\nfunction envVarLinesFor(handle: string, walletAddress: string, bearer?: string): string[] {\n const suffix = handle.toUpperCase().replace(/-/g, '_');\n const lines = [`HOLOSCRIPT_AGENT_WALLET_${suffix}=${walletAddress}`];\n if (bearer) {\n lines.push(`HOLOMESH_API_KEY_${suffix}_X402=${bearer}`);\n }\n return lines;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,62 @@
1
+ import { ILLMProvider } from '@holoscript/llm-provider';
2
+ import { CostGuard } from './cost-guard.js';
3
+ import { HolomeshClient } from './holomesh-client.js';
4
+ import { AuditLog } from './audit-log.js';
5
+ import { AgentIdentity, RuntimeBrainConfig, ExecutionResult, BoardTask, TickResult } from './types.js';
6
+
7
+ interface AgentRunnerOptions {
8
+ identity: AgentIdentity;
9
+ brain: RuntimeBrainConfig;
10
+ provider: ILLMProvider;
11
+ costGuard: CostGuard;
12
+ mesh: HolomeshClient;
13
+ logger?: (event: Record<string, unknown>) => void;
14
+ onTaskExecuted?: (result: ExecutionResult, task: BoardTask) => Promise<void>;
15
+ auditLog?: AuditLog;
16
+ }
17
+ declare class AgentRunner {
18
+ private readonly opts;
19
+ private stopped;
20
+ private prevCaelChain;
21
+ private joinedThisProcess;
22
+ constructor(opts: AgentRunnerOptions);
23
+ tick(): Promise<TickResult>;
24
+ runForever(opts?: {
25
+ tickIntervalMs?: number;
26
+ }): Promise<void>;
27
+ stop(): void;
28
+ /**
29
+ * Heartbeat with one-shot self-rejoin on 403 "Not a member of this team".
30
+ *
31
+ * Pairs with task_1777112258989_eeyp: fresh-deploy fleet workers whose
32
+ * provisioning didn't atomically call /join (or whose membership was
33
+ * reaped) hit 403 every tick and never recover. We detect the specific
34
+ * server error string (see packages/mcp-server/src/holomesh/routes/
35
+ * team-routes.ts:903 → `{ error: 'Not a member' }` for /presence), call
36
+ * mesh.joinTeam() ONCE per runner process, and retry the heartbeat.
37
+ *
38
+ * Strict scope:
39
+ * - Only retries on 403 + "Not a member" body. Any other 403 (insufficient
40
+ * permissions, signing failure) re-throws unchanged.
41
+ * - Only retries ONCE per process. If we already rejoined this process and
42
+ * the heartbeat is *still* 403, the team is rejecting us for a reason
43
+ * /join can't fix (e.g. capacity, ban) — surface the error.
44
+ * - If joinTeam() itself throws, we DO mark joinedThisProcess=true before
45
+ * re-throwing so we don't slam the join endpoint on every subsequent
46
+ * tick. The next tick will surface the same heartbeat 403 and the
47
+ * runner-level catch in runForever logs tick-error and sleeps. Operator
48
+ * inspection (SSH/log) is the recovery path at that point.
49
+ */
50
+ private heartbeatWithAutoRejoin;
51
+ /**
52
+ * Detect the server's "Not a member" 403 error from HolomeshClient.req().
53
+ * The error message format is: `HoloMesh POST /team/<id>/presence 403: <body>`
54
+ * where body contains `{"error":"Not a member"}` (or "Not a member of this team").
55
+ * Match conservatively: BOTH a "403" status marker AND the "Not a member"
56
+ * substring must appear, so unrelated 403s (insufficient permissions,
57
+ * signing failures) do NOT trigger a rejoin.
58
+ */
59
+ private isNotAMemberError;
60
+ }
61
+
62
+ export { AgentRunner, type AgentRunnerOptions };