@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.
- package/LICENSE +21 -0
- package/dist/ablation.d.ts +56 -0
- package/dist/ablation.js +183 -0
- package/dist/ablation.js.map +1 -0
- package/dist/audit-log.d.ts +99 -0
- package/dist/audit-log.js +123 -0
- package/dist/audit-log.js.map +1 -0
- package/dist/brain.d.ts +6 -0
- package/dist/brain.js +66 -0
- package/dist/brain.js.map +1 -0
- package/dist/commit-hook.d.ts +22 -0
- package/dist/commit-hook.js +103 -0
- package/dist/commit-hook.js.map +1 -0
- package/dist/cost-guard.d.ts +54 -0
- package/dist/cost-guard.js +92 -0
- package/dist/cost-guard.js.map +1 -0
- package/dist/holomesh-client.d.ts +63 -0
- package/dist/holomesh-client.js +117 -0
- package/dist/holomesh-client.js.map +1 -0
- package/dist/identity.d.ts +7 -0
- package/dist/identity.js +64 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2101 -0
- package/dist/index.js.map +1 -0
- package/dist/provision.d.ts +43 -0
- package/dist/provision.js +195 -0
- package/dist/provision.js.map +1 -0
- package/dist/runner.d.ts +62 -0
- package/dist/runner.js +543 -0
- package/dist/runner.js.map +1 -0
- package/dist/supervisor-config.d.ts +26 -0
- package/dist/supervisor-config.js +109 -0
- package/dist/supervisor-config.js.map +1 -0
- package/dist/supervisor.d.ts +53 -0
- package/dist/supervisor.js +1167 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +99 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { homedir as homedir3 } from "os";
|
|
5
|
+
import { join as join4 } from "path";
|
|
6
|
+
import {
|
|
7
|
+
createAnthropicProvider,
|
|
8
|
+
createOpenAIProvider,
|
|
9
|
+
createGeminiProvider,
|
|
10
|
+
createMockProvider,
|
|
11
|
+
createLocalLLMProvider
|
|
12
|
+
} from "@holoscript/llm-provider";
|
|
13
|
+
|
|
14
|
+
// src/identity.ts
|
|
15
|
+
var VALID_PROVIDERS = /* @__PURE__ */ new Set([
|
|
16
|
+
"anthropic",
|
|
17
|
+
"openai",
|
|
18
|
+
"gemini",
|
|
19
|
+
"mock",
|
|
20
|
+
"bitnet",
|
|
21
|
+
"local-llm"
|
|
22
|
+
]);
|
|
23
|
+
function loadIdentity(env = process.env) {
|
|
24
|
+
const handle = required(env, "HOLOSCRIPT_AGENT_HANDLE");
|
|
25
|
+
const provider = required(env, "HOLOSCRIPT_AGENT_PROVIDER");
|
|
26
|
+
if (!VALID_PROVIDERS.has(provider)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`HOLOSCRIPT_AGENT_PROVIDER=${provider} not in [${[...VALID_PROVIDERS].join(", ")}]`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const x402Bearer = required(env, "HOLOSCRIPT_AGENT_X402_BEARER");
|
|
32
|
+
const wallet = required(env, "HOLOSCRIPT_AGENT_WALLET");
|
|
33
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(wallet)) {
|
|
34
|
+
throw new Error(`HOLOSCRIPT_AGENT_WALLET is not a 0x-prefixed 40-hex address: ${wallet}`);
|
|
35
|
+
}
|
|
36
|
+
const budgetRaw = env.HOLOSCRIPT_AGENT_BUDGET_USD_DAY ?? "5";
|
|
37
|
+
const budget = Number(budgetRaw);
|
|
38
|
+
if (!Number.isFinite(budget) || budget < 0) {
|
|
39
|
+
throw new Error(`HOLOSCRIPT_AGENT_BUDGET_USD_DAY must be >= 0 (0 = unlimited), got ${budgetRaw}`);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
handle,
|
|
43
|
+
surface: env.HOLOSCRIPT_AGENT_SURFACE ?? handle,
|
|
44
|
+
wallet,
|
|
45
|
+
x402Bearer,
|
|
46
|
+
llmProvider: provider,
|
|
47
|
+
llmModel: required(env, "HOLOSCRIPT_AGENT_MODEL"),
|
|
48
|
+
brainPath: required(env, "HOLOSCRIPT_AGENT_BRAIN"),
|
|
49
|
+
budgetUsdPerDay: budget,
|
|
50
|
+
teamId: required(env, "HOLOMESH_TEAM_ID"),
|
|
51
|
+
meshApiBase: env.HOLOMESH_API_BASE ?? "https://mcp.holoscript.net/api/holomesh"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function required(env, key) {
|
|
55
|
+
const v = env[key];
|
|
56
|
+
if (!v || v.trim().length === 0) {
|
|
57
|
+
throw new Error(`Missing required env var: ${key}`);
|
|
58
|
+
}
|
|
59
|
+
return v.trim();
|
|
60
|
+
}
|
|
61
|
+
function identityForLog(id) {
|
|
62
|
+
return {
|
|
63
|
+
handle: id.handle,
|
|
64
|
+
surface: id.surface,
|
|
65
|
+
wallet: `${id.wallet.slice(0, 6)}\u2026${id.wallet.slice(-4)}`,
|
|
66
|
+
bearer: `${id.x402Bearer.slice(0, 6)}\u2026`,
|
|
67
|
+
provider: id.llmProvider,
|
|
68
|
+
model: id.llmModel,
|
|
69
|
+
brain: id.brainPath,
|
|
70
|
+
budgetUsdPerDay: id.budgetUsdPerDay
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/brain.ts
|
|
75
|
+
import { readFile } from "fs/promises";
|
|
76
|
+
async function loadBrain(brainPath, scopeTier = "warm") {
|
|
77
|
+
const systemPrompt = await readFile(brainPath, "utf8");
|
|
78
|
+
const { domain, capabilityTags } = extractIdentity(systemPrompt);
|
|
79
|
+
return { brainPath, systemPrompt, capabilityTags, domain, scopeTier };
|
|
80
|
+
}
|
|
81
|
+
function extractIdentity(brain) {
|
|
82
|
+
const identityBlock = sliceNamedBlock(brain, "identity");
|
|
83
|
+
if (!identityBlock) return { domain: "unknown", capabilityTags: [] };
|
|
84
|
+
const domain = scalarField(identityBlock, "domain") ?? "unknown";
|
|
85
|
+
const capabilityTags = listField(identityBlock, "capability_tags") ?? [];
|
|
86
|
+
return { domain, capabilityTags };
|
|
87
|
+
}
|
|
88
|
+
function sliceNamedBlock(src, name) {
|
|
89
|
+
const re = new RegExp(`\\b${name}\\s*:?\\s*\\{`, "g");
|
|
90
|
+
const match = re.exec(src);
|
|
91
|
+
if (!match) return void 0;
|
|
92
|
+
const headerEnd = match.index + match[0].length;
|
|
93
|
+
let depth = 1;
|
|
94
|
+
for (let i = headerEnd; i < src.length; i++) {
|
|
95
|
+
const ch = src[i];
|
|
96
|
+
if (ch === "{") depth++;
|
|
97
|
+
else if (ch === "}") {
|
|
98
|
+
depth--;
|
|
99
|
+
if (depth === 0) return src.slice(headerEnd, i);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
function scalarField(block, key) {
|
|
105
|
+
const idx = block.indexOf(`${key}:`);
|
|
106
|
+
if (idx < 0) return void 0;
|
|
107
|
+
const after = block.slice(idx + key.length + 1).trimStart();
|
|
108
|
+
if (after.startsWith('"')) {
|
|
109
|
+
const end = after.indexOf('"', 1);
|
|
110
|
+
if (end > 0) return after.slice(1, end);
|
|
111
|
+
}
|
|
112
|
+
const eol = after.indexOf("\n");
|
|
113
|
+
return after.slice(0, eol < 0 ? void 0 : eol).trim();
|
|
114
|
+
}
|
|
115
|
+
function listField(block, key) {
|
|
116
|
+
const idx = block.indexOf(`${key}:`);
|
|
117
|
+
if (idx < 0) return void 0;
|
|
118
|
+
const after = block.slice(idx + key.length + 1).trimStart();
|
|
119
|
+
if (!after.startsWith("[")) return void 0;
|
|
120
|
+
let depth = 0;
|
|
121
|
+
let end = -1;
|
|
122
|
+
for (let i = 0; i < after.length; i++) {
|
|
123
|
+
if (after[i] === "[") depth++;
|
|
124
|
+
else if (after[i] === "]") {
|
|
125
|
+
depth--;
|
|
126
|
+
if (depth === 0) {
|
|
127
|
+
end = i;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (end < 0) return void 0;
|
|
133
|
+
const inner = after.slice(1, end);
|
|
134
|
+
return inner.split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter((s) => s.length > 0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/cost-guard.ts
|
|
138
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
139
|
+
import { dirname } from "path";
|
|
140
|
+
var ANTHROPIC_PRICING_USD_PER_MTOK = {
|
|
141
|
+
"claude-opus-4-7": { input: 15, output: 75 },
|
|
142
|
+
"claude-opus-4-6": { input: 15, output: 75 },
|
|
143
|
+
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
144
|
+
"claude-haiku-4-5-20251001": { input: 1, output: 5 },
|
|
145
|
+
"claude-haiku-4-5": { input: 1, output: 5 }
|
|
146
|
+
};
|
|
147
|
+
function defaultAnthropicPricer(model, usage) {
|
|
148
|
+
const price = ANTHROPIC_PRICING_USD_PER_MTOK[model];
|
|
149
|
+
if (!price) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`No pricing configured for model "${model}" \u2014 add to ANTHROPIC_PRICING_USD_PER_MTOK or pass a custom pricer`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return (usage.promptTokens * price.input + usage.completionTokens * price.output) / 1e6;
|
|
155
|
+
}
|
|
156
|
+
function defaultLocalLlmPricer(_model, _usage) {
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
function defaultPricerForProvider(provider) {
|
|
160
|
+
if (provider === "local-llm" || provider === "mock") return defaultLocalLlmPricer;
|
|
161
|
+
return defaultAnthropicPricer;
|
|
162
|
+
}
|
|
163
|
+
var CostGuard = class {
|
|
164
|
+
constructor(opts) {
|
|
165
|
+
this.statePath = opts.statePath;
|
|
166
|
+
this.dailyBudgetUsd = opts.dailyBudgetUsd;
|
|
167
|
+
this.pricer = opts.pricer ?? defaultAnthropicPricer;
|
|
168
|
+
this.state = this.loadOrInit();
|
|
169
|
+
}
|
|
170
|
+
recordUsage(model, usage) {
|
|
171
|
+
this.rolloverIfNewDay();
|
|
172
|
+
const costUsd = this.pricer(model, usage);
|
|
173
|
+
this.state.spentUsd += costUsd;
|
|
174
|
+
this.state.promptTokens += usage.promptTokens;
|
|
175
|
+
this.state.completionTokens += usage.completionTokens;
|
|
176
|
+
this.state.callCount += 1;
|
|
177
|
+
this.persist();
|
|
178
|
+
return {
|
|
179
|
+
costUsd,
|
|
180
|
+
spentUsd: this.state.spentUsd,
|
|
181
|
+
remainingUsd: Math.max(0, this.dailyBudgetUsd - this.state.spentUsd)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
isOverBudget() {
|
|
185
|
+
if (this.dailyBudgetUsd === 0) return false;
|
|
186
|
+
this.rolloverIfNewDay();
|
|
187
|
+
return this.state.spentUsd >= this.dailyBudgetUsd;
|
|
188
|
+
}
|
|
189
|
+
getRemainingUsd() {
|
|
190
|
+
if (this.dailyBudgetUsd === 0) return Number.POSITIVE_INFINITY;
|
|
191
|
+
this.rolloverIfNewDay();
|
|
192
|
+
return Math.max(0, this.dailyBudgetUsd - this.state.spentUsd);
|
|
193
|
+
}
|
|
194
|
+
getState() {
|
|
195
|
+
this.rolloverIfNewDay();
|
|
196
|
+
return { ...this.state };
|
|
197
|
+
}
|
|
198
|
+
rolloverIfNewDay() {
|
|
199
|
+
const today = todayUtc();
|
|
200
|
+
if (this.state.date !== today) {
|
|
201
|
+
this.state = { date: today, spentUsd: 0, promptTokens: 0, completionTokens: 0, callCount: 0 };
|
|
202
|
+
this.persist();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
loadOrInit() {
|
|
206
|
+
if (existsSync(this.statePath)) {
|
|
207
|
+
const raw = readFileSync(this.statePath, "utf8");
|
|
208
|
+
const parsed = JSON.parse(raw);
|
|
209
|
+
if (parsed.date === todayUtc()) return parsed;
|
|
210
|
+
}
|
|
211
|
+
return { date: todayUtc(), spentUsd: 0, promptTokens: 0, completionTokens: 0, callCount: 0 };
|
|
212
|
+
}
|
|
213
|
+
persist() {
|
|
214
|
+
mkdirSync(dirname(this.statePath), { recursive: true });
|
|
215
|
+
writeFileSync(this.statePath, JSON.stringify(this.state, null, 2), "utf8");
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
function todayUtc() {
|
|
219
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/holomesh-client.ts
|
|
223
|
+
var HolomeshClient = class {
|
|
224
|
+
constructor(opts) {
|
|
225
|
+
this.apiBase = opts.apiBase.replace(/\/$/, "");
|
|
226
|
+
this.bearer = opts.bearer;
|
|
227
|
+
this.teamId = opts.teamId;
|
|
228
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
229
|
+
}
|
|
230
|
+
async heartbeat(payload) {
|
|
231
|
+
await this.req("POST", `/team/${this.teamId}/presence`, payload);
|
|
232
|
+
}
|
|
233
|
+
async getOpenTasks() {
|
|
234
|
+
const data = await this.req(
|
|
235
|
+
"GET",
|
|
236
|
+
`/team/${this.teamId}/board`
|
|
237
|
+
);
|
|
238
|
+
return data.tasks ?? data.open ?? [];
|
|
239
|
+
}
|
|
240
|
+
async claim(taskId) {
|
|
241
|
+
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, { action: "claim" });
|
|
242
|
+
}
|
|
243
|
+
async joinTeam() {
|
|
244
|
+
return this.req(
|
|
245
|
+
"POST",
|
|
246
|
+
`/team/${this.teamId}/join`,
|
|
247
|
+
{}
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
async sendMessageOnTask(taskId, body) {
|
|
251
|
+
await this.req("POST", `/team/${this.teamId}/message`, {
|
|
252
|
+
to: "team",
|
|
253
|
+
subject: `task:${taskId}`,
|
|
254
|
+
content: body
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
async markDone(taskId, summary, commitHash) {
|
|
258
|
+
await this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, {
|
|
259
|
+
action: "done",
|
|
260
|
+
summary,
|
|
261
|
+
commitHash
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// POST CAEL audit records for this agent. Server validator at
|
|
265
|
+
// packages/mcp-server/src/holomesh/routes/core-routes.ts:472-533 requires
|
|
266
|
+
// bearer == handle owner OR founder; the per-surface x402 bearer is the
|
|
267
|
+
// handle owner so this resolves correctly. Records that fail shape
|
|
268
|
+
// validation (layer_hashes != 7 elements, missing tick_iso/operation/
|
|
269
|
+
// fnv1a_chain) are silently dropped server-side, not rejected as a batch.
|
|
270
|
+
async postAuditRecords(handle, records) {
|
|
271
|
+
return this.req(
|
|
272
|
+
"POST",
|
|
273
|
+
`/agent/${encodeURIComponent(handle)}/audit`,
|
|
274
|
+
{ records }
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
async whoAmI() {
|
|
278
|
+
const raw = await this.req("GET", "/me");
|
|
279
|
+
return {
|
|
280
|
+
agentId: raw.agentId,
|
|
281
|
+
surface: deriveSurface(raw.name),
|
|
282
|
+
wallet: raw.wallet
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
async req(method, path, body) {
|
|
286
|
+
const url = `${this.apiBase}${path}`;
|
|
287
|
+
const res = await this.fetchImpl(url, {
|
|
288
|
+
method,
|
|
289
|
+
headers: {
|
|
290
|
+
Authorization: `Bearer ${this.bearer}`,
|
|
291
|
+
"content-type": "application/json"
|
|
292
|
+
},
|
|
293
|
+
body: body ? JSON.stringify(body) : void 0
|
|
294
|
+
});
|
|
295
|
+
if (!res.ok) {
|
|
296
|
+
const txt = await res.text().catch(() => "");
|
|
297
|
+
throw new Error(`HoloMesh ${method} ${path} ${res.status}: ${txt.slice(0, 300)}`);
|
|
298
|
+
}
|
|
299
|
+
if (res.status === 204) return void 0;
|
|
300
|
+
return await res.json();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
function deriveSurface(seatName) {
|
|
304
|
+
if (!seatName) return "unknown";
|
|
305
|
+
const n = seatName.toLowerCase();
|
|
306
|
+
if (n.startsWith("claudecode")) return "claude-code";
|
|
307
|
+
if (n.startsWith("cursor")) return "claude-cursor";
|
|
308
|
+
if (n.startsWith("claudedesktop")) return "claude-desktop";
|
|
309
|
+
if (n.startsWith("vscode-claude") || n.startsWith("claude-vscode")) return "claude-vscode";
|
|
310
|
+
if (n.startsWith("gemini")) return "gemini-antigravity";
|
|
311
|
+
if (n.startsWith("copilot")) return "copilot-vscode";
|
|
312
|
+
return "unknown";
|
|
313
|
+
}
|
|
314
|
+
function pickClaimableTask(tasks, brainCapabilityTags) {
|
|
315
|
+
const wanted = new Set(brainCapabilityTags.map((t) => t.toLowerCase()));
|
|
316
|
+
const open = tasks.filter((t) => t.status === "open" && !t.claimedBy);
|
|
317
|
+
const scored = open.map((t) => ({ task: t, score: scoreTask(t, wanted) })).filter((s) => s.score > 0).sort((a, b) => b.score - a.score || priority(a.task) - priority(b.task));
|
|
318
|
+
return scored[0]?.task;
|
|
319
|
+
}
|
|
320
|
+
function scoreTask(task, wanted) {
|
|
321
|
+
const tags = (task.tags ?? []).map((t) => t.toLowerCase());
|
|
322
|
+
const text = `${task.title} ${task.description ?? ""}`.toLowerCase();
|
|
323
|
+
let score = 0;
|
|
324
|
+
for (const tag of tags) if (wanted.has(tag)) score += 2;
|
|
325
|
+
for (const w of wanted) if (text.includes(w)) score += 1;
|
|
326
|
+
return score;
|
|
327
|
+
}
|
|
328
|
+
function priority(t) {
|
|
329
|
+
if (typeof t.priority === "number") return t.priority;
|
|
330
|
+
const map = { critical: 1, high: 2, medium: 4, low: 6 };
|
|
331
|
+
return map[String(t.priority).toLowerCase()] ?? 5;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/cael-builder.ts
|
|
335
|
+
import { createHash } from "crypto";
|
|
336
|
+
function sha(input) {
|
|
337
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
338
|
+
}
|
|
339
|
+
function brainClassOf(brain) {
|
|
340
|
+
const p = String(brain.brainPath ?? "");
|
|
341
|
+
let m = p.match(/compositions[\\/]([\w-]+)-brain\.hsplus$/);
|
|
342
|
+
if (m) return m[1];
|
|
343
|
+
m = p.match(/([\w-]+)-brain\.hsplus$/);
|
|
344
|
+
if (m) return m[1];
|
|
345
|
+
m = p.match(/([\w-]+)\.hsplus$/);
|
|
346
|
+
if (m) return m[1];
|
|
347
|
+
const domain = String(brain.domain ?? "").trim();
|
|
348
|
+
if (domain && domain !== "unknown") return domain;
|
|
349
|
+
return "unknown";
|
|
350
|
+
}
|
|
351
|
+
function buildCaelRecord(input) {
|
|
352
|
+
const { identity, brain, task, messages, finalText, usage, costUsd, spentUsd, prevChain, runtimeVersion } = input;
|
|
353
|
+
const l0 = sha(brain.systemPrompt);
|
|
354
|
+
const l1 = sha(`${task.id}|${task.title}|${task.description ?? ""}`);
|
|
355
|
+
const l2 = sha(JSON.stringify(messages));
|
|
356
|
+
const l3 = sha(finalText);
|
|
357
|
+
const l4 = sha(JSON.stringify(usage));
|
|
358
|
+
const l5 = sha(`${costUsd.toFixed(6)}|${spentUsd.toFixed(6)}`);
|
|
359
|
+
const l6 = sha([l0, l1, l2, l3, l4, l5].join("|"));
|
|
360
|
+
const fnv1a_chain = sha(`${prevChain ?? ""}|${l6}`);
|
|
361
|
+
return {
|
|
362
|
+
tick_iso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
363
|
+
layer_hashes: [l0, l1, l2, l3, l4, l5, l6],
|
|
364
|
+
operation: `task-executed:${task.id}`,
|
|
365
|
+
prev_hash: prevChain,
|
|
366
|
+
fnv1a_chain,
|
|
367
|
+
version_vector_fingerprint: `agent@${runtimeVersion}|brain@${brainClassOf(brain)}|provider@${identity.llmProvider}|model@${identity.llmModel}`,
|
|
368
|
+
brain_class: brainClassOf(brain)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/tools.ts
|
|
373
|
+
import { readFile as readFile2, writeFile, readdir, mkdir, stat } from "fs/promises";
|
|
374
|
+
import { resolve, dirname as dirname2 } from "path";
|
|
375
|
+
import { spawn } from "child_process";
|
|
376
|
+
var ALLOWED_READ_ROOTS = [
|
|
377
|
+
"/root/msc-paper-22",
|
|
378
|
+
// Paper 22 mechanization inputs (scp'd by deploy)
|
|
379
|
+
"/root/holoscript-mesh",
|
|
380
|
+
// Read-only repo view (clone path on instance)
|
|
381
|
+
"/root/agent-output"
|
|
382
|
+
// Read back what we wrote
|
|
383
|
+
];
|
|
384
|
+
var ALLOWED_WRITE_ROOTS = [
|
|
385
|
+
"/root/agent-output"
|
|
386
|
+
// Single write sink — keeps deliverables in one place
|
|
387
|
+
];
|
|
388
|
+
var BASH_WHITELIST = [
|
|
389
|
+
"lake build",
|
|
390
|
+
"lake env",
|
|
391
|
+
"lake clean",
|
|
392
|
+
"lean ",
|
|
393
|
+
"ls ",
|
|
394
|
+
"ls\n",
|
|
395
|
+
"ls$",
|
|
396
|
+
"cat ",
|
|
397
|
+
"grep ",
|
|
398
|
+
"rg ",
|
|
399
|
+
"find ",
|
|
400
|
+
"wc ",
|
|
401
|
+
"head ",
|
|
402
|
+
"tail ",
|
|
403
|
+
"git status",
|
|
404
|
+
"git log",
|
|
405
|
+
"git diff",
|
|
406
|
+
"git show",
|
|
407
|
+
"pnpm --filter",
|
|
408
|
+
"pnpm vitest",
|
|
409
|
+
"vitest run",
|
|
410
|
+
"pwd",
|
|
411
|
+
"echo "
|
|
412
|
+
];
|
|
413
|
+
var MESH_TOOLS = [
|
|
414
|
+
{
|
|
415
|
+
name: "read_file",
|
|
416
|
+
description: "Read a file from the agent sandbox. Allowed roots: /root/msc-paper-22, /root/holoscript-mesh, /root/agent-output. Returns the file content as text. Use this to inspect inputs scp'd to the instance (e.g. MSC/Invariants.lean).",
|
|
417
|
+
input_schema: {
|
|
418
|
+
type: "object",
|
|
419
|
+
properties: {
|
|
420
|
+
path: { type: "string", description: "Absolute path under an allowed read root" }
|
|
421
|
+
},
|
|
422
|
+
required: ["path"]
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "list_dir",
|
|
427
|
+
description: "List entries in a directory under the agent sandbox. Same root restrictions as read_file. Returns a newline-separated list of entries.",
|
|
428
|
+
input_schema: {
|
|
429
|
+
type: "object",
|
|
430
|
+
properties: {
|
|
431
|
+
path: { type: "string", description: "Absolute path under an allowed read root" }
|
|
432
|
+
},
|
|
433
|
+
required: ["path"]
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: "write_file",
|
|
438
|
+
description: "Write a file to /root/agent-output/. This is the deliverable sink \u2014 anything you want to emit as task output (a Lean proof, a markdown report, a JSON dataset) goes here. Creates parent directories. Will refuse paths outside the write root.",
|
|
439
|
+
input_schema: {
|
|
440
|
+
type: "object",
|
|
441
|
+
properties: {
|
|
442
|
+
path: { type: "string", description: "Absolute path under /root/agent-output/" },
|
|
443
|
+
content: { type: "string", description: "File content to write (UTF-8)" }
|
|
444
|
+
},
|
|
445
|
+
required: ["path", "content"]
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: "bash",
|
|
450
|
+
description: "Run a shell command. Whitelisted prefixes only: lake build, lean, ls, cat, grep, find, wc, head, tail, git status/log/diff/show, pnpm --filter, vitest run, pwd, echo. Hard 60s wall timeout, 1MB stdout cap. Use for lake build / lean kernel-checks, git inspection, repo greps. Refuses rm, curl, ssh, sudo, eval.",
|
|
451
|
+
input_schema: {
|
|
452
|
+
type: "object",
|
|
453
|
+
properties: {
|
|
454
|
+
cmd: { type: "string", description: "Whitelisted shell command" },
|
|
455
|
+
cwd: { type: "string", description: "Optional working directory (defaults to /root)" }
|
|
456
|
+
},
|
|
457
|
+
required: ["cmd"]
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
];
|
|
461
|
+
function isUnderRoot(absPath, root) {
|
|
462
|
+
const resolved = resolve(absPath);
|
|
463
|
+
const rootResolved = resolve(root);
|
|
464
|
+
return resolved === rootResolved || resolved.startsWith(rootResolved + "/");
|
|
465
|
+
}
|
|
466
|
+
function checkReadAllowed(path) {
|
|
467
|
+
if (!path.startsWith("/")) return `path must be absolute, got "${path}"`;
|
|
468
|
+
for (const root of ALLOWED_READ_ROOTS) {
|
|
469
|
+
if (isUnderRoot(path, root)) return null;
|
|
470
|
+
}
|
|
471
|
+
return `read denied \u2014 path "${path}" not under allowed roots: ${ALLOWED_READ_ROOTS.join(", ")}`;
|
|
472
|
+
}
|
|
473
|
+
function checkWriteAllowed(path) {
|
|
474
|
+
if (!path.startsWith("/")) return `path must be absolute, got "${path}"`;
|
|
475
|
+
for (const root of ALLOWED_WRITE_ROOTS) {
|
|
476
|
+
if (isUnderRoot(path, root)) return null;
|
|
477
|
+
}
|
|
478
|
+
return `write denied \u2014 path "${path}" not under allowed roots: ${ALLOWED_WRITE_ROOTS.join(", ")}`;
|
|
479
|
+
}
|
|
480
|
+
function checkBashAllowed(cmd) {
|
|
481
|
+
const trimmed = cmd.trim();
|
|
482
|
+
if (trimmed.length === 0) return "empty command";
|
|
483
|
+
if (/[;&|`$<>]|>>|\|\||&&/.test(trimmed)) {
|
|
484
|
+
return `command contains shell metachars (; & | \` $ < > >> || &&) \u2014 not allowed for safety`;
|
|
485
|
+
}
|
|
486
|
+
for (const prefix of BASH_WHITELIST) {
|
|
487
|
+
if (trimmed.startsWith(prefix.trim())) return null;
|
|
488
|
+
}
|
|
489
|
+
return `command not on whitelist. Allowed prefixes: ${BASH_WHITELIST.join(" / ")}`;
|
|
490
|
+
}
|
|
491
|
+
async function runTool(use) {
|
|
492
|
+
try {
|
|
493
|
+
if (use.name === "read_file") {
|
|
494
|
+
const path = use.input.path;
|
|
495
|
+
const denied = checkReadAllowed(path);
|
|
496
|
+
if (denied) return errResult(use.id, denied);
|
|
497
|
+
const text = await readFile2(path, "utf8");
|
|
498
|
+
const truncated = text.length > 2e5 ? text.slice(0, 2e5) + `
|
|
499
|
+
\u2026[truncated, full file is ${text.length} bytes]` : text;
|
|
500
|
+
return okResult(use.id, truncated);
|
|
501
|
+
}
|
|
502
|
+
if (use.name === "list_dir") {
|
|
503
|
+
const path = use.input.path;
|
|
504
|
+
const denied = checkReadAllowed(path);
|
|
505
|
+
if (denied) return errResult(use.id, denied);
|
|
506
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
507
|
+
const lines = entries.map((e) => `${e.isDirectory() ? "d" : "-"} ${e.name}`);
|
|
508
|
+
return okResult(use.id, lines.join("\n"));
|
|
509
|
+
}
|
|
510
|
+
if (use.name === "write_file") {
|
|
511
|
+
const path = use.input.path;
|
|
512
|
+
const content = use.input.content;
|
|
513
|
+
const denied = checkWriteAllowed(path);
|
|
514
|
+
if (denied) return errResult(use.id, denied);
|
|
515
|
+
await mkdir(dirname2(path), { recursive: true });
|
|
516
|
+
await writeFile(path, content, "utf8");
|
|
517
|
+
const s = await stat(path);
|
|
518
|
+
return okResult(use.id, `wrote ${s.size} bytes to ${path}`);
|
|
519
|
+
}
|
|
520
|
+
if (use.name === "bash") {
|
|
521
|
+
const cmd = use.input.cmd;
|
|
522
|
+
const cwd = use.input.cwd ?? "/root";
|
|
523
|
+
const denied = checkBashAllowed(cmd);
|
|
524
|
+
if (denied) return errResult(use.id, denied);
|
|
525
|
+
const result = await runBash(cmd, cwd);
|
|
526
|
+
return result.code === 0 ? okResult(use.id, result.stdout) : errResult(use.id, `exit=${result.code}
|
|
527
|
+
${result.stderr || result.stdout}`);
|
|
528
|
+
}
|
|
529
|
+
return errResult(use.id, `unknown tool: ${use.name}`);
|
|
530
|
+
} catch (err) {
|
|
531
|
+
return errResult(use.id, err instanceof Error ? err.message : String(err));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function runBash(cmd, cwd) {
|
|
535
|
+
return new Promise((resolveProm) => {
|
|
536
|
+
const child = spawn("bash", ["-c", cmd], { cwd, env: process.env });
|
|
537
|
+
let stdout = "";
|
|
538
|
+
let stderr = "";
|
|
539
|
+
let killed = false;
|
|
540
|
+
const STDOUT_CAP = 1e6;
|
|
541
|
+
const TIMEOUT_MS = 6e4;
|
|
542
|
+
const killer = setTimeout(() => {
|
|
543
|
+
killed = true;
|
|
544
|
+
child.kill("SIGKILL");
|
|
545
|
+
}, TIMEOUT_MS);
|
|
546
|
+
child.stdout.on("data", (d) => {
|
|
547
|
+
if (stdout.length < STDOUT_CAP) stdout += d.toString("utf8");
|
|
548
|
+
});
|
|
549
|
+
child.stderr.on("data", (d) => {
|
|
550
|
+
if (stderr.length < STDOUT_CAP) stderr += d.toString("utf8");
|
|
551
|
+
});
|
|
552
|
+
child.on("error", (err) => {
|
|
553
|
+
clearTimeout(killer);
|
|
554
|
+
resolveProm({ code: 1, stdout, stderr: stderr + "\nspawn-error: " + err.message });
|
|
555
|
+
});
|
|
556
|
+
child.on("exit", (code) => {
|
|
557
|
+
clearTimeout(killer);
|
|
558
|
+
const finalStdout = stdout.length >= STDOUT_CAP ? stdout + `
|
|
559
|
+
\u2026[stdout truncated at ${STDOUT_CAP} bytes]` : stdout;
|
|
560
|
+
const note = killed ? `
|
|
561
|
+
[bash killed after ${TIMEOUT_MS}ms timeout]` : "";
|
|
562
|
+
resolveProm({ code: code ?? 1, stdout: finalStdout + note, stderr });
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
function okResult(id, content) {
|
|
567
|
+
return { type: "tool_result", tool_use_id: id, content };
|
|
568
|
+
}
|
|
569
|
+
function errResult(id, message) {
|
|
570
|
+
return { type: "tool_result", tool_use_id: id, content: message, is_error: true };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/runner.ts
|
|
574
|
+
var RUNTIME_VERSION = "1.0.0";
|
|
575
|
+
var AgentRunner = class {
|
|
576
|
+
constructor(opts) {
|
|
577
|
+
this.opts = opts;
|
|
578
|
+
this.stopped = false;
|
|
579
|
+
// CAEL audit hash chain — survives across ticks within a single runner
|
|
580
|
+
// process. On process restart it resets to null; the first post-restart
|
|
581
|
+
// record breaks the chain, which is honest (the runner has no memory of
|
|
582
|
+
// its prior chain state and shouldn't fake continuity). prev_hash=null
|
|
583
|
+
// is a valid value the audit-store accepts.
|
|
584
|
+
this.prevCaelChain = null;
|
|
585
|
+
// Self-recovery flag for the auto-rejoin path (task_1777112258989_eeyp).
|
|
586
|
+
// When the heartbeat returns 403 "Not a member of this team" — typical of
|
|
587
|
+
// a fresh Vast.ai worker whose provisioning didn't atomically /join, or of
|
|
588
|
+
// a worker whose membership was reaped — the runner calls mesh.joinTeam()
|
|
589
|
+
// ONCE per process and retries the heartbeat. After a successful rejoin
|
|
590
|
+
// we set this flag so subsequent 403s on the same process don't loop back
|
|
591
|
+
// into joinTeam (avoiding a retry storm if the team-cap is full or the
|
|
592
|
+
// join itself is permanently rejected). On process restart the flag
|
|
593
|
+
// resets, which is the correct semantics: a fresh process gets one fresh
|
|
594
|
+
// chance to self-rejoin. Discovered 2026-04-25 SSH-probing 5 fleet
|
|
595
|
+
// workers stuck in indefinite 403→tick-error→sleep→retry loops; without
|
|
596
|
+
// this, a fresh-deploy of an unjoined agent stays silent forever.
|
|
597
|
+
this.joinedThisProcess = false;
|
|
598
|
+
}
|
|
599
|
+
async tick() {
|
|
600
|
+
const { identity, brain, mesh, costGuard, provider, logger } = this.opts;
|
|
601
|
+
const log = logger ?? (() => void 0);
|
|
602
|
+
await this.heartbeatWithAutoRejoin();
|
|
603
|
+
if (costGuard.isOverBudget()) {
|
|
604
|
+
const state = costGuard.getState();
|
|
605
|
+
log({ ev: "over-budget", spentUsd: state.spentUsd, budget: identity.budgetUsdPerDay });
|
|
606
|
+
return {
|
|
607
|
+
action: "over-budget",
|
|
608
|
+
spentUsd: state.spentUsd,
|
|
609
|
+
remainingUsd: 0,
|
|
610
|
+
message: `daily budget $${identity.budgetUsdPerDay} exhausted`
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
const tasks = await mesh.getOpenTasks();
|
|
614
|
+
const target = pickClaimableTask(tasks, brain.capabilityTags);
|
|
615
|
+
if (!target) {
|
|
616
|
+
log({ ev: "no-claimable-task", open: tasks.length });
|
|
617
|
+
return {
|
|
618
|
+
action: "no-claimable-task",
|
|
619
|
+
spentUsd: costGuard.getState().spentUsd,
|
|
620
|
+
remainingUsd: costGuard.getRemainingUsd()
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
log({ ev: "claim", taskId: target.id, title: target.title });
|
|
624
|
+
await mesh.claim(target.id);
|
|
625
|
+
const start = Date.now();
|
|
626
|
+
const messages = [
|
|
627
|
+
{ role: "system", content: brain.systemPrompt },
|
|
628
|
+
{ role: "user", content: buildTaskPrompt(target) }
|
|
629
|
+
];
|
|
630
|
+
let aggUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
631
|
+
let finalText = "";
|
|
632
|
+
let iters = 0;
|
|
633
|
+
const MAX_TOOL_ITERS = 30;
|
|
634
|
+
let lastResponse;
|
|
635
|
+
const toolsCalled = /* @__PURE__ */ new Set();
|
|
636
|
+
while (true) {
|
|
637
|
+
iters++;
|
|
638
|
+
if (iters > MAX_TOOL_ITERS) {
|
|
639
|
+
log({ ev: "tool-loop-cap", taskId: target.id, iters });
|
|
640
|
+
finalText = finalText || `[tool-loop hit ${MAX_TOOL_ITERS}-iter cap before final text]`;
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
const resp = await provider.complete(
|
|
644
|
+
{
|
|
645
|
+
messages,
|
|
646
|
+
maxTokens: 4096,
|
|
647
|
+
temperature: 0.4,
|
|
648
|
+
tools: MESH_TOOLS
|
|
649
|
+
},
|
|
650
|
+
identity.llmModel
|
|
651
|
+
);
|
|
652
|
+
lastResponse = resp;
|
|
653
|
+
aggUsage = {
|
|
654
|
+
promptTokens: aggUsage.promptTokens + resp.usage.promptTokens,
|
|
655
|
+
completionTokens: aggUsage.completionTokens + resp.usage.completionTokens,
|
|
656
|
+
totalTokens: aggUsage.totalTokens + resp.usage.totalTokens
|
|
657
|
+
};
|
|
658
|
+
if (resp.finishReason === "tool_use" && resp.toolUses && resp.toolUses.length > 0) {
|
|
659
|
+
log({ ev: "tool-call", taskId: target.id, iter: iters, tools: resp.toolUses.map((t) => t.name) });
|
|
660
|
+
for (const u of resp.toolUses) toolsCalled.add(u.name);
|
|
661
|
+
messages.push({
|
|
662
|
+
role: "assistant",
|
|
663
|
+
content: resp.assistantBlocks ?? []
|
|
664
|
+
});
|
|
665
|
+
const toolResults = await Promise.all(resp.toolUses.map((u) => runTool(u)));
|
|
666
|
+
messages.push({
|
|
667
|
+
role: "user",
|
|
668
|
+
content: toolResults
|
|
669
|
+
});
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
finalText = resp.content;
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
const durationMs = Date.now() - start;
|
|
676
|
+
const SIDE_EFFECTING_TOOLS = /* @__PURE__ */ new Set(["write_file", "bash"]);
|
|
677
|
+
const sideEffectingCalled = [...toolsCalled].some((t) => SIDE_EFFECTING_TOOLS.has(t));
|
|
678
|
+
if (!sideEffectingCalled) {
|
|
679
|
+
log({
|
|
680
|
+
ev: "no-artifact",
|
|
681
|
+
taskId: target.id,
|
|
682
|
+
tool_iters: iters,
|
|
683
|
+
toolsCalled: [...toolsCalled],
|
|
684
|
+
message: "task execution called no side-effecting tool (write_file/bash) \u2014 refusing to mark executed. Likely a pure-text or read-only-inspection response. Task remains open for a grounded attempt."
|
|
685
|
+
});
|
|
686
|
+
return {
|
|
687
|
+
action: "no-artifact",
|
|
688
|
+
taskId: target.id,
|
|
689
|
+
spentUsd: costGuard.getState().spentUsd,
|
|
690
|
+
remainingUsd: costGuard.getRemainingUsd(),
|
|
691
|
+
message: `no side-effecting tool called (toolsCalled=[${[...toolsCalled].join(",")}], iters=${iters})`
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const cost = costGuard.recordUsage(identity.llmModel, aggUsage);
|
|
695
|
+
log({
|
|
696
|
+
ev: "executed",
|
|
697
|
+
taskId: target.id,
|
|
698
|
+
costUsd: cost.costUsd.toFixed(4),
|
|
699
|
+
spentUsd: cost.spentUsd.toFixed(4),
|
|
700
|
+
tokens: aggUsage.totalTokens,
|
|
701
|
+
tool_iters: iters
|
|
702
|
+
});
|
|
703
|
+
const response = { ...lastResponse ?? { content: finalText, usage: aggUsage }, content: finalText, usage: aggUsage };
|
|
704
|
+
const execResult = {
|
|
705
|
+
taskId: target.id,
|
|
706
|
+
responseText: response.content,
|
|
707
|
+
usage: response.usage,
|
|
708
|
+
costUsd: cost.costUsd,
|
|
709
|
+
durationMs
|
|
710
|
+
};
|
|
711
|
+
if (this.opts.auditLog) {
|
|
712
|
+
try {
|
|
713
|
+
this.opts.auditLog.recordTaskExecuted({
|
|
714
|
+
identity,
|
|
715
|
+
task: target,
|
|
716
|
+
result: execResult
|
|
717
|
+
});
|
|
718
|
+
} catch (err) {
|
|
719
|
+
log({ ev: "audit-log-error", message: err instanceof Error ? err.message : String(err) });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
const caelRecord = buildCaelRecord({
|
|
724
|
+
identity,
|
|
725
|
+
brain,
|
|
726
|
+
task: target,
|
|
727
|
+
messages,
|
|
728
|
+
finalText,
|
|
729
|
+
usage: aggUsage,
|
|
730
|
+
costUsd: cost.costUsd,
|
|
731
|
+
spentUsd: cost.spentUsd,
|
|
732
|
+
prevChain: this.prevCaelChain,
|
|
733
|
+
runtimeVersion: RUNTIME_VERSION
|
|
734
|
+
});
|
|
735
|
+
const posted = await mesh.postAuditRecords(identity.handle, [caelRecord]);
|
|
736
|
+
this.prevCaelChain = caelRecord.fnv1a_chain;
|
|
737
|
+
log({ ev: "cael-posted", taskId: target.id, appended: posted.appended, rejected: posted.rejected });
|
|
738
|
+
} catch (err) {
|
|
739
|
+
log({ ev: "cael-post-error", message: err instanceof Error ? err.message : String(err) });
|
|
740
|
+
}
|
|
741
|
+
if (this.opts.onTaskExecuted) {
|
|
742
|
+
await this.opts.onTaskExecuted(execResult, target);
|
|
743
|
+
} else {
|
|
744
|
+
await mesh.sendMessageOnTask(
|
|
745
|
+
target.id,
|
|
746
|
+
`[${identity.handle}] response (${response.usage.totalTokens} tok, $${cost.costUsd.toFixed(4)}):
|
|
747
|
+
|
|
748
|
+
${response.content}`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
action: "executed",
|
|
753
|
+
taskId: target.id,
|
|
754
|
+
spentUsd: cost.spentUsd,
|
|
755
|
+
remainingUsd: cost.remainingUsd
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
async runForever(opts = {}) {
|
|
759
|
+
const interval = opts.tickIntervalMs ?? 6e4;
|
|
760
|
+
while (!this.stopped) {
|
|
761
|
+
try {
|
|
762
|
+
await this.tick();
|
|
763
|
+
} catch (err) {
|
|
764
|
+
const log = this.opts.logger ?? (() => void 0);
|
|
765
|
+
log({ ev: "tick-error", message: err instanceof Error ? err.message : String(err) });
|
|
766
|
+
}
|
|
767
|
+
await sleep(interval + jitter(interval));
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
stop() {
|
|
771
|
+
this.stopped = true;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Heartbeat with one-shot self-rejoin on 403 "Not a member of this team".
|
|
775
|
+
*
|
|
776
|
+
* Pairs with task_1777112258989_eeyp: fresh-deploy fleet workers whose
|
|
777
|
+
* provisioning didn't atomically call /join (or whose membership was
|
|
778
|
+
* reaped) hit 403 every tick and never recover. We detect the specific
|
|
779
|
+
* server error string (see packages/mcp-server/src/holomesh/routes/
|
|
780
|
+
* team-routes.ts:903 → `{ error: 'Not a member' }` for /presence), call
|
|
781
|
+
* mesh.joinTeam() ONCE per runner process, and retry the heartbeat.
|
|
782
|
+
*
|
|
783
|
+
* Strict scope:
|
|
784
|
+
* - Only retries on 403 + "Not a member" body. Any other 403 (insufficient
|
|
785
|
+
* permissions, signing failure) re-throws unchanged.
|
|
786
|
+
* - Only retries ONCE per process. If we already rejoined this process and
|
|
787
|
+
* the heartbeat is *still* 403, the team is rejecting us for a reason
|
|
788
|
+
* /join can't fix (e.g. capacity, ban) — surface the error.
|
|
789
|
+
* - If joinTeam() itself throws, we DO mark joinedThisProcess=true before
|
|
790
|
+
* re-throwing so we don't slam the join endpoint on every subsequent
|
|
791
|
+
* tick. The next tick will surface the same heartbeat 403 and the
|
|
792
|
+
* runner-level catch in runForever logs tick-error and sleeps. Operator
|
|
793
|
+
* inspection (SSH/log) is the recovery path at that point.
|
|
794
|
+
*/
|
|
795
|
+
async heartbeatWithAutoRejoin() {
|
|
796
|
+
const { identity, mesh, logger } = this.opts;
|
|
797
|
+
const log = logger ?? (() => void 0);
|
|
798
|
+
try {
|
|
799
|
+
await mesh.heartbeat({ agentName: identity.handle, surface: identity.surface });
|
|
800
|
+
} catch (err) {
|
|
801
|
+
if (!this.isNotAMemberError(err) || this.joinedThisProcess) {
|
|
802
|
+
throw err;
|
|
803
|
+
}
|
|
804
|
+
log({ ev: "auto-rejoin-attempt", reason: "heartbeat-403-not-a-member" });
|
|
805
|
+
this.joinedThisProcess = true;
|
|
806
|
+
try {
|
|
807
|
+
const joinResult = await mesh.joinTeam();
|
|
808
|
+
log({ ev: "auto-rejoin-success", role: joinResult.role, members: joinResult.members });
|
|
809
|
+
} catch (joinErr) {
|
|
810
|
+
log({
|
|
811
|
+
ev: "auto-rejoin-failed",
|
|
812
|
+
message: joinErr instanceof Error ? joinErr.message : String(joinErr)
|
|
813
|
+
});
|
|
814
|
+
throw joinErr;
|
|
815
|
+
}
|
|
816
|
+
await mesh.heartbeat({ agentName: identity.handle, surface: identity.surface });
|
|
817
|
+
log({ ev: "auto-rejoin-heartbeat-recovered" });
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Detect the server's "Not a member" 403 error from HolomeshClient.req().
|
|
822
|
+
* The error message format is: `HoloMesh POST /team/<id>/presence 403: <body>`
|
|
823
|
+
* where body contains `{"error":"Not a member"}` (or "Not a member of this team").
|
|
824
|
+
* Match conservatively: BOTH a "403" status marker AND the "Not a member"
|
|
825
|
+
* substring must appear, so unrelated 403s (insufficient permissions,
|
|
826
|
+
* signing failures) do NOT trigger a rejoin.
|
|
827
|
+
*/
|
|
828
|
+
isNotAMemberError(err) {
|
|
829
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
830
|
+
return / 403:/.test(msg) && /Not a member/i.test(msg);
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
function buildTaskPrompt(task) {
|
|
834
|
+
return [
|
|
835
|
+
`Board task to execute: ${task.id}`,
|
|
836
|
+
`Title: ${task.title}`,
|
|
837
|
+
`Priority: ${task.priority}`,
|
|
838
|
+
`Tags: ${(task.tags ?? []).join(", ")}`,
|
|
839
|
+
"",
|
|
840
|
+
"Description:",
|
|
841
|
+
task.description ?? "(no description)",
|
|
842
|
+
"",
|
|
843
|
+
"Produce the deliverable described in the task. Apply your brain composition rules \u2014 anti-patterns, decision loop, and scope tier all bind. Return the response as plain text suitable for posting to /room as a message on this task."
|
|
844
|
+
].join("\n");
|
|
845
|
+
}
|
|
846
|
+
function sleep(ms) {
|
|
847
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
848
|
+
}
|
|
849
|
+
function jitter(base) {
|
|
850
|
+
return Math.floor((Math.random() - 0.5) * base * 0.2);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/commit-hook.ts
|
|
854
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
855
|
+
import { dirname as dirname3, join, resolve as resolve2 } from "path";
|
|
856
|
+
import { spawnSync } from "child_process";
|
|
857
|
+
var SAFE_HANDLE = /^[a-z0-9_-]{1,64}$/i;
|
|
858
|
+
function makeCommitHook(opts) {
|
|
859
|
+
if (!opts.outputDir || opts.outputDir.trim().length === 0) {
|
|
860
|
+
throw new Error("CommitHookOptions.outputDir is required");
|
|
861
|
+
}
|
|
862
|
+
const spawn2 = opts.spawn ?? spawnSync;
|
|
863
|
+
const cwd = opts.workingDir ?? process.cwd();
|
|
864
|
+
const outputDir = resolve2(cwd, opts.outputDir);
|
|
865
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
866
|
+
const scope = opts.scope ?? "agent";
|
|
867
|
+
return async (result, task, identity) => {
|
|
868
|
+
if (!SAFE_HANDLE.test(identity.handle)) {
|
|
869
|
+
throw new Error(`Refusing to commit: handle "${identity.handle}" must match ${SAFE_HANDLE}`);
|
|
870
|
+
}
|
|
871
|
+
const date = now().toISOString().slice(0, 10);
|
|
872
|
+
const safeTaskId = task.id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
|
|
873
|
+
const fileName = `${date}_${safeTaskId}_${identity.handle}.md`;
|
|
874
|
+
const filePath = join(outputDir, fileName);
|
|
875
|
+
mkdirSync2(dirname3(filePath), { recursive: true });
|
|
876
|
+
writeFileSync2(filePath, renderMemo(result, task, identity, date), "utf8");
|
|
877
|
+
const relPath = relativeTo(cwd, filePath);
|
|
878
|
+
const addRes = spawn2("git", ["add", relPath], { cwd, encoding: "utf8" });
|
|
879
|
+
if (addRes.status !== 0) {
|
|
880
|
+
throw new Error(`git add failed: ${addRes.stderr || addRes.stdout || `exit ${addRes.status}`}`);
|
|
881
|
+
}
|
|
882
|
+
const message = renderCommitMessage({ scope, task, identity, result });
|
|
883
|
+
const commitArgs = ["commit", "-m", message];
|
|
884
|
+
if (opts.authorName && opts.authorEmail) {
|
|
885
|
+
commitArgs.push("--author", `${opts.authorName} <${opts.authorEmail}>`);
|
|
886
|
+
}
|
|
887
|
+
const commitRes = spawn2("git", commitArgs, { cwd, encoding: "utf8" });
|
|
888
|
+
if (commitRes.status !== 0) {
|
|
889
|
+
throw new Error(`git commit failed: ${commitRes.stderr || commitRes.stdout || `exit ${commitRes.status}`}`);
|
|
890
|
+
}
|
|
891
|
+
const hashRes = spawn2("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" });
|
|
892
|
+
const commitHash = hashRes.status === 0 ? hashRes.stdout.trim() : void 0;
|
|
893
|
+
return { filePath, commitHash, staged: [relPath], message };
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
function renderMemo(result, task, identity, date) {
|
|
897
|
+
return [
|
|
898
|
+
"---",
|
|
899
|
+
`title: "${task.title.replace(/"/g, "'")}"`,
|
|
900
|
+
`task_id: ${task.id}`,
|
|
901
|
+
`agent: ${identity.handle}`,
|
|
902
|
+
`surface: ${identity.surface}`,
|
|
903
|
+
`provider: ${identity.llmProvider}`,
|
|
904
|
+
`model: ${identity.llmModel}`,
|
|
905
|
+
`wallet: ${identity.wallet}`,
|
|
906
|
+
`date: ${date}`,
|
|
907
|
+
`tokens: ${result.usage.totalTokens}`,
|
|
908
|
+
`cost_usd: ${result.costUsd.toFixed(4)}`,
|
|
909
|
+
`duration_ms: ${result.durationMs}`,
|
|
910
|
+
`tags: [${(task.tags ?? []).map((t) => JSON.stringify(t)).join(", ")}]`,
|
|
911
|
+
"---",
|
|
912
|
+
"",
|
|
913
|
+
`# ${task.title}`,
|
|
914
|
+
"",
|
|
915
|
+
"## Task description",
|
|
916
|
+
"",
|
|
917
|
+
task.description ?? "(no description)",
|
|
918
|
+
"",
|
|
919
|
+
"## Agent response",
|
|
920
|
+
"",
|
|
921
|
+
result.responseText.trim(),
|
|
922
|
+
""
|
|
923
|
+
].join("\n");
|
|
924
|
+
}
|
|
925
|
+
var SUBJECT_MAX = 72;
|
|
926
|
+
function renderCommitMessage(opts) {
|
|
927
|
+
const suffix = ` [agent:${opts.identity.handle}]`;
|
|
928
|
+
const prefix = `${opts.scope}: `;
|
|
929
|
+
const titleBudget = Math.max(8, SUBJECT_MAX - prefix.length - suffix.length);
|
|
930
|
+
const subject = `${prefix}${truncate(opts.task.title, titleBudget)}${suffix}`;
|
|
931
|
+
const body = [
|
|
932
|
+
"",
|
|
933
|
+
`task: ${opts.task.id}`,
|
|
934
|
+
`agent: ${opts.identity.handle} (${opts.identity.llmProvider}/${opts.identity.llmModel})`,
|
|
935
|
+
`wallet: ${opts.identity.wallet}`,
|
|
936
|
+
`cost: $${opts.result.costUsd.toFixed(4)} / ${opts.result.usage.totalTokens} tok / ${opts.result.durationMs}ms`
|
|
937
|
+
].join("\n");
|
|
938
|
+
return `${subject}
|
|
939
|
+
${body}
|
|
940
|
+
`;
|
|
941
|
+
}
|
|
942
|
+
function truncate(s, max) {
|
|
943
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
944
|
+
}
|
|
945
|
+
function relativeTo(base, target) {
|
|
946
|
+
const b = base.replace(/\\/g, "/");
|
|
947
|
+
const t = target.replace(/\\/g, "/");
|
|
948
|
+
if (t.startsWith(b + "/")) return t.slice(b.length + 1);
|
|
949
|
+
if (t === b) return ".";
|
|
950
|
+
return t;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/ablation.ts
|
|
954
|
+
import { createHash as createHash2 } from "crypto";
|
|
955
|
+
async function runAblation(opts) {
|
|
956
|
+
const { task, providers, costGuard } = opts;
|
|
957
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
958
|
+
const promptHash = hashPrompt(task.systemPrompt, task.userPrompt);
|
|
959
|
+
const request = {
|
|
960
|
+
messages: [
|
|
961
|
+
{ role: "system", content: task.systemPrompt },
|
|
962
|
+
{ role: "user", content: task.userPrompt }
|
|
963
|
+
],
|
|
964
|
+
maxTokens: task.maxTokens ?? 2048,
|
|
965
|
+
temperature: task.temperature ?? 0.4
|
|
966
|
+
};
|
|
967
|
+
const cells = [];
|
|
968
|
+
let budgetExhausted = false;
|
|
969
|
+
const matrixId = opts.matrixId ?? `mx_${promptHash}_${Date.now()}`;
|
|
970
|
+
for (const spec of providers) {
|
|
971
|
+
if (costGuard?.isOverBudget()) {
|
|
972
|
+
budgetExhausted = true;
|
|
973
|
+
cells.push({
|
|
974
|
+
label: spec.label,
|
|
975
|
+
provider: spec.provider,
|
|
976
|
+
model: spec.model,
|
|
977
|
+
responseText: "",
|
|
978
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
979
|
+
costUsd: 0,
|
|
980
|
+
durationMs: 0,
|
|
981
|
+
finishReason: "error",
|
|
982
|
+
errorMessage: "budget-exhausted-before-cell"
|
|
983
|
+
});
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
const t0 = Date.now();
|
|
987
|
+
try {
|
|
988
|
+
const provider = await spec.build();
|
|
989
|
+
const cellPromise = provider.complete(request, spec.model);
|
|
990
|
+
const response = opts.timeoutPerCellMs ? await withTimeout(cellPromise, opts.timeoutPerCellMs, spec.label) : await cellPromise;
|
|
991
|
+
const durationMs = Date.now() - t0;
|
|
992
|
+
const costUsd = spec.pricer ? spec.pricer(response.usage) : costGuard?.recordUsage(spec.model, response.usage).costUsd ?? 0;
|
|
993
|
+
if (spec.pricer && costGuard) {
|
|
994
|
+
costGuard.recordUsage(spec.model, response.usage);
|
|
995
|
+
}
|
|
996
|
+
cells.push({
|
|
997
|
+
label: spec.label,
|
|
998
|
+
provider: spec.provider,
|
|
999
|
+
model: spec.model,
|
|
1000
|
+
responseText: response.content,
|
|
1001
|
+
usage: response.usage,
|
|
1002
|
+
costUsd,
|
|
1003
|
+
durationMs,
|
|
1004
|
+
finishReason: response.finishReason
|
|
1005
|
+
});
|
|
1006
|
+
recordAblationCellIfWired(opts, spec, {
|
|
1007
|
+
matrixId,
|
|
1008
|
+
promptHash,
|
|
1009
|
+
promptTokens: response.usage.promptTokens,
|
|
1010
|
+
completionTokens: response.usage.completionTokens,
|
|
1011
|
+
costUsd,
|
|
1012
|
+
durationMs,
|
|
1013
|
+
finishReason: response.finishReason
|
|
1014
|
+
});
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1017
|
+
cells.push({
|
|
1018
|
+
label: spec.label,
|
|
1019
|
+
provider: spec.provider,
|
|
1020
|
+
model: spec.model,
|
|
1021
|
+
responseText: "",
|
|
1022
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
1023
|
+
costUsd: 0,
|
|
1024
|
+
durationMs: Date.now() - t0,
|
|
1025
|
+
finishReason: "error",
|
|
1026
|
+
errorMessage
|
|
1027
|
+
});
|
|
1028
|
+
recordAblationCellIfWired(opts, spec, {
|
|
1029
|
+
matrixId,
|
|
1030
|
+
promptHash,
|
|
1031
|
+
promptTokens: 0,
|
|
1032
|
+
completionTokens: 0,
|
|
1033
|
+
costUsd: 0,
|
|
1034
|
+
durationMs: Date.now() - t0,
|
|
1035
|
+
finishReason: "error",
|
|
1036
|
+
errorMessage
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
taskId: task.taskId,
|
|
1042
|
+
taskTitle: task.taskTitle,
|
|
1043
|
+
brainPath: task.brainPath,
|
|
1044
|
+
promptHash,
|
|
1045
|
+
cells,
|
|
1046
|
+
totalCostUsd: cells.reduce((sum, c) => sum + c.costUsd, 0),
|
|
1047
|
+
startedAt,
|
|
1048
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1049
|
+
budgetExhausted
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function renderAblationMarkdown(matrix) {
|
|
1053
|
+
const header = [
|
|
1054
|
+
`# Ablation: ${matrix.taskTitle}`,
|
|
1055
|
+
"",
|
|
1056
|
+
`- task_id: \`${matrix.taskId}\``,
|
|
1057
|
+
`- prompt_hash: \`${matrix.promptHash}\``,
|
|
1058
|
+
matrix.brainPath ? `- brain: \`${matrix.brainPath}\`` : "- brain: _(none)_",
|
|
1059
|
+
`- started: ${matrix.startedAt}`,
|
|
1060
|
+
`- completed: ${matrix.completedAt}`,
|
|
1061
|
+
`- total_cost_usd: $${matrix.totalCostUsd.toFixed(4)}`,
|
|
1062
|
+
matrix.budgetExhausted ? `- **budget_exhausted: true** (some cells skipped)` : "",
|
|
1063
|
+
"",
|
|
1064
|
+
"| Label | Provider | Model | Tokens (in/out) | Cost (USD) | Duration (ms) | Finish | Excerpt |",
|
|
1065
|
+
"|---|---|---|---|---|---|---|---|"
|
|
1066
|
+
].filter((line) => line !== "");
|
|
1067
|
+
const rows = matrix.cells.map((c) => {
|
|
1068
|
+
const tokens = `${c.usage.promptTokens}/${c.usage.completionTokens}`;
|
|
1069
|
+
const excerpt = c.errorMessage ? `_error: ${truncate2(c.errorMessage, 80)}_` : truncate2(escapeMd(c.responseText.replace(/\n/g, " ")), 80);
|
|
1070
|
+
return `| ${c.label} | ${c.provider} | ${c.model} | ${tokens} | $${c.costUsd.toFixed(4)} | ${c.durationMs} | ${c.finishReason} | ${excerpt} |`;
|
|
1071
|
+
});
|
|
1072
|
+
return [...header, ...rows, ""].join("\n");
|
|
1073
|
+
}
|
|
1074
|
+
function recordAblationCellIfWired(opts, spec, cell) {
|
|
1075
|
+
if (!opts.auditLog) return;
|
|
1076
|
+
const identity = opts.identityFor?.(spec) ?? {
|
|
1077
|
+
handle: `ablation:${spec.label}`,
|
|
1078
|
+
surface: `ablation:${spec.label}`,
|
|
1079
|
+
wallet: "0x0000000000000000000000000000000000000000",
|
|
1080
|
+
x402Bearer: "",
|
|
1081
|
+
llmProvider: spec.provider,
|
|
1082
|
+
llmModel: spec.model,
|
|
1083
|
+
brainPath: opts.task.brainPath ?? "(none)",
|
|
1084
|
+
budgetUsdPerDay: 0,
|
|
1085
|
+
teamId: "(ablation)",
|
|
1086
|
+
meshApiBase: "(ablation)"
|
|
1087
|
+
};
|
|
1088
|
+
try {
|
|
1089
|
+
opts.auditLog.recordAblationCell({
|
|
1090
|
+
identity,
|
|
1091
|
+
matrixId: cell.matrixId,
|
|
1092
|
+
label: spec.label,
|
|
1093
|
+
taskId: opts.task.taskId,
|
|
1094
|
+
taskTitle: opts.task.taskTitle,
|
|
1095
|
+
promptHash: cell.promptHash,
|
|
1096
|
+
promptTokens: cell.promptTokens,
|
|
1097
|
+
completionTokens: cell.completionTokens,
|
|
1098
|
+
costUsd: cell.costUsd,
|
|
1099
|
+
durationMs: cell.durationMs,
|
|
1100
|
+
finishReason: cell.finishReason,
|
|
1101
|
+
errorMessage: cell.errorMessage
|
|
1102
|
+
});
|
|
1103
|
+
} catch {
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function hashPrompt(system, user) {
|
|
1107
|
+
return createHash2("sha256").update(`SYS:${system}
|
|
1108
|
+
USR:${user}`).digest("hex").slice(0, 16);
|
|
1109
|
+
}
|
|
1110
|
+
function withTimeout(p, ms, label) {
|
|
1111
|
+
return new Promise((resolve4, reject) => {
|
|
1112
|
+
const timer = setTimeout(() => reject(new Error(`ablation cell "${label}" timed out after ${ms}ms`)), ms);
|
|
1113
|
+
p.then(
|
|
1114
|
+
(v) => {
|
|
1115
|
+
clearTimeout(timer);
|
|
1116
|
+
resolve4(v);
|
|
1117
|
+
},
|
|
1118
|
+
(e) => {
|
|
1119
|
+
clearTimeout(timer);
|
|
1120
|
+
reject(e);
|
|
1121
|
+
}
|
|
1122
|
+
);
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
function truncate2(s, max) {
|
|
1126
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
1127
|
+
}
|
|
1128
|
+
function escapeMd(s) {
|
|
1129
|
+
return s.replace(/\|/g, "\\|");
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/supervisor.ts
|
|
1133
|
+
import { homedir } from "os";
|
|
1134
|
+
import { join as join2 } from "path";
|
|
1135
|
+
|
|
1136
|
+
// src/audit-log.ts
|
|
1137
|
+
import { mkdirSync as mkdirSync3, appendFileSync, readFileSync as readFileSync2, existsSync as existsSync2, statSync, renameSync } from "fs";
|
|
1138
|
+
import { dirname as dirname4 } from "path";
|
|
1139
|
+
var AuditLog = class {
|
|
1140
|
+
constructor(opts) {
|
|
1141
|
+
this.logPath = opts.logPath;
|
|
1142
|
+
this.maxBytes = opts.maxBytes ?? 50 * 1024 * 1024;
|
|
1143
|
+
mkdirSync3(dirname4(this.logPath), { recursive: true });
|
|
1144
|
+
}
|
|
1145
|
+
record(event) {
|
|
1146
|
+
this.rotateIfFull();
|
|
1147
|
+
appendFileSync(this.logPath, `${JSON.stringify(event)}
|
|
1148
|
+
`, "utf8");
|
|
1149
|
+
}
|
|
1150
|
+
recordTaskExecuted(opts) {
|
|
1151
|
+
this.record({
|
|
1152
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1153
|
+
kind: "task-executed",
|
|
1154
|
+
agent: agentFromIdentity(opts.identity),
|
|
1155
|
+
task: { id: opts.task.id, title: opts.task.title, tags: opts.task.tags ?? [] },
|
|
1156
|
+
execution: {
|
|
1157
|
+
promptTokens: opts.result.usage.promptTokens,
|
|
1158
|
+
completionTokens: opts.result.usage.completionTokens,
|
|
1159
|
+
totalTokens: opts.result.usage.totalTokens,
|
|
1160
|
+
costUsd: opts.result.costUsd,
|
|
1161
|
+
durationMs: opts.result.durationMs
|
|
1162
|
+
},
|
|
1163
|
+
result: {
|
|
1164
|
+
commitHash: opts.commitHash,
|
|
1165
|
+
filePath: opts.filePath
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
recordAblationCell(opts) {
|
|
1170
|
+
this.record({
|
|
1171
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1172
|
+
kind: "ablation-cell",
|
|
1173
|
+
agent: agentFromIdentity(opts.identity),
|
|
1174
|
+
task: { id: opts.taskId, title: opts.taskTitle, tags: ["ablation"] },
|
|
1175
|
+
execution: {
|
|
1176
|
+
promptTokens: opts.promptTokens,
|
|
1177
|
+
completionTokens: opts.completionTokens,
|
|
1178
|
+
totalTokens: opts.promptTokens + opts.completionTokens,
|
|
1179
|
+
costUsd: opts.costUsd,
|
|
1180
|
+
durationMs: opts.durationMs,
|
|
1181
|
+
finishReason: opts.finishReason,
|
|
1182
|
+
promptHash: opts.promptHash
|
|
1183
|
+
},
|
|
1184
|
+
result: { errorMessage: opts.errorMessage },
|
|
1185
|
+
ablation: { label: opts.label, matrixId: opts.matrixId }
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
query(filter = {}) {
|
|
1189
|
+
if (!existsSync2(this.logPath)) return [];
|
|
1190
|
+
const raw = readFileSync2(this.logPath, "utf8");
|
|
1191
|
+
const lines = raw.split("\n").filter((l) => l.length > 0);
|
|
1192
|
+
const events = [];
|
|
1193
|
+
for (const line of lines) {
|
|
1194
|
+
try {
|
|
1195
|
+
events.push(JSON.parse(line));
|
|
1196
|
+
} catch {
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return applyFilter(events, filter);
|
|
1200
|
+
}
|
|
1201
|
+
rollup(filter = {}) {
|
|
1202
|
+
const events = this.query(filter);
|
|
1203
|
+
const byAgent = {};
|
|
1204
|
+
const byProvider = {};
|
|
1205
|
+
let totalCostUsd = 0;
|
|
1206
|
+
let totalTokens = 0;
|
|
1207
|
+
for (const e of events) {
|
|
1208
|
+
const agent = e.agent.handle;
|
|
1209
|
+
const provider = e.agent.provider;
|
|
1210
|
+
const cost = e.execution?.costUsd ?? 0;
|
|
1211
|
+
const tokens = e.execution?.totalTokens ?? 0;
|
|
1212
|
+
byAgent[agent] = byAgent[agent] ?? { events: 0, costUsd: 0, tokens: 0 };
|
|
1213
|
+
byAgent[agent].events += 1;
|
|
1214
|
+
byAgent[agent].costUsd += cost;
|
|
1215
|
+
byAgent[agent].tokens += tokens;
|
|
1216
|
+
byProvider[provider] = byProvider[provider] ?? { events: 0, costUsd: 0, tokens: 0 };
|
|
1217
|
+
byProvider[provider].events += 1;
|
|
1218
|
+
byProvider[provider].costUsd += cost;
|
|
1219
|
+
byProvider[provider].tokens += tokens;
|
|
1220
|
+
totalCostUsd += cost;
|
|
1221
|
+
totalTokens += tokens;
|
|
1222
|
+
}
|
|
1223
|
+
return { totalEvents: events.length, byAgent, byProvider, totalCostUsd, totalTokens };
|
|
1224
|
+
}
|
|
1225
|
+
rotateIfFull() {
|
|
1226
|
+
if (!existsSync2(this.logPath)) return;
|
|
1227
|
+
const size = statSync(this.logPath).size;
|
|
1228
|
+
if (size < this.maxBytes) return;
|
|
1229
|
+
const archived = `${this.logPath}.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.jsonl`;
|
|
1230
|
+
renameSync(this.logPath, archived);
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
function agentFromIdentity(identity) {
|
|
1234
|
+
return {
|
|
1235
|
+
handle: identity.handle,
|
|
1236
|
+
surface: identity.surface,
|
|
1237
|
+
wallet: identity.wallet,
|
|
1238
|
+
walletShort: `${identity.wallet.slice(0, 6)}\u2026${identity.wallet.slice(-4)}`,
|
|
1239
|
+
provider: identity.llmProvider,
|
|
1240
|
+
model: identity.llmModel,
|
|
1241
|
+
brainPath: identity.brainPath
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
function applyFilter(events, filter) {
|
|
1245
|
+
let result = events;
|
|
1246
|
+
if (filter.agent) result = result.filter((e) => e.agent.handle === filter.agent);
|
|
1247
|
+
if (filter.provider) result = result.filter((e) => e.agent.provider === filter.provider);
|
|
1248
|
+
if (filter.task) result = result.filter((e) => e.task?.id === filter.task);
|
|
1249
|
+
if (filter.kind) result = result.filter((e) => e.kind === filter.kind);
|
|
1250
|
+
if (filter.since) result = result.filter((e) => e.ts >= filter.since);
|
|
1251
|
+
if (filter.until) result = result.filter((e) => e.ts <= filter.until);
|
|
1252
|
+
if (filter.limit != null) result = result.slice(-filter.limit);
|
|
1253
|
+
return result;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/supervisor.ts
|
|
1257
|
+
var Supervisor = class {
|
|
1258
|
+
constructor(opts) {
|
|
1259
|
+
this.agents = /* @__PURE__ */ new Map();
|
|
1260
|
+
this.stopped = false;
|
|
1261
|
+
this.opts = opts;
|
|
1262
|
+
this.globalBudgetUsdPerDay = opts.config.globalBudgetUsdPerDay;
|
|
1263
|
+
this.auditLog = opts.auditLogPath ? new AuditLog({ logPath: opts.auditLogPath }) : void 0;
|
|
1264
|
+
}
|
|
1265
|
+
async start() {
|
|
1266
|
+
const enabledAgents = this.opts.config.agents.filter((a) => a.enabled !== false);
|
|
1267
|
+
for (const spec of enabledAgents) {
|
|
1268
|
+
const managed = await this.bootAgent(spec);
|
|
1269
|
+
this.agents.set(spec.handle, managed);
|
|
1270
|
+
}
|
|
1271
|
+
this.log({ ev: "supervisor-started", count: enabledAgents.length });
|
|
1272
|
+
for (const managed of this.agents.values()) {
|
|
1273
|
+
this.spawnLoop(managed);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
async stop() {
|
|
1277
|
+
this.stopped = true;
|
|
1278
|
+
for (const managed of this.agents.values()) {
|
|
1279
|
+
managed.runner.stop();
|
|
1280
|
+
managed.status.state = "stopped";
|
|
1281
|
+
}
|
|
1282
|
+
this.log({ ev: "supervisor-stopped", count: this.agents.size });
|
|
1283
|
+
}
|
|
1284
|
+
status() {
|
|
1285
|
+
const agents = [...this.agents.values()].map((m) => ({ ...m.status }));
|
|
1286
|
+
const globalSpentUsd = agents.reduce((s, a) => s + a.spentUsd, 0);
|
|
1287
|
+
const globalBudgetExhausted = this.globalBudgetUsdPerDay != null ? globalSpentUsd >= this.globalBudgetUsdPerDay : false;
|
|
1288
|
+
const globalRemainingUsd = this.globalBudgetUsdPerDay != null ? Math.max(0, this.globalBudgetUsdPerDay - globalSpentUsd) : Infinity;
|
|
1289
|
+
return { globalSpentUsd, globalRemainingUsd, globalBudgetExhausted, agents };
|
|
1290
|
+
}
|
|
1291
|
+
async tickOnce(handle) {
|
|
1292
|
+
const managed = this.agents.get(handle);
|
|
1293
|
+
if (!managed) throw new Error(`No agent: ${handle}`);
|
|
1294
|
+
await this.runOneTick(managed);
|
|
1295
|
+
return { ...managed.status };
|
|
1296
|
+
}
|
|
1297
|
+
async bootAgent(spec) {
|
|
1298
|
+
const identity = this.identityFromSpec(spec);
|
|
1299
|
+
const brain = await loadBrain(spec.brainPath, spec.scopeTier ?? "warm");
|
|
1300
|
+
const provider = await this.opts.providerFactory(spec, identity);
|
|
1301
|
+
const stateDir = this.opts.stateDir ?? join2(homedir(), ".holoscript-agent", "cost-state");
|
|
1302
|
+
const isFree = spec.provider === "mock" || spec.provider === "local-llm" || spec.provider === "bitnet";
|
|
1303
|
+
const costGuard = new CostGuard({
|
|
1304
|
+
statePath: join2(stateDir, `${spec.handle}.json`),
|
|
1305
|
+
dailyBudgetUsd: identity.budgetUsdPerDay,
|
|
1306
|
+
pricer: isFree ? () => 0 : void 0
|
|
1307
|
+
});
|
|
1308
|
+
const mesh = new HolomeshClient({
|
|
1309
|
+
apiBase: identity.meshApiBase,
|
|
1310
|
+
bearer: identity.x402Bearer,
|
|
1311
|
+
teamId: identity.teamId,
|
|
1312
|
+
fetchImpl: this.opts.fetchImpl
|
|
1313
|
+
});
|
|
1314
|
+
const onTaskExecuted = spec.enableCommitHook ? this.buildCommitHook(spec, identity, mesh) : void 0;
|
|
1315
|
+
const runner = new AgentRunner({
|
|
1316
|
+
identity,
|
|
1317
|
+
brain,
|
|
1318
|
+
provider,
|
|
1319
|
+
costGuard,
|
|
1320
|
+
mesh,
|
|
1321
|
+
onTaskExecuted,
|
|
1322
|
+
auditLog: this.auditLog,
|
|
1323
|
+
logger: (ev) => this.log({ agent: spec.handle, ...ev })
|
|
1324
|
+
});
|
|
1325
|
+
const status = {
|
|
1326
|
+
handle: spec.handle,
|
|
1327
|
+
state: "starting",
|
|
1328
|
+
spentUsd: 0,
|
|
1329
|
+
remainingUsd: identity.budgetUsdPerDay,
|
|
1330
|
+
restarts: 0
|
|
1331
|
+
};
|
|
1332
|
+
return { spec, identity, brain, runner, costGuard, status };
|
|
1333
|
+
}
|
|
1334
|
+
buildCommitHook(spec, identity, mesh) {
|
|
1335
|
+
const writer = makeCommitHook({
|
|
1336
|
+
outputDir: spec.outputDir ?? "agent-out",
|
|
1337
|
+
workingDir: spec.workingDir,
|
|
1338
|
+
scope: `agent(${spec.handle})`
|
|
1339
|
+
});
|
|
1340
|
+
return async (result, task) => {
|
|
1341
|
+
const out = await writer(result, task, identity);
|
|
1342
|
+
if (out.commitHash) {
|
|
1343
|
+
await mesh.markDone(task.id, `auto: ${task.title}`, out.commitHash);
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
identityFromSpec(spec) {
|
|
1348
|
+
const bearer = process.env[spec.bearerEnvKey];
|
|
1349
|
+
if (!bearer || bearer.trim().length === 0) {
|
|
1350
|
+
throw new Error(`Missing bearer env var "${spec.bearerEnvKey}" for agent "${spec.handle}"`);
|
|
1351
|
+
}
|
|
1352
|
+
const wallet = process.env[spec.walletEnvKey];
|
|
1353
|
+
if (!wallet || !/^0x[0-9a-fA-F]{40}$/.test(wallet)) {
|
|
1354
|
+
throw new Error(`Missing or malformed wallet env var "${spec.walletEnvKey}" for agent "${spec.handle}"`);
|
|
1355
|
+
}
|
|
1356
|
+
return {
|
|
1357
|
+
handle: spec.handle,
|
|
1358
|
+
surface: spec.handle,
|
|
1359
|
+
wallet,
|
|
1360
|
+
x402Bearer: bearer,
|
|
1361
|
+
llmProvider: spec.provider,
|
|
1362
|
+
llmModel: spec.model,
|
|
1363
|
+
brainPath: spec.brainPath,
|
|
1364
|
+
budgetUsdPerDay: spec.budgetUsdPerDay ?? 5,
|
|
1365
|
+
teamId: this.opts.teamId,
|
|
1366
|
+
meshApiBase: this.opts.meshApiBase ?? "https://mcp.holoscript.net/api/holomesh"
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
async spawnLoop(managed) {
|
|
1370
|
+
const interval = managed.spec.tickIntervalMs ?? this.opts.config.defaultTickIntervalMs ?? 6e4;
|
|
1371
|
+
while (!this.stopped) {
|
|
1372
|
+
try {
|
|
1373
|
+
await this.runOneTick(managed);
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
managed.status.state = "crashed";
|
|
1376
|
+
managed.status.lastError = err instanceof Error ? err.message : String(err);
|
|
1377
|
+
managed.status.restarts += 1;
|
|
1378
|
+
const backoffMs = Math.min(6e4, 2 ** Math.min(managed.status.restarts, 6) * 1e3);
|
|
1379
|
+
this.log({
|
|
1380
|
+
ev: "agent-crashed-restarting",
|
|
1381
|
+
agent: managed.spec.handle,
|
|
1382
|
+
backoffMs,
|
|
1383
|
+
restarts: managed.status.restarts,
|
|
1384
|
+
message: managed.status.lastError
|
|
1385
|
+
});
|
|
1386
|
+
await sleep2(backoffMs);
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
await sleep2(interval + jitter2(interval));
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
async runOneTick(managed) {
|
|
1393
|
+
if (this.globalBudgetUsdPerDay != null) {
|
|
1394
|
+
const totalSpent = [...this.agents.values()].reduce((s, a) => s + a.status.spentUsd, 0);
|
|
1395
|
+
if (totalSpent >= this.globalBudgetUsdPerDay) {
|
|
1396
|
+
managed.status.state = "over-budget";
|
|
1397
|
+
this.log({ ev: "global-budget-exhausted", agent: managed.spec.handle, totalSpent });
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
const result = await managed.runner.tick();
|
|
1402
|
+
const cs = managed.costGuard.getState();
|
|
1403
|
+
managed.status.spentUsd = cs.spentUsd;
|
|
1404
|
+
managed.status.remainingUsd = managed.costGuard.getRemainingUsd();
|
|
1405
|
+
managed.status.lastTickAt = (this.opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
1406
|
+
managed.status.state = result.action === "over-budget" ? "over-budget" : "running";
|
|
1407
|
+
if (result.action === "errored") {
|
|
1408
|
+
managed.status.lastError = result.message;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
log(event) {
|
|
1412
|
+
if (this.opts.logger) {
|
|
1413
|
+
this.opts.logger({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...event });
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
function sleep2(ms) {
|
|
1418
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1419
|
+
}
|
|
1420
|
+
function jitter2(base) {
|
|
1421
|
+
return Math.floor((Math.random() - 0.5) * base * 0.2);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// src/supervisor-config.ts
|
|
1425
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1426
|
+
var VALID_PROVIDERS2 = /* @__PURE__ */ new Set([
|
|
1427
|
+
"anthropic",
|
|
1428
|
+
"openai",
|
|
1429
|
+
"gemini",
|
|
1430
|
+
"mock",
|
|
1431
|
+
"bitnet",
|
|
1432
|
+
"local-llm"
|
|
1433
|
+
]);
|
|
1434
|
+
var VALID_TIERS = /* @__PURE__ */ new Set(["cold", "warm", "hot"]);
|
|
1435
|
+
var HANDLE_PATTERN = /^[a-z0-9_-]{1,64}$/i;
|
|
1436
|
+
function loadSupervisorConfig(path) {
|
|
1437
|
+
return parseSupervisorConfig(readFileSync3(path, "utf8"));
|
|
1438
|
+
}
|
|
1439
|
+
function parseSupervisorConfig(raw) {
|
|
1440
|
+
const data = JSON.parse(raw);
|
|
1441
|
+
if (!isObject(data)) throw new Error("Supervisor config must be a JSON object");
|
|
1442
|
+
if (!Array.isArray(data.agents)) throw new Error("Supervisor config.agents must be an array");
|
|
1443
|
+
if (data.agents.length === 0) throw new Error("Supervisor config.agents must have at least one entry");
|
|
1444
|
+
const seenHandles = /* @__PURE__ */ new Set();
|
|
1445
|
+
const agents = data.agents.map((entry, idx) => validateAgent(entry, idx, seenHandles));
|
|
1446
|
+
const globalBudgetUsdPerDay = optionalNumber(data, "globalBudgetUsdPerDay");
|
|
1447
|
+
const defaultTickIntervalMs = optionalNumber(data, "defaultTickIntervalMs");
|
|
1448
|
+
if (globalBudgetUsdPerDay != null && globalBudgetUsdPerDay <= 0) {
|
|
1449
|
+
throw new Error(`globalBudgetUsdPerDay must be positive, got ${globalBudgetUsdPerDay}`);
|
|
1450
|
+
}
|
|
1451
|
+
if (defaultTickIntervalMs != null && defaultTickIntervalMs < 5e3) {
|
|
1452
|
+
throw new Error(`defaultTickIntervalMs must be >= 5000ms (mesh-friendly), got ${defaultTickIntervalMs}`);
|
|
1453
|
+
}
|
|
1454
|
+
return { agents, globalBudgetUsdPerDay, defaultTickIntervalMs };
|
|
1455
|
+
}
|
|
1456
|
+
function validateAgent(entry, idx, seen) {
|
|
1457
|
+
if (!isObject(entry)) throw new Error(`agents[${idx}] must be an object`);
|
|
1458
|
+
const handle = requiredString(entry, "handle", `agents[${idx}].handle`);
|
|
1459
|
+
if (!HANDLE_PATTERN.test(handle)) {
|
|
1460
|
+
throw new Error(`agents[${idx}].handle "${handle}" must match ${HANDLE_PATTERN}`);
|
|
1461
|
+
}
|
|
1462
|
+
if (seen.has(handle)) throw new Error(`Duplicate agent handle: "${handle}"`);
|
|
1463
|
+
seen.add(handle);
|
|
1464
|
+
const provider = requiredString(entry, "provider", `agents[${idx}].provider`);
|
|
1465
|
+
if (!VALID_PROVIDERS2.has(provider)) {
|
|
1466
|
+
throw new Error(
|
|
1467
|
+
`agents[${idx}].provider "${provider}" not in [${[...VALID_PROVIDERS2].join(", ")}]`
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
const scopeTier = optionalString(entry, "scopeTier");
|
|
1471
|
+
if (scopeTier && !VALID_TIERS.has(scopeTier)) {
|
|
1472
|
+
throw new Error(`agents[${idx}].scopeTier "${scopeTier}" must be cold | warm | hot`);
|
|
1473
|
+
}
|
|
1474
|
+
const budgetUsdPerDay = optionalNumber(entry, "budgetUsdPerDay");
|
|
1475
|
+
if (budgetUsdPerDay != null && budgetUsdPerDay <= 0) {
|
|
1476
|
+
throw new Error(`agents[${idx}].budgetUsdPerDay must be positive, got ${budgetUsdPerDay}`);
|
|
1477
|
+
}
|
|
1478
|
+
const tickIntervalMs = optionalNumber(entry, "tickIntervalMs");
|
|
1479
|
+
if (tickIntervalMs != null && tickIntervalMs < 5e3) {
|
|
1480
|
+
throw new Error(`agents[${idx}].tickIntervalMs must be >= 5000ms, got ${tickIntervalMs}`);
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
handle,
|
|
1484
|
+
brainPath: requiredString(entry, "brainPath", `agents[${idx}].brainPath`),
|
|
1485
|
+
provider,
|
|
1486
|
+
model: requiredString(entry, "model", `agents[${idx}].model`),
|
|
1487
|
+
walletEnvKey: requiredString(entry, "walletEnvKey", `agents[${idx}].walletEnvKey`),
|
|
1488
|
+
bearerEnvKey: requiredString(entry, "bearerEnvKey", `agents[${idx}].bearerEnvKey`),
|
|
1489
|
+
budgetUsdPerDay,
|
|
1490
|
+
scopeTier,
|
|
1491
|
+
enabled: optionalBoolean(entry, "enabled"),
|
|
1492
|
+
tickIntervalMs,
|
|
1493
|
+
enableCommitHook: optionalBoolean(entry, "enableCommitHook"),
|
|
1494
|
+
outputDir: optionalString(entry, "outputDir"),
|
|
1495
|
+
workingDir: optionalString(entry, "workingDir")
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
function isObject(v) {
|
|
1499
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1500
|
+
}
|
|
1501
|
+
function requiredString(obj, key, label) {
|
|
1502
|
+
const v = obj[key];
|
|
1503
|
+
if (typeof v !== "string" || v.trim().length === 0) {
|
|
1504
|
+
throw new Error(`${label} is required and must be a non-empty string`);
|
|
1505
|
+
}
|
|
1506
|
+
return v.trim();
|
|
1507
|
+
}
|
|
1508
|
+
function optionalString(obj, key) {
|
|
1509
|
+
const v = obj[key];
|
|
1510
|
+
if (v === void 0 || v === null) return void 0;
|
|
1511
|
+
if (typeof v !== "string") throw new Error(`${key} must be a string when present`);
|
|
1512
|
+
return v.trim();
|
|
1513
|
+
}
|
|
1514
|
+
function optionalNumber(obj, key) {
|
|
1515
|
+
const v = obj[key];
|
|
1516
|
+
if (v === void 0 || v === null) return void 0;
|
|
1517
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1518
|
+
throw new Error(`${key} must be a finite number when present`);
|
|
1519
|
+
}
|
|
1520
|
+
return v;
|
|
1521
|
+
}
|
|
1522
|
+
function optionalBoolean(obj, key) {
|
|
1523
|
+
const v = obj[key];
|
|
1524
|
+
if (v === void 0 || v === null) return void 0;
|
|
1525
|
+
if (typeof v !== "boolean") throw new Error(`${key} must be a boolean when present`);
|
|
1526
|
+
return v;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// src/provision.ts
|
|
1530
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync3, chmodSync } from "fs";
|
|
1531
|
+
import { join as join3 } from "path";
|
|
1532
|
+
import { homedir as homedir2, hostname } from "os";
|
|
1533
|
+
import { randomBytes, createCipheriv, createHash as createHash3 } from "crypto";
|
|
1534
|
+
import { Wallet } from "ethers";
|
|
1535
|
+
var HANDLE_PATTERN2 = /^[a-z0-9_-]{1,64}$/i;
|
|
1536
|
+
var EIP712_DOMAIN = { name: "HoloMesh", version: "1" };
|
|
1537
|
+
var EIP712_TYPES = {
|
|
1538
|
+
Registration: [{ name: "nonce", type: "string" }]
|
|
1539
|
+
};
|
|
1540
|
+
async function provisionAgent(req, opts = { execute: false }) {
|
|
1541
|
+
if (!HANDLE_PATTERN2.test(req.handle)) {
|
|
1542
|
+
throw new Error(`handle "${req.handle}" must match ${HANDLE_PATTERN2}`);
|
|
1543
|
+
}
|
|
1544
|
+
if (!req.founderBearer || req.founderBearer.trim().length === 0) {
|
|
1545
|
+
throw new Error("founderBearer is required (HOLOMESH_API_KEY of an agent that can call /register)");
|
|
1546
|
+
}
|
|
1547
|
+
const meshApiBase = (req.meshApiBase ?? "https://mcp.holoscript.net/api/holomesh").replace(/\/$/, "");
|
|
1548
|
+
const seatsRoot = req.seatsRoot ?? defaultSeatsRoot();
|
|
1549
|
+
const surface = req.handle;
|
|
1550
|
+
const seatId = makeSeatId(surface);
|
|
1551
|
+
const seatDir = join3(seatsRoot, seatId);
|
|
1552
|
+
const walletPath = join3(seatDir, "wallet.enc");
|
|
1553
|
+
const regPath = join3(seatDir, "registration.json");
|
|
1554
|
+
if (!opts.execute) {
|
|
1555
|
+
return {
|
|
1556
|
+
status: "dry-run",
|
|
1557
|
+
handle: req.handle,
|
|
1558
|
+
surface,
|
|
1559
|
+
seatId,
|
|
1560
|
+
seatDir,
|
|
1561
|
+
willGenerateWallet: !existsSync3(walletPath),
|
|
1562
|
+
willCallEndpoints: [
|
|
1563
|
+
`POST ${meshApiBase}/register/challenge`,
|
|
1564
|
+
`POST ${meshApiBase}/register`
|
|
1565
|
+
]
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
if (existsSync3(walletPath) && !opts.force) {
|
|
1569
|
+
const blob = JSON.parse(readFileSync4(walletPath, "utf8"));
|
|
1570
|
+
const reused = {
|
|
1571
|
+
status: "reused",
|
|
1572
|
+
handle: req.handle,
|
|
1573
|
+
surface,
|
|
1574
|
+
seatId,
|
|
1575
|
+
seatDir,
|
|
1576
|
+
walletAddress: blob.address,
|
|
1577
|
+
envVarLines: envVarLinesFor(req.handle, blob.address, void 0)
|
|
1578
|
+
};
|
|
1579
|
+
return reused;
|
|
1580
|
+
}
|
|
1581
|
+
const wallet = Wallet.createRandom();
|
|
1582
|
+
mkdirSync4(seatDir, { recursive: true });
|
|
1583
|
+
const masterKey = ensureMasterKey(seatsRoot);
|
|
1584
|
+
const encryptedBlob = {
|
|
1585
|
+
seat_id: seatId,
|
|
1586
|
+
surface,
|
|
1587
|
+
handle: req.handle,
|
|
1588
|
+
address: wallet.address,
|
|
1589
|
+
encrypted_privkey: encryptPrivateKey(wallet.privateKey, masterKey),
|
|
1590
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1591
|
+
source: "holoscript-agent.provision"
|
|
1592
|
+
};
|
|
1593
|
+
writeFileSync3(walletPath, JSON.stringify(encryptedBlob, null, 2), "utf8");
|
|
1594
|
+
try {
|
|
1595
|
+
chmodSync(walletPath, 384);
|
|
1596
|
+
} catch {
|
|
1597
|
+
}
|
|
1598
|
+
const fetchImpl = req.fetchImpl ?? fetch;
|
|
1599
|
+
const challenge = await postJson(
|
|
1600
|
+
fetchImpl,
|
|
1601
|
+
`${meshApiBase}/register/challenge`,
|
|
1602
|
+
req.founderBearer,
|
|
1603
|
+
{ wallet_address: wallet.address }
|
|
1604
|
+
);
|
|
1605
|
+
if (!challenge.nonce) {
|
|
1606
|
+
throw new Error(`/register/challenge returned no nonce: ${JSON.stringify(challenge)}`);
|
|
1607
|
+
}
|
|
1608
|
+
const signature = await wallet.signTypedData(EIP712_DOMAIN, EIP712_TYPES, { nonce: challenge.nonce });
|
|
1609
|
+
const registration = await postJson(
|
|
1610
|
+
fetchImpl,
|
|
1611
|
+
`${meshApiBase}/register`,
|
|
1612
|
+
req.founderBearer,
|
|
1613
|
+
{
|
|
1614
|
+
name: req.handle,
|
|
1615
|
+
wallet_address: wallet.address,
|
|
1616
|
+
nonce: challenge.nonce,
|
|
1617
|
+
signature
|
|
1618
|
+
}
|
|
1619
|
+
);
|
|
1620
|
+
writeFileSync3(
|
|
1621
|
+
regPath,
|
|
1622
|
+
JSON.stringify({ status: 201, response: registration, registered_at: (/* @__PURE__ */ new Date()).toISOString(), flow: "x402" }, null, 2),
|
|
1623
|
+
"utf8"
|
|
1624
|
+
);
|
|
1625
|
+
const agentId = registration.agent?.id;
|
|
1626
|
+
const bearer = registration.agent?.api_key;
|
|
1627
|
+
if (!agentId || !bearer) {
|
|
1628
|
+
throw new Error(`/register did not return agent.id + agent.api_key: ${JSON.stringify(registration).slice(0, 400)}`);
|
|
1629
|
+
}
|
|
1630
|
+
if (registration.wallet?.private_key) {
|
|
1631
|
+
console.warn("[provision] WARN \u2014 server returned private_key despite x402 flow; ignoring (using local key).");
|
|
1632
|
+
}
|
|
1633
|
+
let joinedTeam;
|
|
1634
|
+
if (req.autoJoinTeamId) {
|
|
1635
|
+
try {
|
|
1636
|
+
const joinRes = await postJson(
|
|
1637
|
+
fetchImpl,
|
|
1638
|
+
`${meshApiBase}/team/${req.autoJoinTeamId}/join`,
|
|
1639
|
+
bearer,
|
|
1640
|
+
{}
|
|
1641
|
+
);
|
|
1642
|
+
joinedTeam = {
|
|
1643
|
+
teamId: req.autoJoinTeamId,
|
|
1644
|
+
role: joinRes.role ?? "member",
|
|
1645
|
+
members: joinRes.members ?? 0
|
|
1646
|
+
};
|
|
1647
|
+
} catch (err) {
|
|
1648
|
+
joinedTeam = {
|
|
1649
|
+
teamId: req.autoJoinTeamId,
|
|
1650
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
status: "executed",
|
|
1656
|
+
handle: req.handle,
|
|
1657
|
+
surface,
|
|
1658
|
+
seatId,
|
|
1659
|
+
seatDir,
|
|
1660
|
+
walletAddress: wallet.address,
|
|
1661
|
+
bearer,
|
|
1662
|
+
agentId,
|
|
1663
|
+
envVarLines: envVarLinesFor(req.handle, wallet.address, bearer),
|
|
1664
|
+
joinedTeam
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function defaultSeatsRoot() {
|
|
1668
|
+
return process.env.HOLOSCRIPT_AGENT_SEATS_ROOT ?? join3(homedir2(), ".holoscript-agent", "seats");
|
|
1669
|
+
}
|
|
1670
|
+
function makeSeatId(surface) {
|
|
1671
|
+
const fp = createHash3("sha256").update(hostname() + homedir2()).digest("hex").slice(0, 8);
|
|
1672
|
+
return `holoscript-${surface}-${fp}-x402`;
|
|
1673
|
+
}
|
|
1674
|
+
function ensureMasterKey(seatsRoot) {
|
|
1675
|
+
const keyPath = join3(seatsRoot, ".master-key");
|
|
1676
|
+
if (!existsSync3(seatsRoot)) mkdirSync4(seatsRoot, { recursive: true });
|
|
1677
|
+
if (!existsSync3(keyPath)) {
|
|
1678
|
+
const k = randomBytes(32);
|
|
1679
|
+
writeFileSync3(keyPath, k);
|
|
1680
|
+
try {
|
|
1681
|
+
chmodSync(keyPath, 384);
|
|
1682
|
+
} catch {
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return readFileSync4(keyPath);
|
|
1686
|
+
}
|
|
1687
|
+
function encryptPrivateKey(privKey, masterKey) {
|
|
1688
|
+
const iv = randomBytes(12);
|
|
1689
|
+
const cipher = createCipheriv("aes-256-gcm", masterKey, iv);
|
|
1690
|
+
const ct = Buffer.concat([cipher.update(privKey, "utf8"), cipher.final()]);
|
|
1691
|
+
return { iv: iv.toString("base64"), ct: ct.toString("base64"), tag: cipher.getAuthTag().toString("base64"), alg: "aes-256-gcm" };
|
|
1692
|
+
}
|
|
1693
|
+
async function postJson(fetchImpl, url, bearer, body) {
|
|
1694
|
+
const res = await fetchImpl(url, {
|
|
1695
|
+
method: "POST",
|
|
1696
|
+
headers: {
|
|
1697
|
+
Authorization: `Bearer ${bearer}`,
|
|
1698
|
+
"Content-Type": "application/json"
|
|
1699
|
+
},
|
|
1700
|
+
body: JSON.stringify(body)
|
|
1701
|
+
});
|
|
1702
|
+
const text = await res.text();
|
|
1703
|
+
if (!res.ok) {
|
|
1704
|
+
throw new Error(`POST ${url} ${res.status}: ${text.slice(0, 400)}`);
|
|
1705
|
+
}
|
|
1706
|
+
try {
|
|
1707
|
+
return JSON.parse(text);
|
|
1708
|
+
} catch {
|
|
1709
|
+
throw new Error(`POST ${url} returned non-JSON: ${text.slice(0, 200)}`);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
function envVarLinesFor(handle, walletAddress, bearer) {
|
|
1713
|
+
const suffix = handle.toUpperCase().replace(/-/g, "_");
|
|
1714
|
+
const lines = [`HOLOSCRIPT_AGENT_WALLET_${suffix}=${walletAddress}`];
|
|
1715
|
+
if (bearer) {
|
|
1716
|
+
lines.push(`HOLOMESH_API_KEY_${suffix}_X402=${bearer}`);
|
|
1717
|
+
}
|
|
1718
|
+
return lines;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// src/index.ts
|
|
1722
|
+
import { writeFileSync as writeFileSync4, readFileSync as readFileSync5, existsSync as existsSync4, mkdirSync as mkdirSync5 } from "fs";
|
|
1723
|
+
import { dirname as dirname5, resolve as resolve3 } from "path";
|
|
1724
|
+
async function main() {
|
|
1725
|
+
const args = process.argv.slice(2);
|
|
1726
|
+
const cmd = args[0] ?? "help";
|
|
1727
|
+
switch (cmd) {
|
|
1728
|
+
case "run":
|
|
1729
|
+
await cmdRun({ once: false });
|
|
1730
|
+
return;
|
|
1731
|
+
case "tick":
|
|
1732
|
+
await cmdRun({ once: true });
|
|
1733
|
+
return;
|
|
1734
|
+
case "whoami":
|
|
1735
|
+
await cmdWhoami();
|
|
1736
|
+
return;
|
|
1737
|
+
case "ablate":
|
|
1738
|
+
await cmdAblate(args.slice(1));
|
|
1739
|
+
return;
|
|
1740
|
+
case "supervise":
|
|
1741
|
+
await cmdSupervise(args.slice(1));
|
|
1742
|
+
return;
|
|
1743
|
+
case "status":
|
|
1744
|
+
await cmdStatus(args.slice(1));
|
|
1745
|
+
return;
|
|
1746
|
+
case "provision":
|
|
1747
|
+
await cmdProvision(args.slice(1));
|
|
1748
|
+
return;
|
|
1749
|
+
case "audit":
|
|
1750
|
+
await cmdAudit(args.slice(1));
|
|
1751
|
+
return;
|
|
1752
|
+
case "help":
|
|
1753
|
+
case "--help":
|
|
1754
|
+
case "-h":
|
|
1755
|
+
printHelp();
|
|
1756
|
+
return;
|
|
1757
|
+
default:
|
|
1758
|
+
console.error(`Unknown command: ${cmd}`);
|
|
1759
|
+
printHelp();
|
|
1760
|
+
process.exit(2);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
async function cmdRun(opts) {
|
|
1764
|
+
const identity = loadIdentity();
|
|
1765
|
+
const brain = await loadBrain(identity.brainPath, scopeTierFromEnv());
|
|
1766
|
+
const provider = await buildProvider(identity);
|
|
1767
|
+
const costGuard = new CostGuard({
|
|
1768
|
+
statePath: stateFilePath(identity),
|
|
1769
|
+
dailyBudgetUsd: identity.budgetUsdPerDay,
|
|
1770
|
+
pricer: defaultPricerForProvider(identity.llmProvider)
|
|
1771
|
+
});
|
|
1772
|
+
const mesh = new HolomeshClient({
|
|
1773
|
+
apiBase: identity.meshApiBase,
|
|
1774
|
+
bearer: identity.x402Bearer,
|
|
1775
|
+
teamId: identity.teamId
|
|
1776
|
+
});
|
|
1777
|
+
const commitHook = buildCommitHook(identity, mesh);
|
|
1778
|
+
const auditLog = buildAuditLog();
|
|
1779
|
+
const runner = new AgentRunner({
|
|
1780
|
+
identity,
|
|
1781
|
+
brain,
|
|
1782
|
+
provider,
|
|
1783
|
+
costGuard,
|
|
1784
|
+
mesh,
|
|
1785
|
+
logger: (ev) => console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...ev })),
|
|
1786
|
+
onTaskExecuted: commitHook,
|
|
1787
|
+
auditLog
|
|
1788
|
+
});
|
|
1789
|
+
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ev: "boot", identity: identityForLog(identity), brain: { domain: brain.domain, tags: brain.capabilityTags, tier: brain.scopeTier } }));
|
|
1790
|
+
if (opts.once) {
|
|
1791
|
+
const result = await runner.tick();
|
|
1792
|
+
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ev: "tick-result", ...result }));
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
const interval = Number(process.env.HOLOSCRIPT_AGENT_TICK_MS ?? "60000");
|
|
1796
|
+
const onSig = () => {
|
|
1797
|
+
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ev: "shutdown" }));
|
|
1798
|
+
runner.stop();
|
|
1799
|
+
setTimeout(() => process.exit(0), 250);
|
|
1800
|
+
};
|
|
1801
|
+
process.on("SIGINT", onSig);
|
|
1802
|
+
process.on("SIGTERM", onSig);
|
|
1803
|
+
await runner.runForever({ tickIntervalMs: interval });
|
|
1804
|
+
}
|
|
1805
|
+
function supervisorProviderFactory() {
|
|
1806
|
+
return (spec) => {
|
|
1807
|
+
switch (spec.provider) {
|
|
1808
|
+
case "anthropic":
|
|
1809
|
+
return createAnthropicProvider({ defaultModel: spec.model });
|
|
1810
|
+
case "openai":
|
|
1811
|
+
return createOpenAIProvider({ defaultModel: spec.model });
|
|
1812
|
+
case "gemini":
|
|
1813
|
+
return createGeminiProvider({ defaultModel: spec.model });
|
|
1814
|
+
case "local-llm":
|
|
1815
|
+
return createLocalLLMProvider({
|
|
1816
|
+
baseURL: process.env.HOLOSCRIPT_AGENT_LOCAL_LLM_BASE_URL,
|
|
1817
|
+
model: spec.model
|
|
1818
|
+
});
|
|
1819
|
+
case "mock":
|
|
1820
|
+
return createMockProvider();
|
|
1821
|
+
default:
|
|
1822
|
+
throw new Error(`Provider "${spec.provider}" not yet wired in supervisor \u2014 Phase 2.5 deliverable.`);
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
async function cmdSupervise(rest) {
|
|
1827
|
+
const cfgPath = rest.find((a) => a.startsWith("--config="))?.split("=")[1];
|
|
1828
|
+
if (!cfgPath) {
|
|
1829
|
+
throw new Error("Usage: holoscript-agent supervise --config=<path-to-agents.json>");
|
|
1830
|
+
}
|
|
1831
|
+
const teamId = process.env.HOLOMESH_TEAM_ID;
|
|
1832
|
+
if (!teamId) throw new Error("HOLOMESH_TEAM_ID env var required for supervise command");
|
|
1833
|
+
const config = loadSupervisorConfig(cfgPath);
|
|
1834
|
+
const sup = new Supervisor({
|
|
1835
|
+
config,
|
|
1836
|
+
providerFactory: supervisorProviderFactory(),
|
|
1837
|
+
teamId,
|
|
1838
|
+
meshApiBase: process.env.HOLOMESH_API_BASE,
|
|
1839
|
+
auditLogPath: auditLogPath(),
|
|
1840
|
+
logger: (ev) => console.log(JSON.stringify(ev))
|
|
1841
|
+
});
|
|
1842
|
+
const onSig = async () => {
|
|
1843
|
+
await sup.stop();
|
|
1844
|
+
setTimeout(() => process.exit(0), 250);
|
|
1845
|
+
};
|
|
1846
|
+
process.on("SIGINT", onSig);
|
|
1847
|
+
process.on("SIGTERM", onSig);
|
|
1848
|
+
await sup.start();
|
|
1849
|
+
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ev: "supervise-running", config: cfgPath }));
|
|
1850
|
+
const reportEvery = Number(process.env.HOLOSCRIPT_AGENT_STATUS_REPORT_MS ?? "300000");
|
|
1851
|
+
if (reportEvery > 0) {
|
|
1852
|
+
setInterval(() => {
|
|
1853
|
+
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ev: "supervisor-status", ...sup.status() }));
|
|
1854
|
+
}, reportEvery);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
async function cmdAudit(rest) {
|
|
1858
|
+
const logPath = rest.find((a) => a.startsWith("--log="))?.split("=")[1] ?? process.env.HOLOSCRIPT_AGENT_AUDIT_LOG ?? join4(homedir3(), ".holoscript-agent", "audit", "audit.jsonl");
|
|
1859
|
+
const sub = rest.find((a) => !a.startsWith("--")) ?? "rollup";
|
|
1860
|
+
const filter = {};
|
|
1861
|
+
for (const arg of rest) {
|
|
1862
|
+
if (arg.startsWith("--agent=")) filter.agent = arg.split("=")[1];
|
|
1863
|
+
if (arg.startsWith("--provider=")) filter.provider = arg.split("=")[1];
|
|
1864
|
+
if (arg.startsWith("--task=")) filter.task = arg.split("=")[1];
|
|
1865
|
+
if (arg.startsWith("--kind=")) filter.kind = arg.split("=")[1];
|
|
1866
|
+
if (arg.startsWith("--limit=")) filter.limit = Number(arg.split("=")[1]);
|
|
1867
|
+
}
|
|
1868
|
+
const log = new AuditLog({ logPath });
|
|
1869
|
+
if (sub === "rollup") {
|
|
1870
|
+
console.log(JSON.stringify(log.rollup(filter), null, 2));
|
|
1871
|
+
} else if (sub === "tail" || sub === "query") {
|
|
1872
|
+
const events = log.query(filter);
|
|
1873
|
+
for (const e of events) console.log(JSON.stringify(e));
|
|
1874
|
+
} else {
|
|
1875
|
+
throw new Error("Usage: holoscript-agent audit [rollup|query|tail] [--agent=<h>] [--provider=<p>] [--task=<id>] [--kind=<k>] [--limit=<n>] [--log=<path>]");
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
async function cmdProvision(rest) {
|
|
1879
|
+
const handle = rest.find((a) => a.startsWith("--handle="))?.split("=")[1];
|
|
1880
|
+
if (!handle) {
|
|
1881
|
+
throw new Error("Usage: holoscript-agent provision --handle=<name> [--execute] [--force]");
|
|
1882
|
+
}
|
|
1883
|
+
const execute = rest.includes("--execute");
|
|
1884
|
+
const force = rest.includes("--force");
|
|
1885
|
+
const founderBearer = process.env.HOLOMESH_API_KEY;
|
|
1886
|
+
if (!founderBearer) {
|
|
1887
|
+
throw new Error("HOLOMESH_API_KEY env var required for provisioning (founder-tier bearer for /register endpoints)");
|
|
1888
|
+
}
|
|
1889
|
+
const result = await provisionAgent(
|
|
1890
|
+
{
|
|
1891
|
+
handle,
|
|
1892
|
+
founderBearer,
|
|
1893
|
+
meshApiBase: process.env.HOLOMESH_API_BASE,
|
|
1894
|
+
seatsRoot: process.env.HOLOSCRIPT_AGENT_SEATS_ROOT,
|
|
1895
|
+
autoJoinTeamId: rest.includes("--no-join") ? void 0 : process.env.HOLOMESH_TEAM_ID
|
|
1896
|
+
},
|
|
1897
|
+
{ execute, force }
|
|
1898
|
+
);
|
|
1899
|
+
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ev: "provision-result", ...result }, null, 2));
|
|
1900
|
+
if (result.status === "executed" || result.status === "reused") {
|
|
1901
|
+
console.log("\n# Add these lines to your .env to use this seat:");
|
|
1902
|
+
for (const line of result.envVarLines) console.log(line);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
async function cmdStatus(rest) {
|
|
1906
|
+
const cfgPath = rest.find((a) => a.startsWith("--config="))?.split("=")[1];
|
|
1907
|
+
if (!cfgPath) {
|
|
1908
|
+
throw new Error("Usage: holoscript-agent status --config=<path-to-agents.json>");
|
|
1909
|
+
}
|
|
1910
|
+
const config = loadSupervisorConfig(cfgPath);
|
|
1911
|
+
console.log(JSON.stringify({
|
|
1912
|
+
config: cfgPath,
|
|
1913
|
+
agentCount: config.agents.length,
|
|
1914
|
+
enabled: config.agents.filter((a) => a.enabled !== false).map((a) => a.handle),
|
|
1915
|
+
disabled: config.agents.filter((a) => a.enabled === false).map((a) => a.handle),
|
|
1916
|
+
globalBudgetUsdPerDay: config.globalBudgetUsdPerDay ?? null,
|
|
1917
|
+
defaultTickIntervalMs: config.defaultTickIntervalMs ?? null
|
|
1918
|
+
}, null, 2));
|
|
1919
|
+
}
|
|
1920
|
+
async function cmdAblate(rest) {
|
|
1921
|
+
const specPath = rest.find((a) => a.startsWith("--spec="))?.split("=")[1];
|
|
1922
|
+
if (!specPath) {
|
|
1923
|
+
throw new Error("Usage: holoscript-agent ablate --spec=<path-to-ablation.json> [--out-md=<path>] [--out-json=<path>]");
|
|
1924
|
+
}
|
|
1925
|
+
const outMd = rest.find((a) => a.startsWith("--out-md="))?.split("=")[1];
|
|
1926
|
+
const outJson = rest.find((a) => a.startsWith("--out-json="))?.split("=")[1];
|
|
1927
|
+
if (!existsSync4(specPath)) throw new Error(`Spec file not found: ${specPath}`);
|
|
1928
|
+
const spec = JSON.parse(readFileSync5(specPath, "utf8"));
|
|
1929
|
+
const providers = spec.providers.map((p) => ({
|
|
1930
|
+
label: p.label,
|
|
1931
|
+
provider: p.provider,
|
|
1932
|
+
model: p.model,
|
|
1933
|
+
build: () => {
|
|
1934
|
+
switch (p.provider) {
|
|
1935
|
+
case "anthropic":
|
|
1936
|
+
return createAnthropicProvider({ defaultModel: p.model });
|
|
1937
|
+
case "openai":
|
|
1938
|
+
return createOpenAIProvider({ defaultModel: p.model });
|
|
1939
|
+
case "gemini":
|
|
1940
|
+
return createGeminiProvider({ defaultModel: p.model });
|
|
1941
|
+
case "local-llm":
|
|
1942
|
+
return createLocalLLMProvider({
|
|
1943
|
+
baseURL: process.env.HOLOSCRIPT_AGENT_LOCAL_LLM_BASE_URL,
|
|
1944
|
+
model: p.model
|
|
1945
|
+
});
|
|
1946
|
+
case "mock":
|
|
1947
|
+
return createMockProvider();
|
|
1948
|
+
}
|
|
1949
|
+
},
|
|
1950
|
+
pricer: p.pricePerCallUsd != null ? () => p.pricePerCallUsd : p.pricePerMtokInput != null && p.pricePerMtokOutput != null ? (u) => (u.promptTokens * p.pricePerMtokInput + u.completionTokens * p.pricePerMtokOutput) / 1e6 : void 0
|
|
1951
|
+
}));
|
|
1952
|
+
const startMsg = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ev: "ablation-start", task: spec.task.taskId, cells: providers.length });
|
|
1953
|
+
console.log(startMsg);
|
|
1954
|
+
const matrix = await runAblation({
|
|
1955
|
+
task: spec.task,
|
|
1956
|
+
providers,
|
|
1957
|
+
timeoutPerCellMs: spec.timeoutPerCellMs
|
|
1958
|
+
});
|
|
1959
|
+
if (outJson) {
|
|
1960
|
+
mkdirSync5(dirname5(resolve3(outJson)), { recursive: true });
|
|
1961
|
+
writeFileSync4(outJson, JSON.stringify(matrix, null, 2), "utf8");
|
|
1962
|
+
}
|
|
1963
|
+
if (outMd) {
|
|
1964
|
+
mkdirSync5(dirname5(resolve3(outMd)), { recursive: true });
|
|
1965
|
+
writeFileSync4(outMd, renderAblationMarkdown(matrix), "utf8");
|
|
1966
|
+
}
|
|
1967
|
+
if (!outMd && !outJson) {
|
|
1968
|
+
console.log(renderAblationMarkdown(matrix));
|
|
1969
|
+
}
|
|
1970
|
+
console.log(JSON.stringify({
|
|
1971
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1972
|
+
ev: "ablation-done",
|
|
1973
|
+
task: matrix.taskId,
|
|
1974
|
+
cells: matrix.cells.length,
|
|
1975
|
+
errors: matrix.cells.filter((c) => c.errorMessage).length,
|
|
1976
|
+
totalCostUsd: matrix.totalCostUsd,
|
|
1977
|
+
promptHash: matrix.promptHash,
|
|
1978
|
+
outMd: outMd ?? null,
|
|
1979
|
+
outJson: outJson ?? null
|
|
1980
|
+
}));
|
|
1981
|
+
}
|
|
1982
|
+
async function cmdWhoami() {
|
|
1983
|
+
const identity = loadIdentity();
|
|
1984
|
+
const mesh = new HolomeshClient({
|
|
1985
|
+
apiBase: identity.meshApiBase,
|
|
1986
|
+
bearer: identity.x402Bearer,
|
|
1987
|
+
teamId: identity.teamId
|
|
1988
|
+
});
|
|
1989
|
+
const me = await mesh.whoAmI();
|
|
1990
|
+
console.log(JSON.stringify({ identity: identityForLog(identity), me }, null, 2));
|
|
1991
|
+
}
|
|
1992
|
+
async function buildProvider(identity) {
|
|
1993
|
+
const p = identity.llmProvider;
|
|
1994
|
+
switch (p) {
|
|
1995
|
+
case "anthropic":
|
|
1996
|
+
return createAnthropicProvider({ defaultModel: identity.llmModel });
|
|
1997
|
+
case "openai":
|
|
1998
|
+
return createOpenAIProvider({ defaultModel: identity.llmModel });
|
|
1999
|
+
case "gemini":
|
|
2000
|
+
return createGeminiProvider({ defaultModel: identity.llmModel });
|
|
2001
|
+
case "mock":
|
|
2002
|
+
return createMockProvider();
|
|
2003
|
+
case "local-llm":
|
|
2004
|
+
return createLocalLLMProvider({
|
|
2005
|
+
baseURL: process.env.HOLOSCRIPT_AGENT_LOCAL_LLM_BASE_URL,
|
|
2006
|
+
model: identity.llmModel
|
|
2007
|
+
});
|
|
2008
|
+
default:
|
|
2009
|
+
throw new Error(
|
|
2010
|
+
`Provider "${p}" not yet wired in CLI \u2014 Phase 2 deliverable. Use anthropic | openai | gemini | local-llm | mock for now.`
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
function buildCommitHook(identity, mesh) {
|
|
2015
|
+
const enabled = (process.env.HOLOSCRIPT_AGENT_COMMIT_RESPONSES ?? "").toLowerCase();
|
|
2016
|
+
if (enabled !== "1" && enabled !== "true") return void 0;
|
|
2017
|
+
const outputDir = process.env.HOLOSCRIPT_AGENT_OUTPUT_DIR ?? "agent-out";
|
|
2018
|
+
const workingDir = process.env.HOLOSCRIPT_AGENT_WORKING_DIR ?? process.cwd();
|
|
2019
|
+
const scope = process.env.HOLOSCRIPT_AGENT_COMMIT_SCOPE ?? `agent(${identity.handle})`;
|
|
2020
|
+
const writer = makeCommitHook({ outputDir, workingDir, scope });
|
|
2021
|
+
return async (result, task) => {
|
|
2022
|
+
const out = await writer(result, task, identity);
|
|
2023
|
+
await mesh.sendMessageOnTask(
|
|
2024
|
+
task.id,
|
|
2025
|
+
`[${identity.handle}] response committed at ${out.commitHash?.slice(0, 12) ?? "(no-hash)"} -> ${out.filePath}`
|
|
2026
|
+
);
|
|
2027
|
+
if (out.commitHash) {
|
|
2028
|
+
await mesh.markDone(task.id, `auto: ${task.title}`, out.commitHash);
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
function scopeTierFromEnv() {
|
|
2033
|
+
const t = (process.env.HOLOSCRIPT_AGENT_SCOPE_TIER ?? "warm").toLowerCase();
|
|
2034
|
+
if (t === "cold" || t === "warm" || t === "hot") return t;
|
|
2035
|
+
throw new Error(`HOLOSCRIPT_AGENT_SCOPE_TIER must be cold|warm|hot, got: ${t}`);
|
|
2036
|
+
}
|
|
2037
|
+
function stateFilePath(identity) {
|
|
2038
|
+
const dir = process.env.HOLOSCRIPT_AGENT_STATE_DIR ?? join4(homedir3(), ".holoscript-agent", "cost-state");
|
|
2039
|
+
return join4(dir, `${identity.handle}.json`);
|
|
2040
|
+
}
|
|
2041
|
+
function auditLogPath() {
|
|
2042
|
+
return process.env.HOLOSCRIPT_AGENT_AUDIT_LOG ?? join4(homedir3(), ".holoscript-agent", "audit", "audit.jsonl");
|
|
2043
|
+
}
|
|
2044
|
+
function buildAuditLog() {
|
|
2045
|
+
const enabled = (process.env.HOLOSCRIPT_AGENT_AUDIT_ENABLED ?? "1").toLowerCase();
|
|
2046
|
+
if (enabled === "0" || enabled === "false") return void 0;
|
|
2047
|
+
return new AuditLog({ logPath: auditLogPath() });
|
|
2048
|
+
}
|
|
2049
|
+
function printHelp() {
|
|
2050
|
+
console.log(`holoscript-agent \u2014 headless agent runtime
|
|
2051
|
+
|
|
2052
|
+
USAGE
|
|
2053
|
+
holoscript-agent run start the daemon (heartbeat + claim + execute loop)
|
|
2054
|
+
holoscript-agent tick single tick, then exit (useful in CI / cron / smoke tests)
|
|
2055
|
+
holoscript-agent whoami verify identity tuple resolves end-to-end (/me + env)
|
|
2056
|
+
holoscript-agent ablate --spec=<path> run a cross-LLM ablation; spec = JSON with task + providers
|
|
2057
|
+
[--out-md=<path>] optional: write markdown ablation table
|
|
2058
|
+
[--out-json=<path>] optional: write structured JSON matrix
|
|
2059
|
+
holoscript-agent supervise --config=<path> run N agents from agents.json (multi-agent daemon)
|
|
2060
|
+
holoscript-agent status --config=<path> print parsed config summary (validates schema)
|
|
2061
|
+
holoscript-agent provision --handle=<name> provision a fresh x402 seat for a brain (dry-run by default)
|
|
2062
|
+
[--execute] actually generate wallet + register against production
|
|
2063
|
+
[--force] re-register a handle whose seat already exists (dangerous)
|
|
2064
|
+
holoscript-agent audit [rollup|query|tail] query the per-agent audit log (default sub: rollup)
|
|
2065
|
+
[--agent=<h>] filter by agent handle
|
|
2066
|
+
[--provider=<p>] filter by LLM provider
|
|
2067
|
+
[--task=<id>] filter by task id
|
|
2068
|
+
[--kind=<k>] filter by kind (task-executed | ablation-cell | ...)
|
|
2069
|
+
[--limit=<n>] keep last N events
|
|
2070
|
+
[--log=<path>] override log path (default ~/.holoscript-agent/audit/audit.jsonl)
|
|
2071
|
+
holoscript-agent help print this
|
|
2072
|
+
|
|
2073
|
+
REQUIRED ENV
|
|
2074
|
+
HOLOSCRIPT_AGENT_HANDLE agent handle (e.g. "security-auditor")
|
|
2075
|
+
HOLOSCRIPT_AGENT_PROVIDER anthropic | openai | gemini | local-llm | mock
|
|
2076
|
+
HOLOSCRIPT_AGENT_MODEL model id (e.g. "claude-opus-4-7")
|
|
2077
|
+
HOLOSCRIPT_AGENT_BRAIN path to .hsplus brain composition
|
|
2078
|
+
HOLOSCRIPT_AGENT_WALLET 0x\u2026 wallet address
|
|
2079
|
+
HOLOSCRIPT_AGENT_X402_BEARER per-surface mesh bearer (W.087 vertex B)
|
|
2080
|
+
HOLOMESH_TEAM_ID target team id
|
|
2081
|
+
ANTHROPIC_API_KEY | OPENAI_API_KEY | GEMINI_API_KEY per provider
|
|
2082
|
+
|
|
2083
|
+
OPTIONAL ENV
|
|
2084
|
+
HOLOSCRIPT_AGENT_BUDGET_USD_DAY default 5
|
|
2085
|
+
HOLOSCRIPT_AGENT_SCOPE_TIER cold | warm | hot (default warm)
|
|
2086
|
+
HOLOSCRIPT_AGENT_TICK_MS daemon tick interval, default 60000
|
|
2087
|
+
HOLOSCRIPT_AGENT_STATE_DIR where to persist cost state (default ~/.holoscript-agent/cost-state)
|
|
2088
|
+
HOLOSCRIPT_AGENT_SURFACE label for handoffs / presence (default = handle)
|
|
2089
|
+
HOLOMESH_API_BASE default https://mcp.holoscript.net/api/holomesh
|
|
2090
|
+
HOLOSCRIPT_AGENT_COMMIT_RESPONSES "1" or "true" \u2192 write responses as memos and git-commit them
|
|
2091
|
+
HOLOSCRIPT_AGENT_OUTPUT_DIR memo output dir (rel to working dir, default "agent-out")
|
|
2092
|
+
HOLOSCRIPT_AGENT_WORKING_DIR git repo to commit into (default process.cwd())
|
|
2093
|
+
HOLOSCRIPT_AGENT_COMMIT_SCOPE commit subject scope (default "agent(<handle>)")
|
|
2094
|
+
HOLOSCRIPT_AGENT_LOCAL_LLM_BASE_URL local-llm provider base URL (default http://localhost:8080)
|
|
2095
|
+
`);
|
|
2096
|
+
}
|
|
2097
|
+
main().catch((err) => {
|
|
2098
|
+
console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ev: "fatal", message: err instanceof Error ? err.message : String(err) }));
|
|
2099
|
+
process.exit(1);
|
|
2100
|
+
});
|
|
2101
|
+
//# sourceMappingURL=index.js.map
|