@holoscript/holoscript-agent 2.0.2 → 2.0.4

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/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { homedir as homedir3, hostname as hostname2 } from "os";
5
5
  import { join as join4 } from "path";
6
6
  import { createHash as createHash4, createDecipheriv, randomBytes as randomBytes2 } from "crypto";
7
- import { Wallet as Wallet2 } from "ethers";
7
+ import { Wallet as Wallet3 } from "ethers";
8
8
  import {
9
9
  createAnthropicProvider,
10
10
  createOpenAIProvider,
@@ -35,7 +35,7 @@ function loadIdentity(env = process.env) {
35
35
  `HOLOSCRIPT_AGENT_PROVIDER=${provider} not in [${[...VALID_PROVIDERS].join(", ")}]`
36
36
  );
37
37
  }
38
- const x402Bearer = required(env, "HOLOSCRIPT_AGENT_X402_BEARER");
38
+ const x402Bearer = (env.HOLOSCRIPT_AGENT_X402_BEARER ?? "").trim();
39
39
  const wallet = required(env, "HOLOSCRIPT_AGENT_WALLET");
40
40
  if (!/^0x[0-9a-fA-F]{40}$/.test(wallet)) {
41
41
  throw new Error(`HOLOSCRIPT_AGENT_WALLET is not a 0x-prefixed 40-hex address: ${wallet}`);
@@ -72,7 +72,7 @@ function identityForLog(id) {
72
72
  handle: id.handle,
73
73
  surface: id.surface,
74
74
  wallet: `${id.wallet.slice(0, 6)}\u2026${id.wallet.slice(-4)}`,
75
- bearer: `${id.x402Bearer.slice(0, 6)}\u2026`,
75
+ bearer: id.x402Bearer ? `${id.x402Bearer.slice(0, 6)}\u2026` : "(broker-resolved)",
76
76
  provider: id.llmProvider,
77
77
  model: id.llmModel,
78
78
  brain: id.brainPath,
@@ -95,7 +95,8 @@ async function loadBrain(brainPath, scopeTier = "warm") {
95
95
  requires,
96
96
  prefers,
97
97
  avoids,
98
- reflect: extractReflect(raw)
98
+ reflect: extractReflect(raw),
99
+ onTaskActions: extractOnTaskActions(raw)
99
100
  };
100
101
  }
101
102
  function extractReflect(brain) {
@@ -130,6 +131,53 @@ function extractIdentity(brain) {
130
131
  const avoids = listField(identityBlock, "avoids") ?? [];
131
132
  return { domain, capabilityTags, requires, prefers, avoids };
132
133
  }
134
+ function extractOnTaskActions(brain) {
135
+ const block = sliceNamedBlock(brain, "on_task");
136
+ if (!block) return [];
137
+ const VERBS = ["recall", "rag_query", "llm_call", "plan", "reflect"];
138
+ const entries = [];
139
+ for (const verb of VERBS) {
140
+ const re = new RegExp(`\\b${verb}\\s*\\{`, "g");
141
+ let m;
142
+ while ((m = re.exec(block)) !== null) {
143
+ const start = m.index + m[0].length;
144
+ let depth = 1;
145
+ let end = -1;
146
+ for (let i = start; i < block.length; i++) {
147
+ if (block[i] === "{") depth++;
148
+ else if (block[i] === "}") {
149
+ depth--;
150
+ if (depth === 0) {
151
+ end = i;
152
+ break;
153
+ }
154
+ }
155
+ }
156
+ if (end < 0) continue;
157
+ entries.push({ verb, config: parseKVBlock(block.slice(start, end)), _pos: m.index });
158
+ }
159
+ }
160
+ return entries.sort((a, b) => a._pos - b._pos).map(({ _pos: _ignored, ...rest }) => rest);
161
+ }
162
+ function parseKVBlock(block) {
163
+ const out = {};
164
+ const strRe = /\b(\w+)\s*:\s*"([^"]*)"/g;
165
+ let m;
166
+ while ((m = strRe.exec(block)) !== null) out[m[1]] = m[2];
167
+ const arrRe = /\b(\w+)\s*:\s*\[([^\]]*)\]/g;
168
+ while ((m = arrRe.exec(block)) !== null) {
169
+ out[m[1]] = m[2].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter((s) => s.length > 0);
170
+ }
171
+ const boolRe = /\b(\w+)\s*:\s*(true|false)\b/g;
172
+ while ((m = boolRe.exec(block)) !== null) {
173
+ if (!(m[1] in out)) out[m[1]] = m[2] === "true";
174
+ }
175
+ const numRe = /\b(\w+)\s*:\s*(-?\d+(?:\.\d+)?)\b/g;
176
+ while ((m = numRe.exec(block)) !== null) {
177
+ if (!(m[1] in out)) out[m[1]] = parseFloat(m[2]);
178
+ }
179
+ return out;
180
+ }
133
181
  function sliceNamedBlock(src, name) {
134
182
  const re = new RegExp(`\\b${name}\\s*:?\\s*\\{`, "g");
135
183
  const match = re.exec(src);
@@ -570,6 +618,37 @@ var HolomeshClient = class {
570
618
  async delegateTask(taskId, toAgentId) {
571
619
  return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "delegate", toAgentId }));
572
620
  }
621
+ // ── Cognitive-verb knowledge surface (Phase 2.2 — recall / rag_query) ────────
622
+ /**
623
+ * Query the TEAM knowledge base (the `rag_query` cognitive verb). Bearer-only
624
+ * GET; `q` is the server-side search filter. Returns [] on any failure so a
625
+ * retrieval miss never breaks a tick.
626
+ */
627
+ async queryTeamKnowledge(query, limit = 5) {
628
+ const qs = new URLSearchParams({ q: query, limit: String(limit) }).toString();
629
+ try {
630
+ const data = await this.req(
631
+ "GET",
632
+ `/team/${this.teamId}/knowledge?${qs}`
633
+ );
634
+ return data.entries ?? [];
635
+ } catch {
636
+ return [];
637
+ }
638
+ }
639
+ /**
640
+ * Query this agent's PRIVATE workspace knowledge (the `recall` cognitive verb).
641
+ * The endpoint has no server-side search param, so the caller filters by query
642
+ * client-side. Returns [] on any failure.
643
+ */
644
+ async queryPrivateKnowledge() {
645
+ try {
646
+ const data = await this.req("GET", `/knowledge/private`);
647
+ return data.entries ?? [];
648
+ } catch {
649
+ return [];
650
+ }
651
+ }
573
652
  async req(method, path, body) {
574
653
  const url = `${this.apiBase}${path}`;
575
654
  const res = await this.fetchImpl(url, {
@@ -619,6 +698,44 @@ function priority(t) {
619
698
  return map[String(t.priority).toLowerCase()] ?? 5;
620
699
  }
621
700
 
701
+ // src/bearer-broker.ts
702
+ import { Wallet } from "ethers";
703
+ var RECOVERY_DOMAIN = { name: "HoloMesh", version: "1" };
704
+ var RECOVERY_TYPES = {
705
+ Recovery: [{ name: "nonce", type: "string" }]
706
+ };
707
+ async function resolveBearerViaBroker(opts) {
708
+ const doFetch = opts.fetchImpl ?? fetch;
709
+ const wallet = new Wallet(opts.privateKey);
710
+ const address = wallet.address;
711
+ const base = opts.meshApiBase.replace(/\/+$/, "");
712
+ const chalRes = await doFetch(`${base}/key/challenge`, {
713
+ method: "POST",
714
+ headers: { "Content-Type": "application/json" },
715
+ body: JSON.stringify({ wallet_address: address })
716
+ });
717
+ if (!chalRes.ok) {
718
+ throw new Error(`bearer-broker: /key/challenge returned ${chalRes.status} for ${address}`);
719
+ }
720
+ const chal = await chalRes.json();
721
+ if (!chal.nonce) throw new Error("bearer-broker: /key/challenge returned no nonce");
722
+ const signature = await wallet.signTypedData(RECOVERY_DOMAIN, RECOVERY_TYPES, {
723
+ nonce: chal.nonce
724
+ });
725
+ const recRes = await doFetch(`${base}/key/recover`, {
726
+ method: "POST",
727
+ headers: { "Content-Type": "application/json" },
728
+ body: JSON.stringify({ wallet_address: address, nonce: chal.nonce, signature })
729
+ });
730
+ if (!recRes.ok) {
731
+ throw new Error(`bearer-broker: /key/recover returned ${recRes.status} for ${address}`);
732
+ }
733
+ const rec = await recRes.json();
734
+ const bearer = rec.agent?.api_key;
735
+ if (!bearer) throw new Error("bearer-broker: /key/recover returned no api_key");
736
+ return bearer;
737
+ }
738
+
622
739
  // src/cael-builder.ts
623
740
  import { createHash } from "crypto";
624
741
  function sha(input) {
@@ -1030,6 +1147,109 @@ function errResult(id, message) {
1030
1147
  return { type: "tool_result", tool_use_id: id, content: message, is_error: true };
1031
1148
  }
1032
1149
 
1150
+ // src/cognitive-verbs.ts
1151
+ var DEFAULT_LIMIT = 5;
1152
+ var MAX_ENTRY_CHARS = 320;
1153
+ var MAX_INJECTED_CHARS = 2400;
1154
+ function strField(config, ...keys) {
1155
+ for (const k of keys) {
1156
+ const v = config[k];
1157
+ if (typeof v === "string" && v.trim()) return v.trim();
1158
+ }
1159
+ return "";
1160
+ }
1161
+ function numField(config, key, fallback) {
1162
+ const v = config[key];
1163
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback;
1164
+ }
1165
+ function formatEntries(entries) {
1166
+ let out = "";
1167
+ for (const e of entries) {
1168
+ const line = `- ${(e.content ?? "").replace(/\s+/g, " ").trim().slice(0, MAX_ENTRY_CHARS)}`;
1169
+ if (out.length + line.length > MAX_INJECTED_CHARS) break;
1170
+ out += (out ? "\n" : "") + line;
1171
+ }
1172
+ return out;
1173
+ }
1174
+ async function augmentWithOnTaskCognition(deps) {
1175
+ let content = deps.systemPrompt;
1176
+ if (!deps.onTaskActions || deps.onTaskActions.length === 0) return content;
1177
+ for (const action of deps.onTaskActions) {
1178
+ try {
1179
+ switch (action.verb) {
1180
+ case "llm_call": {
1181
+ const prompt = strField(action.config, "prompt");
1182
+ if (prompt) {
1183
+ content += `
1184
+
1185
+ [Brain on_task directive]
1186
+ ${prompt}`;
1187
+ deps.log({ ev: "on-task-llm-call", taskId: deps.task.id, promptLen: prompt.length });
1188
+ }
1189
+ break;
1190
+ }
1191
+ case "rag_query": {
1192
+ const query = strField(action.config, "query", "q") || deps.task.title;
1193
+ const limit = numField(action.config, "limit", DEFAULT_LIMIT);
1194
+ const entries = await deps.queryTeamKnowledge(query, limit);
1195
+ if (entries.length > 0) {
1196
+ content += `
1197
+
1198
+ [Retrieved knowledge for "${query}"]
1199
+ ${formatEntries(entries)}`;
1200
+ }
1201
+ deps.log({ ev: "on-task-rag-query", taskId: deps.task.id, query, retrieved: entries.length });
1202
+ break;
1203
+ }
1204
+ case "recall": {
1205
+ const query = strField(action.config, "query", "q");
1206
+ const limit = numField(action.config, "limit", DEFAULT_LIMIT);
1207
+ const all = await deps.queryPrivateKnowledge();
1208
+ const needle = query.toLowerCase();
1209
+ const matched = (needle ? all.filter((e) => `${e.id ?? ""} ${e.content ?? ""}`.toLowerCase().includes(needle)) : all).slice(0, limit);
1210
+ if (matched.length > 0) {
1211
+ content += `
1212
+
1213
+ [Recalled memory${query ? ` for "${query}"` : ""}]
1214
+ ${formatEntries(matched)}`;
1215
+ }
1216
+ deps.log({ ev: "on-task-recall", taskId: deps.task.id, query, recalled: matched.length });
1217
+ break;
1218
+ }
1219
+ case "plan": {
1220
+ if (!deps.plan) break;
1221
+ const goal = strField(action.config, "goal", "prompt", "of") || deps.task.title;
1222
+ const planText = await deps.plan(
1223
+ `Produce a short numbered plan (max 6 steps) to accomplish this task. Be concrete and specific to the goal; do not execute it.
1224
+
1225
+ Goal: ${goal}`
1226
+ );
1227
+ const trimmed = planText.trim().slice(0, MAX_INJECTED_CHARS);
1228
+ if (trimmed) {
1229
+ content += `
1230
+
1231
+ [Plan]
1232
+ ${trimmed}`;
1233
+ deps.log({ ev: "on-task-plan", taskId: deps.task.id, planLen: trimmed.length });
1234
+ }
1235
+ break;
1236
+ }
1237
+ // `reflect` is handled post-artifact in runner.ts, not here.
1238
+ default:
1239
+ break;
1240
+ }
1241
+ } catch (err) {
1242
+ deps.log({
1243
+ ev: "on-task-verb-error",
1244
+ taskId: deps.task.id,
1245
+ verb: action.verb,
1246
+ message: err instanceof Error ? err.message : String(err)
1247
+ });
1248
+ }
1249
+ }
1250
+ return content;
1251
+ }
1252
+
1033
1253
  // src/runner.ts
1034
1254
  var RUNTIME_VERSION = "1.0.0";
1035
1255
  var AgentRunner = class {
@@ -1112,8 +1332,23 @@ var AgentRunner = class {
1112
1332
  log({ ev: "claim", taskId: target.id, title: target.title });
1113
1333
  await mesh.claim(target.id);
1114
1334
  const start = Date.now();
1335
+ const systemContent = await augmentWithOnTaskCognition({
1336
+ systemPrompt: brain.systemPrompt,
1337
+ onTaskActions: brain.onTaskActions ?? [],
1338
+ task: { id: target.id, title: target.title },
1339
+ queryTeamKnowledge: (q, limit) => mesh.queryTeamKnowledge(q, limit),
1340
+ queryPrivateKnowledge: () => mesh.queryPrivateKnowledge(),
1341
+ plan: async (prompt) => {
1342
+ const resp = await provider.complete(
1343
+ { messages: [{ role: "user", content: prompt }], maxTokens: 512, temperature: 0.3 },
1344
+ identity.llmModel
1345
+ );
1346
+ return resp.content;
1347
+ },
1348
+ log
1349
+ });
1115
1350
  const messages = [
1116
- { role: "system", content: brain.systemPrompt },
1351
+ { role: "system", content: systemContent },
1117
1352
  { role: "user", content: buildTaskPrompt(target) }
1118
1353
  ];
1119
1354
  let aggUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
@@ -2173,7 +2408,7 @@ import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync a
2173
2408
  import { join as join3 } from "path";
2174
2409
  import { homedir as homedir2, hostname } from "os";
2175
2410
  import { randomBytes, createCipheriv, createHash as createHash3 } from "crypto";
2176
- import { Wallet } from "ethers";
2411
+ import { Wallet as Wallet2 } from "ethers";
2177
2412
  var HANDLE_PATTERN2 = /^[a-z0-9_-]{1,64}$/i;
2178
2413
  var EIP712_DOMAIN = { name: "HoloMesh", version: "1" };
2179
2414
  var EIP712_TYPES = {
@@ -2222,7 +2457,7 @@ async function provisionAgent(req, opts = { execute: false }) {
2222
2457
  };
2223
2458
  return reused;
2224
2459
  }
2225
- const wallet = Wallet.createRandom();
2460
+ const wallet = Wallet2.createRandom();
2226
2461
  mkdirSync4(seatDir, { recursive: true });
2227
2462
  const masterKey = ensureMasterKey(seatsRoot);
2228
2463
  const encryptedBlob = {
@@ -2445,11 +2680,31 @@ async function cmdRun(opts) {
2445
2680
  dailyBudgetUsd: identity.budgetUsdPerDay,
2446
2681
  pricer: defaultPricerForProvider(effectiveIdentity.llmProvider)
2447
2682
  });
2683
+ const seat = loadSeatWallet(identity.handle);
2684
+ let bearer = identity.x402Bearer;
2685
+ if (!bearer) {
2686
+ if (!seat) {
2687
+ throw new Error(
2688
+ "No HOLOSCRIPT_AGENT_X402_BEARER set and no seat wallet found to resolve it from the HoloKey broker. Provide a bearer, or point at the seat wallet via HOLOSCRIPT_AGENT_SEATS_ROOT + HOLOSCRIPT_AGENT_SEAT_ID (+ HOLOSCRIPT_AGENT_SEAT_MASTER_KEY)."
2689
+ );
2690
+ }
2691
+ bearer = await resolveBearerViaBroker({
2692
+ privateKey: seat.wallet.privateKey,
2693
+ meshApiBase: identity.meshApiBase
2694
+ });
2695
+ console.log(
2696
+ JSON.stringify({
2697
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2698
+ ev: "bearer-resolved-via-broker",
2699
+ wallet: `${seat.address.slice(0, 6)}\u2026${seat.address.slice(-4)}`
2700
+ })
2701
+ );
2702
+ }
2448
2703
  const mesh = new HolomeshClient({
2449
2704
  apiBase: identity.meshApiBase,
2450
- bearer: identity.x402Bearer,
2705
+ bearer,
2451
2706
  teamId: identity.teamId,
2452
- signer: buildRequestSigner(identity.handle)
2707
+ signer: buildRequestSigner(seat)
2453
2708
  });
2454
2709
  const commitHook = buildCommitHook(identity, mesh);
2455
2710
  const auditLog = buildAuditLog();
@@ -2701,10 +2956,19 @@ async function cmdAblate(rest) {
2701
2956
  }
2702
2957
  async function cmdWhoami() {
2703
2958
  const identity = loadIdentity();
2959
+ const seat = loadSeatWallet(identity.handle);
2960
+ let bearer = identity.x402Bearer;
2961
+ if (!bearer && seat) {
2962
+ bearer = await resolveBearerViaBroker({
2963
+ privateKey: seat.wallet.privateKey,
2964
+ meshApiBase: identity.meshApiBase
2965
+ });
2966
+ }
2704
2967
  const mesh = new HolomeshClient({
2705
2968
  apiBase: identity.meshApiBase,
2706
- bearer: identity.x402Bearer,
2707
- teamId: identity.teamId
2969
+ bearer,
2970
+ teamId: identity.teamId,
2971
+ signer: buildRequestSigner(seat)
2708
2972
  });
2709
2973
  const me = await mesh.whoAmI();
2710
2974
  console.log(JSON.stringify({ identity: identityForLog(identity), me }, null, 2));
@@ -2763,12 +3027,12 @@ function canonicalizeSigning(value) {
2763
3027
  const obj = value;
2764
3028
  return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${canonicalizeSigning(obj[k])}`).join(",")}}`;
2765
3029
  }
2766
- function buildRequestSigner(handle) {
3030
+ function loadSeatWallet(handle) {
2767
3031
  const seatsRoot = process.env.HOLOSCRIPT_AGENT_SEATS_ROOT ?? join4(homedir3(), ".holoscript-agent", "seats");
2768
3032
  const fp = createHash4("sha256").update(hostname2() + homedir3()).digest("hex").slice(0, 8);
2769
- const seatId = `holoscript-${handle}-${fp}-x402`;
3033
+ const seatId = process.env.HOLOSCRIPT_AGENT_SEAT_ID ?? `holoscript-${handle}-${fp}-x402`;
2770
3034
  const walletPath = join4(seatsRoot, seatId, "wallet.enc");
2771
- const masterKeyPath = join4(seatsRoot, ".master-key");
3035
+ const masterKeyPath = process.env.HOLOSCRIPT_AGENT_SEAT_MASTER_KEY ?? join4(seatsRoot, ".master-key");
2772
3036
  if (!existsSync4(walletPath) || !existsSync4(masterKeyPath)) return void 0;
2773
3037
  try {
2774
3038
  const blob = JSON.parse(readFileSync5(walletPath, "utf8"));
@@ -2776,21 +3040,28 @@ function buildRequestSigner(handle) {
2776
3040
  const iv = Buffer.from(blob.encrypted_privkey.iv, "base64");
2777
3041
  const ct = Buffer.from(blob.encrypted_privkey.ct, "base64");
2778
3042
  const tag = Buffer.from(blob.encrypted_privkey.tag, "base64");
2779
- const decipher = createDecipheriv(blob.encrypted_privkey.alg ?? "aes-256-gcm", masterKey, iv);
3043
+ const decipher = createDecipheriv(
3044
+ blob.encrypted_privkey.alg ?? "aes-256-gcm",
3045
+ masterKey,
3046
+ iv
3047
+ );
2780
3048
  decipher.setAuthTag(tag);
2781
3049
  const privateKey = Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
2782
- const wallet = new Wallet2(privateKey);
2783
- return async (body) => {
2784
- const nonce = randomBytes2(16).toString("hex");
2785
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2786
- const payload = canonicalizeSigning({ body, nonce, timestamp });
2787
- const signature = await wallet.signMessage(payload);
2788
- return { body, signature, signer_address: blob.address, nonce, timestamp };
2789
- };
3050
+ return { wallet: new Wallet3(privateKey), address: blob.address };
2790
3051
  } catch {
2791
3052
  return void 0;
2792
3053
  }
2793
3054
  }
3055
+ function buildRequestSigner(seat) {
3056
+ if (!seat) return void 0;
3057
+ return async (body) => {
3058
+ const nonce = randomBytes2(16).toString("hex");
3059
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3060
+ const payload = canonicalizeSigning({ body, nonce, timestamp });
3061
+ const signature = await seat.wallet.signMessage(payload);
3062
+ return { body, signature, signer_address: seat.address, nonce, timestamp };
3063
+ };
3064
+ }
2794
3065
  function scopeTierFromEnv() {
2795
3066
  const t = (process.env.HOLOSCRIPT_AGENT_SCOPE_TIER ?? "warm").toLowerCase();
2796
3067
  if (t === "cold" || t === "warm" || t === "hot") return t;
@@ -2838,11 +3109,17 @@ REQUIRED ENV
2838
3109
  HOLOSCRIPT_AGENT_MODEL model id (e.g. "claude-opus-4-8")
2839
3110
  HOLOSCRIPT_AGENT_BRAIN path to .hsplus brain composition
2840
3111
  HOLOSCRIPT_AGENT_WALLET 0x\u2026 wallet address
2841
- HOLOSCRIPT_AGENT_X402_BEARER per-surface mesh bearer (W.087 vertex B)
2842
3112
  HOLOMESH_TEAM_ID target team id
2843
3113
  ANTHROPIC_API_KEY | OPENAI_API_KEY | GEMINI_API_KEY per provider
2844
3114
 
2845
3115
  OPTIONAL ENV
3116
+ HOLOSCRIPT_AGENT_X402_BEARER per-surface mesh bearer. OPTIONAL: when absent, the runner
3117
+ resolves it from the HoloKey broker by proving wallet
3118
+ ownership (POST /key/challenge \u2192 sign \u2192 /key/recover), so the
3119
+ bearer is never stored in plaintext .env. Requires a seat wallet.
3120
+ HOLOSCRIPT_AGENT_SEAT_ID override the computed seat-dir name (e.g. a sovereign x402 seat
3121
+ "sovereign-<surface>-<fp>-default-x402"); pairs with SEATS_ROOT
3122
+ HOLOSCRIPT_AGENT_SEAT_MASTER_KEY override the master-key path used to decrypt the seat wallet.enc
2846
3123
  HOLOSCRIPT_AGENT_BUDGET_USD_DAY default 5
2847
3124
  HOLOSCRIPT_AGENT_SCOPE_TIER cold | warm | hot (default warm)
2848
3125
  HOLOSCRIPT_AGENT_TICK_MS daemon tick interval, default 60000