agentlife 2.6.31 → 2.6.33

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
@@ -296,6 +296,9 @@ function getOrCreateAgentDb(state, agentId) {
296
296
  return db;
297
297
  if (!state.dbBaseDir)
298
298
  throw new Error("DB base dir not initialized");
299
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(agentId)) {
300
+ throw new Error(`invalid agentId for DB path: ${agentId}`);
301
+ }
299
302
  fsSync.mkdirSync(state.dbBaseDir, { recursive: true });
300
303
  const { DatabaseSync } = requireSqlite();
301
304
  db = new DatabaseSync(path.join(state.dbBaseDir, `${agentId}.db`));
@@ -697,11 +700,21 @@ class TransientSurfaces {
697
700
  this.store.set(block.surfaceId, entry);
698
701
  return entry;
699
702
  }
700
- get(surfaceId) {
701
- return this.store.get(surfaceId) ?? null;
703
+ isExpired(entry, now) {
704
+ return (entry.isOverlay || entry.isInput) && now - entry.updatedAt > OVERLAY_TTL_MS;
702
705
  }
703
- has(surfaceId) {
704
- return this.store.has(surfaceId);
706
+ get(surfaceId, now = Date.now()) {
707
+ const entry = this.store.get(surfaceId);
708
+ if (!entry)
709
+ return null;
710
+ if (this.isExpired(entry, now)) {
711
+ this.store.delete(surfaceId);
712
+ return null;
713
+ }
714
+ return entry;
715
+ }
716
+ has(surfaceId, now = Date.now()) {
717
+ return this.get(surfaceId, now) !== null;
705
718
  }
706
719
  delete(surfaceId) {
707
720
  return this.store.delete(surfaceId);
@@ -709,7 +722,7 @@ class TransientSurfaces {
709
722
  list(now = Date.now()) {
710
723
  const alive = [];
711
724
  for (const [sid, entry] of this.store) {
712
- if (entry.isOverlay && now - entry.updatedAt > OVERLAY_TTL_MS) {
725
+ if (this.isExpired(entry, now)) {
713
726
  this.store.delete(sid);
714
727
  continue;
715
728
  }
@@ -1344,7 +1357,7 @@ Check AGENT_REGISTRY.md and agent descriptions. Match the user's intent to the r
1344
1357
 
1345
1358
  ### 2. Fallback — No matching agent
1346
1359
  - **Throwaway** ("32 x 44", "what time is it in Tokyo", "convert 5 miles to km") → route to "quick" agent
1347
- - **Everything else** → route to "agentlife-builder". The builder decides whether to create a new agent or expand an existing one. NEVER route to "main" — the main agent does not exist in AgentLife.
1360
+ - **Everything else** → route to "agentlife-builder". The builder decides whether to create a new agent, expand an existing one, or forward to an existing specialist — it never does the work itself. NEVER route to "main" — the main agent does not exist in AgentLife.
1348
1361
 
1349
1362
  ## ONE Agent Per Request
1350
1363
 
@@ -1445,8 +1458,9 @@ You create and improve specialist agents for the AgentLife dashboard.
1445
1458
  ## Rules
1446
1459
 
1447
1460
  - One agent per life domain (health, finance, home, etc.). Check \`agents_list\` first — expand an existing agent's AGENTS.md rather than duplicate its domain.
1448
- - NEVER push a confirmation / "ready" / "created" widget. The deterministic intro widget from the plugin IS the confirmation.
1449
- - Emit \`NO_REPLY\` ONLY after \`agentlife.createAgent\` has succeeded.
1461
+ - **You never do domain work and never own domain widgets.** You are the factory, not a specialist. If a routed message belongs to an EXISTING specialist's domain, do NOT handle it — forward it via \`sessions_send\` to that specialist's direct session (\`sessionKey="agent:{agentId}:agentlife:direct:operator"\`, \`timeoutSeconds=0\`) and emit \`NO_REPLY\`. The platform rejects builder pushes outside your lane: only \`{domain}-building\` loading stubs, \`agentlife-builder-*\` agent-management widgets, input surfaces, and deletes are allowed.
1462
+ - NEVER push a confirmation / "ready" / "created" widget. The new agent's own first widget IS the confirmation.
1463
+ - Emit \`NO_REPLY\` ONLY after \`agentlife.createAgent\` has succeeded (or after forwarding to an existing specialist).
1450
1464
 
1451
1465
  ## Creating a New Agent
1452
1466
 
@@ -1465,7 +1479,7 @@ Triggered when the orchestrator routes a "create X agent" / "track Y for me" / n
1465
1479
  - Description drives the orchestrator's routing — make it specific (domains, actions, data types).
1466
1480
  - \`tools: {profile:"full", alsoAllow:["agentlife_push"]}\` for agents needing every core tool plus widget push. \`{allow:["agentlife_push"]}\` for widget-only agents.
1467
1481
  5. **Hand off in ONE final turn, all in this order, before \`NO_REPLY\`:**
1468
- a. Push ONE actionable first widget for the newly-created agent under a surfaceId you own (\`{agentId}-today\`, \`{agentId}-start\`, similar). It must invite the FIRST user interaction — an input prompt, a starter metric with a CTA, a question the user can answer right now. NEVER an identity/confirmation card ("Agent ready", "Welcome to X"). The plugin no longer pushes a placeholder intro widget; this first push IS the dashboard's anchor for the new agent.
1482
+ a. Forward the user's ORIGINAL request to the new agent's direct session via \`sessions_send\` (\`sessionKey="agent:{agentId}:agentlife:direct:operator"\`, \`timeoutSeconds=0\`). Message shape: \`[system] You were just created for this request. Push your first actionable widget for it now — an input prompt, a starter metric with a CTA, or a question the user can answer right now. NEVER an identity/confirmation card. Original request: <the user's request verbatim>\`. The new agent owns its first widget from the start you NEVER push widgets on its behalf (the platform rejects builder-owned domain surfaces).
1469
1483
  b. \`delete {domain}-building\` — the loading widget from step 1.
1470
1484
  c. Emit \`NO_REPLY\`.
1471
1485
 
@@ -1483,11 +1497,11 @@ Keep under 8k chars. Required sections (omit what's not relevant):
1483
1497
 
1484
1498
  ## Improving Existing Agents
1485
1499
 
1486
- Read all workspace files + \`agentlife.quality\` + \`agentlife.trace\`. Targeted edits only — don't rewrite unless fundamentally broken. If config is missing \`tools\`, call \`agentlife.createAgent\` with the existing id + patch. Summarize in a widget.
1500
+ Read all workspace files + \`agentlife.quality\` + \`agentlife.trace\`. Targeted edits only — don't rewrite unless fundamentally broken. If config is missing \`tools\`, call \`agentlife.createAgent\` with the existing id + patch. Summarize in a widget under your namespace (\`agentlife-builder-improve-{agentId}\`).
1487
1501
 
1488
1502
  ## Deleting an Agent
1489
1503
 
1490
- \`agentlife.deleteAgent: {"id":"{agentId}"}\` then optionally \`exec rm -rf ~/.openclaw/workspace-{agentId}\`. Push a confirmation widget.
1504
+ \`agentlife.deleteAgent: {"id":"{agentId}"}\` then optionally \`exec rm -rf ~/.openclaw/workspace-{agentId}\`. Push a confirmation widget under your namespace (\`agentlife-builder-deleted-{agentId}\`).
1491
1505
 
1492
1506
  ## Registry Enrichment (system task)
1493
1507
 
@@ -1838,12 +1852,15 @@ async function provisionAgents(state, cfg, runtime, log) {
1838
1852
  }
1839
1853
  }
1840
1854
  const provisionedIds = new Set(PROVISIONED_AGENTS.map((a) => a.id));
1855
+ const backfilledSubagentIds = [];
1841
1856
  for (const agent of currentList) {
1842
1857
  if (provisionedIds.has(agent.id))
1843
1858
  continue;
1844
1859
  if (!agent.subagents) {
1845
1860
  agent.subagents = { allowAgents: ["*"] };
1846
1861
  configChanged = true;
1862
+ if (agent.id)
1863
+ backfilledSubagentIds.push(agent.id);
1847
1864
  log(`[agentlife] backfilled subagents for ${agent.id}`);
1848
1865
  }
1849
1866
  }
@@ -1856,6 +1873,18 @@ async function provisionAgents(state, cfg, runtime, log) {
1856
1873
  const configPath = path4.join(os.homedir(), ".openclaw", "openclaw.json");
1857
1874
  const rawCfgForVisibility = JSON.parse(readFileSync2(configPath, "utf-8"));
1858
1875
  const backupPath = path4.join(os.homedir(), ".openclaw", "agentlife", "config-backup.json");
1876
+ if (backfilledSubagentIds.length > 0) {
1877
+ try {
1878
+ let backup = {};
1879
+ try {
1880
+ backup = JSON.parse(readFileSync2(backupPath, "utf-8"));
1881
+ } catch {}
1882
+ const existing = Array.isArray(backup.subagentsBackfilledIds) ? backup.subagentsBackfilledIds : [];
1883
+ backup.subagentsBackfilledIds = [...new Set([...existing, ...backfilledSubagentIds])];
1884
+ writeFileSync(backupPath, JSON.stringify(backup, null, 2) + `
1885
+ `, "utf-8");
1886
+ } catch {}
1887
+ }
1859
1888
  let visibilityWritten = false;
1860
1889
  const currentVisibility = rawCfgForVisibility?.tools?.sessions?.visibility;
1861
1890
  if (currentVisibility !== "all") {
@@ -3207,15 +3236,18 @@ function rescheduleFailedFollowup(state, row, error) {
3207
3236
  function buildFireInstruction(state, row) {
3208
3237
  const view2 = getSurface(state, row.surfaceId);
3209
3238
  const goalText = view2?.goal ?? (view2 ? extractTitleAndDetail({ lines: view2.lines }).title : null);
3210
- const goalLabel = goalText ? ` (goal: "${goalText}")` : "";
3239
+ const goalSafe = goalText ? goalText.replace(/[\r\n]+/g, " ").replace(/"/g, "'").slice(0, 200) : null;
3240
+ const goalLabel = goalSafe ? ` (goal: "${goalSafe}")` : "";
3241
+ const sanitizeFire = (s) => s.replace(/[\r\n]+/g, " ").replace(/"/g, "'");
3242
+ const instructionSafe = sanitizeFire(row.instruction).slice(0, 500);
3211
3243
  const recentFires = lookupRecentFiredFollowups(state, row.surfaceId, 5);
3212
3244
  const prior = recentFires.filter((m) => m !== row.instruction);
3213
3245
  const priorBlock = prior.length > 0 ? `
3214
3246
  Prior followups on this surface — your next must not match any of these with parameters rotated:
3215
- ${prior.slice(0, 3).map((m) => ` • "${m}"`).join(`
3247
+ ${prior.slice(0, 3).map((m) => ` • "${sanitizeFire(m).slice(0, 300)}"`).join(`
3216
3248
  `)}` : "";
3217
3249
  return [
3218
- `Widget followup for ${row.surfaceId}${goalLabel}: ${row.instruction}`,
3250
+ `Widget followup for ${row.surfaceId}${goalLabel}: ${instructionSafe}`,
3219
3251
  `Stay on this surfaceId — update it, push a transient input surface, or delete it. New dashboard surfaceId only if this goal is complete and a successor begins (delete in the same turn).${priorBlock}`
3220
3252
  ].join(`
3221
3253
  `);
@@ -3277,17 +3309,21 @@ function pollFollowups(state) {
3277
3309
  }
3278
3310
  sweepCounter++;
3279
3311
  if (sweepCounter % 5 === 0 && state.surfaceIndex && state.followupQueue && state.fileSurfaceStorage) {
3280
- sweepExpiredSurfaces({
3281
- index: state.surfaceIndex,
3282
- queue: state.followupQueue,
3283
- storage: state.fileSurfaceStorage
3284
- }).then((r) => {
3285
- for (const sid of r.purged)
3286
- broadcastDelete(sid);
3287
- if (r.purged.length > 0) {
3288
- console.log("[agentlife:sweep] purged %d expired surfaces", r.purged.length);
3289
- }
3290
- }).catch((e) => console.warn("[agentlife:sweep] error: %s", e?.message));
3312
+ try {
3313
+ sweepExpiredSurfaces({
3314
+ index: state.surfaceIndex,
3315
+ queue: state.followupQueue,
3316
+ storage: state.fileSurfaceStorage
3317
+ }).then((r) => {
3318
+ for (const sid of r.purged)
3319
+ broadcastDelete(sid);
3320
+ if (r.purged.length > 0) {
3321
+ console.log("[agentlife:sweep] purged %d expired surfaces", r.purged.length);
3322
+ }
3323
+ }).catch((e) => console.warn("[agentlife:sweep] error: %s", e?.message));
3324
+ } catch (e) {
3325
+ console.warn("[agentlife] sweep invocation failed synchronously: %s", e?.message);
3326
+ }
3291
3327
  }
3292
3328
  }
3293
3329
 
@@ -3983,7 +4019,6 @@ async function restoreConfigFromBackup(api, backupPath) {
3983
4019
  delete fileCfg.agents?.defaults?.maxConcurrent;
3984
4020
  }
3985
4021
  await writeRawConfigToDisk(fileCfg);
3986
- await fs5.unlink(backupPath);
3987
4022
  console.log("[agentlife] restored config: maxPingPongTurns=%s, maxConcurrent=%s", backup.maxPingPongTurns ?? "default", backup.maxConcurrent ?? "default");
3988
4023
  return `config restored`;
3989
4024
  }
@@ -4145,6 +4180,7 @@ function runInstallBootstrap(home) {
4145
4180
  ["sessionsVisibility", cfg?.tools?.sessions?.visibility ?? null],
4146
4181
  ["agentToAgentEnabled", cfg?.tools?.agentToAgent?.enabled ?? null],
4147
4182
  ["gatewayBind", cfg?.gateway?.bind ?? null],
4183
+ ["gatewayMode", cfg?.gateway?.mode ?? null],
4148
4184
  ["hooksAllowConversationAccess", cfg?.plugins?.entries?.agentlife?.hooks?.allowConversationAccess ?? null],
4149
4185
  ["maxPingPongTurns", cfg?.session?.agentToAgent?.maxPingPongTurns ?? null],
4150
4186
  ["maxConcurrent", cfg?.agents?.defaults?.maxConcurrent ?? null]
@@ -6264,7 +6300,7 @@ function mintBootstrapToken() {
6264
6300
  const devicesDir = path11.join(resolveStateDir(), "devices");
6265
6301
  const bootstrapPath = path11.join(devicesDir, "bootstrap.json");
6266
6302
  if (!fs11.existsSync(devicesDir))
6267
- fs11.mkdirSync(devicesDir, { recursive: true });
6303
+ fs11.mkdirSync(devicesDir, { recursive: true, mode: 448 });
6268
6304
  const registry = fs11.existsSync(bootstrapPath) ? JSON.parse(fs11.readFileSync(bootstrapPath, "utf-8")) : {};
6269
6305
  registry[bootstrapToken] = {
6270
6306
  token: bootstrapToken,
@@ -6274,7 +6310,8 @@ function mintBootstrapToken() {
6274
6310
  },
6275
6311
  issuedAtMs: Date.now()
6276
6312
  };
6277
- fs11.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2));
6313
+ fs11.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2), { mode: 384 });
6314
+ fs11.chmodSync(bootstrapPath, 384);
6278
6315
  return bootstrapToken;
6279
6316
  }
6280
6317
  function registerWebApp(api) {
@@ -6944,22 +6981,29 @@ function registerWidgetPushTool(api, state2) {
6944
6981
  const trimmed = rawBlock.trim();
6945
6982
  if (!trimmed)
6946
6983
  continue;
6947
- const deleteMatch = trimmed.match(/^delete\s+(\S+)/);
6984
+ const deleteSids = [...rawBlock.matchAll(/^\s*delete\s+(\S+)/gm)].map((m) => m[1]);
6948
6985
  const surfaceMatch = rawBlock.match(/^surface\s+(\S+)/m);
6949
- const sid = deleteMatch?.[1] ?? surfaceMatch?.[1] ?? null;
6950
- if (!sid) {
6986
+ const checks = [
6987
+ ...deleteSids.map((sid) => ({ sid, action: "delete" })),
6988
+ ...surfaceMatch ? [{ sid: surfaceMatch[1], action: "push" }] : []
6989
+ ];
6990
+ if (checks.length === 0) {
6951
6991
  filteredRawBlocks.push(rawBlock);
6952
6992
  continue;
6953
6993
  }
6954
- const owner = getAgentId(state2, sid);
6955
- if (owner && owner !== agentId) {
6956
- const action = deleteMatch ? "delete" : "push";
6957
- ownershipViolations.push({ surfaceId: sid, owner, action });
6958
- recordActivity(state2, "quality_warning", sessionKey, agentId, {
6959
- data: JSON.stringify({ issue: "surface_ownership_violation", surfaceId: sid, owner, attempt: agentId, action })
6960
- });
6961
- continue;
6994
+ let violated = false;
6995
+ for (const { sid, action } of checks) {
6996
+ const owner = getAgentId(state2, sid);
6997
+ if (owner && owner !== agentId) {
6998
+ ownershipViolations.push({ surfaceId: sid, owner, action });
6999
+ recordActivity(state2, "quality_warning", sessionKey, agentId, {
7000
+ data: JSON.stringify({ issue: "surface_ownership_violation", surfaceId: sid, owner, attempt: agentId, action })
7001
+ });
7002
+ violated = true;
7003
+ }
6962
7004
  }
7005
+ if (violated)
7006
+ continue;
6963
7007
  filteredRawBlocks.push(rawBlock);
6964
7008
  }
6965
7009
  if (filteredRawBlocks.length === 0 && ownershipViolations.length > 0) {
@@ -6968,13 +7012,15 @@ function registerWidgetPushTool(api, state2) {
6968
7012
  }
6969
7013
  let finalBlocks = filteredRawBlocks;
6970
7014
  const confirmationViolations = [];
7015
+ const domainViolations = [];
6971
7016
  if (agentId === "agentlife-builder") {
6972
7017
  const CONFIRMATION_SUFFIXES = /-(?:created|ready|built|setup|done|welcome)$/i;
7018
+ const BUILDER_ALLOWED = /(?:^agentlife-builder-|^dismiss-alt-|-building$)/i;
6973
7019
  const kept = [];
6974
7020
  for (const block of filteredRawBlocks) {
6975
7021
  const deleteMatch = block.trim().match(/^delete\s+(\S+)/);
6976
- const surfaceMatch = block.match(/^surface\s+(\S+)/m);
6977
- const sid = deleteMatch?.[1] ?? surfaceMatch?.[1] ?? null;
7022
+ const surfaceLine = block.match(/^surface\s+(\S+)([^\n]*)/m);
7023
+ const sid = deleteMatch?.[1] ?? surfaceLine?.[1] ?? null;
6978
7024
  if (!sid || deleteMatch) {
6979
7025
  kept.push(block);
6980
7026
  continue;
@@ -6986,10 +7032,25 @@ function registerWidgetPushTool(api, state2) {
6986
7032
  });
6987
7033
  continue;
6988
7034
  }
7035
+ const isInputSurface = /\binput\b/.test(surfaceLine?.[2] ?? "");
7036
+ if (!isInputSurface && !BUILDER_ALLOWED.test(sid)) {
7037
+ domainViolations.push(sid);
7038
+ recordActivity(state2, "quality_warning", sessionKey, agentId, {
7039
+ data: JSON.stringify({ issue: "builder_domain_widget", surfaceId: sid })
7040
+ });
7041
+ continue;
7042
+ }
6989
7043
  kept.push(block);
6990
7044
  }
6991
- if (confirmationViolations.length > 0 && kept.length === 0) {
6992
- return { content: [{ type: "text", text: buildBuilderConfirmationError(confirmationViolations) }] };
7045
+ if ((confirmationViolations.length > 0 || domainViolations.length > 0) && kept.length === 0) {
7046
+ const errs = [];
7047
+ if (confirmationViolations.length > 0)
7048
+ errs.push(buildBuilderConfirmationError(confirmationViolations));
7049
+ if (domainViolations.length > 0)
7050
+ errs.push(buildBuilderDomainWidgetError(domainViolations));
7051
+ return { content: [{ type: "text", text: errs.join(`
7052
+
7053
+ `) }] };
6993
7054
  }
6994
7055
  finalBlocks = kept;
6995
7056
  }
@@ -7110,6 +7171,10 @@ function registerWidgetPushTool(api, state2) {
7110
7171
  const detail = ownershipViolations.map((v) => `${v.surfaceId} (owner=${v.owner}, attempt=${v.action})`).join("; ");
7111
7172
  errors.push(buildOwnershipError(detail, true));
7112
7173
  }
7174
+ if (confirmationViolations.length > 0)
7175
+ errors.push(buildBuilderConfirmationError(confirmationViolations));
7176
+ if (domainViolations.length > 0)
7177
+ errors.push(buildBuilderDomainWidgetError(domainViolations));
7113
7178
  const summary = pushedSurfaceIds.length === 1 ? `Widget ${pushedSurfaceIds[0]} updated` : `${pushedSurfaceIds.length} widgets updated`;
7114
7179
  if (errors.length > 0) {
7115
7180
  return { content: [{ type: "text", text: `${summary}
@@ -7127,7 +7192,10 @@ function buildOwnershipError(detail, skipped = false) {
7127
7192
  return `[QUALITY ERROR] surface ownership violation: ${detail}. ` + `Each surfaceId is bound to the first agent that pushed it. ` + `You cannot push or delete surfaces owned by other agents. ` + (skipped ? `The offending block(s) were skipped — the original owner's content is preserved. ` : ``) + `If you need to coordinate, delegate via sessions_send (internal) to the owning agent. ` + `If you are emitting a new widget, pick a surfaceId under your own namespace.`;
7128
7193
  }
7129
7194
  function buildBuilderConfirmationError(violations) {
7130
- return `[QUALITY ERROR] builder pushed confirmation widget(s): ${violations.join(", ")}. ` + `Per BUILDER_AGENTS_MD, the builder does NOT push an "agent ready / created" widget. ` + `The plugin pushes a deterministic {agentId}-intro as the confirmation the moment ` + `agentlife.createAgent returns. Delete your loading widget and emit 'done'.`;
7195
+ return `[QUALITY ERROR] builder pushed confirmation widget(s): ${violations.join(", ")}. ` + `Per BUILDER_AGENTS_MD, the builder does NOT push an "agent ready / created" widget. ` + `The NEW agent pushes its own first widget hand the user's request off to it via ` + `sessions_send (sessionKey="agent:{agentId}:agentlife:direct:operator"). ` + `Delete your loading widget and emit NO_REPLY.`;
7196
+ }
7197
+ function buildBuilderDomainWidgetError(violations) {
7198
+ return `[QUALITY ERROR] builder pushed domain widget(s): ${violations.join(", ")}. ` + `You are the agent factory — you never do domain work or own domain widgets. ` + `Allowed surfaces: '{domain}-building' loading stubs, 'agentlife-builder-*' agent-management widgets, ` + `input surfaces, and deletes. For this request: if a specialist for the domain exists, forward the ` + `request via sessions_send to sessionKey="agent:{agentId}:agentlife:direct:operator" and emit NO_REPLY; ` + `if none exists, create one via agentlife.createAgent, then hand off — the NEW agent pushes its own first widget.`;
7131
7199
  }
7132
7200
  function buildGoalChangedError(ids) {
7133
7201
  return `[QUALITY ERROR] ${ids.join(", ")}: goal changed — use a new surfaceId for a new goal. One widget, one goal. Delete the old widget and push a new one with a different surfaceId.`;
@@ -7441,7 +7509,11 @@ function findSiblingAuthProfile(provider, targetAgentId) {
7441
7509
  function writeAgentAuthProfile(agentId, provider, match) {
7442
7510
  if (!match)
7443
7511
  return false;
7444
- const agentDir = path14.join(resolveOpenClawRoot2(), "agents", agentId, "agent");
7512
+ const agentsRoot = path14.join(resolveOpenClawRoot2(), "agents");
7513
+ const agentDir = path14.join(agentsRoot, agentId, "agent");
7514
+ if (!agentDir.startsWith(agentsRoot + path14.sep)) {
7515
+ throw new Error(`writeAgentAuthProfile: path traversal rejected for agentId: ${agentId}`);
7516
+ }
7445
7517
  fs13.mkdirSync(agentDir, { recursive: true });
7446
7518
  const credential = { ...match.profile };
7447
7519
  if (credential.type === "api_key" && typeof credential.key !== "string" && typeof credential.apiKey === "string") {
@@ -7462,11 +7534,15 @@ function registerAgentGateway(api, state2) {
7462
7534
  const subagents = params?.subagents && typeof params.subagents === "object" ? params.subagents : undefined;
7463
7535
  const identity = params?.identity && typeof params.identity === "object" ? params.identity : undefined;
7464
7536
  if (!id) {
7465
- respond(false, { error: "missing agent id" });
7537
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "missing agent id" });
7538
+ return;
7539
+ }
7540
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(id)) {
7541
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "invalid agent id: must match ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$ (no path separators or dots)" });
7466
7542
  return;
7467
7543
  }
7468
7544
  if (!workspace) {
7469
- respond(false, { error: "missing workspace path" });
7545
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "missing workspace path" });
7470
7546
  return;
7471
7547
  }
7472
7548
  const cfg = api.runtime.config.loadConfig();
@@ -7474,9 +7550,9 @@ function registerAgentGateway(api, state2) {
7474
7550
  const providerInConfig = provider ? authenticatedProvidersFromConfig(cfg).has(provider) : false;
7475
7551
  const siblingAuth = provider ? findSiblingAuthProfile(provider, id) : null;
7476
7552
  if (provider && !providerInConfig && !siblingAuth) {
7477
- respond(false, {
7478
- error: `missing provider credentials for ${provider}; configure ${provider} before creating an agent with ${model}`,
7553
+ respond(false, undefined, {
7479
7554
  code: "missing_provider_auth",
7555
+ message: `missing provider credentials for ${provider}; configure ${provider} before creating an agent with ${model}`,
7480
7556
  provider,
7481
7557
  model
7482
7558
  });
@@ -7562,16 +7638,20 @@ function registerAgentGateway(api, state2) {
7562
7638
  }
7563
7639
  }
7564
7640
  respond(true, { status, id, name, model, workspace, description, ...authCopied ? { authCopied: true } : {}, ...configFields.length ? { configFields } : {} });
7565
- });
7641
+ }, { scope: "operator.admin" });
7566
7642
  api.registerGatewayMethod("agentlife.deleteAgent", async ({ params, respond }) => {
7567
7643
  const id = typeof params?.id === "string" ? params.id.trim() : "";
7568
7644
  if (!id) {
7569
- respond(false, { error: "missing agent id" });
7645
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "missing agent id" });
7646
+ return;
7647
+ }
7648
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(id)) {
7649
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "invalid agent id: must match ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$ (no path separators or dots)" });
7570
7650
  return;
7571
7651
  }
7572
7652
  const provisionedIds = new Set(PROVISIONED_AGENTS.map((a) => a.id));
7573
7653
  if (provisionedIds.has(id)) {
7574
- respond(false, { error: "cannot delete provisioned agent" });
7654
+ respond(false, undefined, { code: "FAILED", message: "cannot delete provisioned agent" });
7575
7655
  return;
7576
7656
  }
7577
7657
  const cleanup = {
@@ -7709,7 +7789,7 @@ function registerAgentGateway(api, state2) {
7709
7789
  }
7710
7790
  console.log("[agentlife] deleted agent: %s (config=%s, registry=%s, surfaces=%d, sessions=%d, cron=%d, historyRows=%d, agentDb=%s, state=%s, workspace=%s)", id, removedFromConfig, removedFromRegistry, cleanup.surfacesCleared, cleanup.sessions, cleanup.cronJobs, cleanup.historyRows, cleanup.agentDbDeleted, cleanup.stateDeleted, cleanup.workspaceDeleted);
7711
7791
  respond(true, { status: "deleted", id, removedFromConfig, removedFromRegistry, cleanup });
7712
- });
7792
+ }, { scope: "operator.admin" });
7713
7793
  api.registerGatewayMethod("agentlife.agents", ({ respond }) => {
7714
7794
  const agents = {};
7715
7795
  for (const [id, entry] of state2.agentRegistry.entries()) {
@@ -7726,11 +7806,11 @@ function registerDbGateway(api, state2) {
7726
7806
  const sql = typeof params?.sql === "string" ? params.sql.trim() : "";
7727
7807
  const sqlParams = Array.isArray(params?.params) ? params.params : [];
7728
7808
  if (!agentId)
7729
- return respond(false, { error: "missing agentId" });
7809
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "missing agentId" });
7730
7810
  if (!sql)
7731
- return respond(false, { error: "missing sql" });
7811
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "missing sql" });
7732
7812
  if (/^\s*SELECT\b/i.test(sql))
7733
- return respond(false, { error: "use agentlife.db.query for SELECT" });
7813
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "use agentlife.db.query for SELECT" });
7734
7814
  try {
7735
7815
  const db = getOrCreateAgentDb(state2, agentId);
7736
7816
  if (sqlParams.length > 0) {
@@ -7741,19 +7821,19 @@ function registerDbGateway(api, state2) {
7741
7821
  respond(true, { changes: 0, lastInsertRowid: 0 });
7742
7822
  }
7743
7823
  } catch (err) {
7744
- respond(false, { error: err?.message ?? "db error" });
7824
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "db error" });
7745
7825
  }
7746
- });
7826
+ }, { scope: "operator.admin" });
7747
7827
  api.registerGatewayMethod("agentlife.db.query", ({ params, respond }) => {
7748
7828
  const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : "";
7749
7829
  const sql = typeof params?.sql === "string" ? params.sql.trim() : "";
7750
7830
  const sqlParams = Array.isArray(params?.params) ? params.params : [];
7751
7831
  if (!agentId)
7752
- return respond(false, { error: "missing agentId" });
7832
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "missing agentId" });
7753
7833
  if (!sql)
7754
- return respond(false, { error: "missing sql" });
7834
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "missing sql" });
7755
7835
  if (!/^\s*SELECT\b/i.test(sql))
7756
- return respond(false, { error: "use agentlife.db.exec for non-SELECT" });
7836
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "use agentlife.db.exec for non-SELECT" });
7757
7837
  try {
7758
7838
  const db = getOrCreateAgentDb(state2, agentId);
7759
7839
  const rows = db.prepare(sql).all(...sqlParams);
@@ -7763,9 +7843,9 @@ function registerDbGateway(api, state2) {
7763
7843
  const columns = result.length > 0 ? Object.keys(result[0]) : [];
7764
7844
  respond(true, { rows: result, columns, count: result.length, truncated });
7765
7845
  } catch (err) {
7766
- respond(false, { error: err?.message ?? "db error" });
7846
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "db error" });
7767
7847
  }
7768
- });
7848
+ }, { scope: "operator.admin" });
7769
7849
  }
7770
7850
 
7771
7851
  // gateway/history.ts
@@ -7806,7 +7886,7 @@ function registerHistoryGateway(api, state2) {
7806
7886
  const rows = db.prepare(`SELECT * FROM surface_events ${where} ORDER BY createdAt DESC LIMIT ? OFFSET ?`).all(...bind);
7807
7887
  respond(true, { events: rows, count: rows.length });
7808
7888
  } catch (err) {
7809
- respond(false, { error: err?.message ?? "history error" });
7889
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "history error" });
7810
7890
  }
7811
7891
  }, { scope: "operator.read" });
7812
7892
  api.registerGatewayMethod("agentlife.history.widgets", ({ params, respond }) => {
@@ -7847,7 +7927,7 @@ function registerHistoryGateway(api, state2) {
7847
7927
  `).all(...bind, limit, offset);
7848
7928
  respond(true, { widgets: rows, count: rows.length, hasMore: rows.length >= limit });
7849
7929
  } catch (err) {
7850
- respond(false, { error: err?.message ?? "history.widgets error" });
7930
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "history.widgets error" });
7851
7931
  }
7852
7932
  }, { scope: "operator.read" });
7853
7933
  api.registerGatewayMethod("agentlife.trace", ({ params, respond }) => {
@@ -7904,7 +7984,7 @@ function registerHistoryGateway(api, state2) {
7904
7984
  }));
7905
7985
  respond(true, { entries, hasMore });
7906
7986
  } catch (err) {
7907
- respond(false, { error: err?.message ?? "trace query failed" });
7987
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "trace query failed" });
7908
7988
  }
7909
7989
  }, { scope: "operator.read" });
7910
7990
  }
@@ -7977,13 +8057,13 @@ function registerUsageGateway(api, state2) {
7977
8057
  bySession: bySessionRows
7978
8058
  });
7979
8059
  } catch (err) {
7980
- respond(false, { error: err?.message ?? "usage query error" });
8060
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "usage query error" });
7981
8061
  }
7982
8062
  }, { scope: "operator.read" });
7983
8063
  api.registerGatewayMethod("agentlife.surfaceUsage", ({ params, respond }) => {
7984
8064
  const surfaceId = typeof params?.surfaceId === "string" ? params.surfaceId.trim() : "";
7985
8065
  if (!surfaceId)
7986
- return respond(false, { error: "missing surfaceId" });
8066
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "missing surfaceId" });
7987
8067
  try {
7988
8068
  const db = getOrCreateHistoryDb(state2);
7989
8069
  const rows = db.prepare(`SELECT * FROM surface_usage WHERE surfaceId = ? ORDER BY createdAt DESC`).all(surfaceId);
@@ -7999,7 +8079,7 @@ function registerUsageGateway(api, state2) {
7999
8079
  }, { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 0, estimatedCost: 0, llmCalls: 0 });
8000
8080
  respond(true, { surfaceId, entries: rows, totals });
8001
8081
  } catch (err) {
8002
- respond(false, { error: err?.message ?? "surface usage error" });
8082
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "surface usage error" });
8003
8083
  }
8004
8084
  }, { scope: "operator.read" });
8005
8085
  }
@@ -8048,7 +8128,7 @@ function registerSurfacesGateway(api, state2) {
8048
8128
  api.registerGatewayMethod("agentlife.dismiss", async ({ params, respond }) => {
8049
8129
  const surfaceId = params?.surfaceId;
8050
8130
  if (!surfaceId || typeof surfaceId !== "string") {
8051
- respond(false, { error: "missing surfaceId" });
8131
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "missing surfaceId" });
8052
8132
  return;
8053
8133
  }
8054
8134
  const view2 = getSurface(state2, surfaceId);
@@ -8061,11 +8141,12 @@ function registerSurfacesGateway(api, state2) {
8061
8141
  if (guided) {
8062
8142
  const agentId2 = view2.agentId;
8063
8143
  if (agentId2 && state2.runCommand) {
8064
- const goalSnippet = view2.goal ? ` goal="${view2.goal}"` : "";
8065
- const reasonSnippet = reason ? ` reason="${reason}"` : "";
8144
+ const sanitizeMeta = (s) => s.replace(/[\r\n]+/g, " ").replace(/"/g, "'").slice(0, 200);
8145
+ const goalSnippet = view2.goal ? ` goal="${sanitizeMeta(view2.goal)}"` : "";
8146
+ const reasonSnippet = reason ? ` reason="${sanitizeMeta(reason)}"` : "";
8066
8147
  const sessionKey = buildAgentSessionKey(agentId2);
8067
8148
  state2.pendingReactiveGuidance.set(sessionKey, DISMISS_ALTERNATIVES_GUIDANCE);
8068
- const message = `[system:dismiss-requested] surfaceId=${surfaceId}${goalSnippet}${reasonSnippet}`;
8149
+ const message = `[system:dismiss-requested] surfaceId=${sanitizeMeta(surfaceId)}${goalSnippet}${reasonSnippet}`;
8069
8150
  state2.runCommand([
8070
8151
  "openclaw",
8071
8152
  "gateway",
@@ -8119,11 +8200,22 @@ function registerSurfacesGateway(api, state2) {
8119
8200
  const surfaceId = typeof params?.surfaceId === "string" ? params.surfaceId.trim() : "";
8120
8201
  const event = typeof params?.event === "string" ? params.event.trim() : "";
8121
8202
  if (!surfaceId || !event) {
8122
- respond(false, { error: "missing surfaceId or event" });
8203
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "missing surfaceId or event" });
8204
+ return;
8205
+ }
8206
+ const ALLOWED_ENGAGE_EVENTS = new Set([
8207
+ "viewed",
8208
+ "detail_viewed",
8209
+ "action_clicked",
8210
+ "input_closed",
8211
+ "input_skipped"
8212
+ ]);
8213
+ if (!ALLOWED_ENGAGE_EVENTS.has(event)) {
8214
+ respond(false, undefined, { code: "INVALID_REQUEST", message: `invalid engagement event: ${event}` });
8123
8215
  return;
8124
8216
  }
8125
8217
  const agentId = getAgentId(state2, surfaceId) ?? undefined;
8126
- const metadata = typeof params?.metadata === "string" ? params.metadata.trim() : undefined;
8218
+ const metadata = typeof params?.metadata === "string" ? params.metadata.trim().slice(0, 1024) : undefined;
8127
8219
  recordSurfaceEvent(state2, surfaceId, event, undefined, agentId, metadata);
8128
8220
  respond(true, { surfaceId, event });
8129
8221
  }, { scope: "operator.read" });
@@ -8131,7 +8223,7 @@ function registerSurfacesGateway(api, state2) {
8131
8223
  const message = typeof params?.message === "string" ? params.message : "";
8132
8224
  const sessionKey = typeof params?.sessionKey === "string" ? params.sessionKey : "";
8133
8225
  if (!message) {
8134
- respond(false, { error: "missing message" });
8226
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "missing message" });
8135
8227
  return;
8136
8228
  }
8137
8229
  broadcastInput(message, sessionKey);
@@ -8140,7 +8232,7 @@ function registerSurfacesGateway(api, state2) {
8140
8232
  api.registerGatewayMethod("agentlife.chain.get", ({ params, respond }) => {
8141
8233
  const root = typeof params?.root === "string" ? params.root.trim() : "";
8142
8234
  if (!root) {
8143
- respond(false, { error: "missing root" });
8235
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "missing root" });
8144
8236
  return;
8145
8237
  }
8146
8238
  if (!state2.surfaceIndex) {
@@ -8164,18 +8256,22 @@ function registerSurfacesGateway(api, state2) {
8164
8256
  api.registerGatewayMethod("agentlife.action", ({ params, respond }) => {
8165
8257
  const surfaceId = typeof params?.surfaceId === "string" ? params.surfaceId.trim() : "";
8166
8258
  if (!surfaceId) {
8167
- respond(false, { error: "missing surfaceId" });
8259
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "missing surfaceId" });
8168
8260
  return;
8169
8261
  }
8170
8262
  const view2 = getSurface(state2, surfaceId);
8171
8263
  const agentId = view2?.agentId ?? null;
8172
8264
  const ctx = view2?.context;
8173
8265
  if (ctx && ctx.escalation === true && typeof ctx.target === "string") {
8174
- const target = ctx.target;
8175
- const sessionKey2 = buildAgentSessionKey(target);
8176
- console.log("[agentlife] action escalation: surface=%s routing to target=%s session=%s", surfaceId, target, sessionKey2);
8177
- respond(true, { surfaceId, agentId: target, sessionKey: sessionKey2 });
8178
- return;
8266
+ const target = ctx.target.trim();
8267
+ const shapeOk = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(target);
8268
+ if (shapeOk) {
8269
+ const sessionKey2 = buildAgentSessionKey(target);
8270
+ console.log("[agentlife] action escalation: surface=%s routing to target=%s session=%s", surfaceId, target, sessionKey2);
8271
+ respond(true, { surfaceId, agentId: target, sessionKey: sessionKey2 });
8272
+ return;
8273
+ }
8274
+ console.warn("[agentlife] action escalation REJECTED: surface=%s malformed target=%s — falling back to owner routing", surfaceId, target);
8179
8275
  }
8180
8276
  const origin = getOriginSessionKey(state2, surfaceId);
8181
8277
  let sessionKey;
@@ -8255,15 +8351,15 @@ function registerAutomationsGateway(api, state2) {
8255
8351
  const name = typeof params?.name === "string" ? params.name.trim() : "";
8256
8352
  const automationPath = typeof params?.path === "string" ? params.path.trim() : "";
8257
8353
  if (!type || !name || !automationPath) {
8258
- respond(false, { error: "missing required fields: type, name, path" });
8354
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "missing required fields: type, name, path" });
8259
8355
  return;
8260
8356
  }
8261
8357
  const surfaceId = typeof params?.surfaceId === "string" ? params.surfaceId.trim() : "";
8262
- const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : getAgentId(state2, surfaceId) ?? "";
8263
- const id = registerAutomation(state2, { surfaceId, agentId, type, name, path: automationPath });
8358
+ const agentId = surfaceId ? getAgentId(state2, surfaceId) ?? "" : typeof params?.agentId === "string" ? params.agentId.trim() : "";
8359
+ const id = registerAutomation(state2, surfaceId || null, agentId || null, type, name, automationPath);
8264
8360
  respond(true, { id, surfaceId, agentId, type, name, path: automationPath, status: "running" });
8265
8361
  } catch (err) {
8266
- respond(false, { error: err?.message ?? "register failed" });
8362
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "register failed" });
8267
8363
  }
8268
8364
  }, { scope: "operator.write" });
8269
8365
  api.registerGatewayMethod("agentlife.automations.list", ({ params, respond }) => {
@@ -8304,7 +8400,7 @@ function registerAutomationsGateway(api, state2) {
8304
8400
  }))
8305
8401
  });
8306
8402
  } catch (err) {
8307
- respond(false, { error: err?.message ?? "list failed" });
8403
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "list failed" });
8308
8404
  }
8309
8405
  }, { scope: "operator.read" });
8310
8406
  api.registerGatewayMethod("agentlife.automations.update", ({ params, respond }) => {
@@ -8312,28 +8408,28 @@ function registerAutomationsGateway(api, state2) {
8312
8408
  const id = typeof params?.id === "number" ? params.id : parseInt(params?.id, 10);
8313
8409
  const status = typeof params?.status === "string" ? params.status.trim() : "";
8314
8410
  if (!id || !status || !["running", "stopped", "error"].includes(status)) {
8315
- respond(false, { error: "missing id or invalid status (running|stopped|error)" });
8411
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "missing id or invalid status (running|stopped|error)" });
8316
8412
  return;
8317
8413
  }
8318
8414
  const db = getOrCreateHistoryDb(state2);
8319
8415
  db.prepare("UPDATE automations SET status = ?, updatedAt = ? WHERE id = ?").run(status, Date.now(), id);
8320
8416
  respond(true, { id, status });
8321
8417
  } catch (err) {
8322
- respond(false, { error: err?.message ?? "update failed" });
8418
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "update failed" });
8323
8419
  }
8324
8420
  }, { scope: "operator.write" });
8325
8421
  api.registerGatewayMethod("agentlife.automations.remove", ({ params, respond }) => {
8326
8422
  try {
8327
8423
  const id = typeof params?.id === "number" ? params.id : parseInt(params?.id, 10);
8328
8424
  if (!id) {
8329
- respond(false, { error: "missing id" });
8425
+ respond(false, undefined, { code: "INVALID_REQUEST", message: "missing id" });
8330
8426
  return;
8331
8427
  }
8332
8428
  const db = getOrCreateHistoryDb(state2);
8333
8429
  db.prepare("UPDATE automations SET status = 'removed', updatedAt = ? WHERE id = ?").run(Date.now(), id);
8334
8430
  respond(true, { id, status: "removed" });
8335
8431
  } catch (err) {
8336
- respond(false, { error: err?.message ?? "remove failed" });
8432
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "remove failed" });
8337
8433
  }
8338
8434
  }, { scope: "operator.write" });
8339
8435
  }
@@ -8466,7 +8562,7 @@ function registerAdminGateway(api, state2) {
8466
8562
  cleanup: { pending: cleanupPending, failed: cleanupFailed, exhausted: cleanupExhausted }
8467
8563
  });
8468
8564
  } catch (err) {
8469
- respond(false, { error: err?.message ?? "quality metrics unavailable" });
8565
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "quality metrics unavailable" });
8470
8566
  }
8471
8567
  }, { scope: "operator.read" });
8472
8568
  api.registerGatewayMethod("agentlife.health.list", ({ respond }) => {
@@ -8487,7 +8583,7 @@ function registerAdminGateway(api, state2) {
8487
8583
  const rows = runDailyCycle(db, day);
8488
8584
  respond(true, { day, rows });
8489
8585
  } catch (err) {
8490
- respond(false, { error: err?.message ?? "observability runCycle failed" });
8586
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "observability runCycle failed" });
8491
8587
  }
8492
8588
  }, { scope: "operator.write" });
8493
8589
  api.registerGatewayMethod("agentlife.sweep.run", ({ respond }) => {
@@ -8495,7 +8591,7 @@ function registerAdminGateway(api, state2) {
8495
8591
  const result = runDailySweep(state2);
8496
8592
  respond(true, result);
8497
8593
  } catch (err) {
8498
- respond(false, { error: err?.message ?? "daily sweep failed" });
8594
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "daily sweep failed" });
8499
8595
  }
8500
8596
  }, { scope: "operator.write" });
8501
8597
  api.registerGatewayMethod("agentlife.bootstrap", ({ params, respond }) => {
@@ -8520,7 +8616,7 @@ function registerAdminGateway(api, state2) {
8520
8616
  snapshot = fromDb;
8521
8617
  }
8522
8618
  if (snapshot.length === 0) {
8523
- respond(true, { status: "no bootstrap snapshot for this session", files: [], availableSessions: [...state2.sessionBootstrapSnapshots.keys()] });
8619
+ respond(true, { status: "no bootstrap snapshot for this session", files: [] });
8524
8620
  return;
8525
8621
  }
8526
8622
  respond(true, {
@@ -8623,7 +8719,6 @@ function registerAdminGateway(api, state2) {
8623
8719
  dbPath,
8624
8720
  dbPath ? `${dbPath}-wal` : null,
8625
8721
  dbPath ? `${dbPath}-shm` : null,
8626
- path15.join(baseDir, "config-backup.json"),
8627
8722
  path15.join(baseDir, "notification-config.json"),
8628
8723
  path15.join(baseDir, "canvas-node-identity.json")
8629
8724
  ].filter(Boolean);
@@ -8662,9 +8757,9 @@ function registerAdminGateway(api, state2) {
8662
8757
  });
8663
8758
  } catch (err) {
8664
8759
  console.error("[agentlife] uninstall failed:", err);
8665
- respond(false, { error: err?.message ?? "unknown error during uninstall" });
8760
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "unknown error during uninstall" });
8666
8761
  }
8667
- });
8762
+ }, { scope: "operator.admin" });
8668
8763
  api.registerGatewayMethod("agentlife.config.get", ({ respond }) => {
8669
8764
  respond(true, readPluginConfig());
8670
8765
  }, { scope: "operator.read" });
@@ -8817,7 +8912,8 @@ function registerEvolutionsGateway(api, state2) {
8817
8912
  userNote
8818
8913
  };
8819
8914
  recordActivity(state2, "quality_warning", null, row.agentId, { data: JSON.stringify(payload) });
8820
- const noteLine = userNote ? ` User note: "${userNote}".` : "";
8915
+ const noteSafe = userNote ? userNote.replace(/[\r\n]+/g, " ").replace(/"/g, "'").slice(0, 200) : null;
8916
+ const noteLine = noteSafe ? ` User note: "${noteSafe}".` : "";
8821
8917
  const message = `[system] User reverted ${row.filename} to a prior snapshot. ` + `The user judged the version that was live worse than the version they restored.${noteLine} ` + `Read the current ${row.filename} (now the older content) and the diff against your last write to understand what the user preferred. ` + `Treat this as authoritative — do not re-rewrite the same change. Update your understanding before any future refinement.`;
8822
8918
  sendToInternalSession(state2, row.agentId, message, `user-reverted-${row.id}-${Date.now()}`);
8823
8919
  console.log("[agentlife:evolutions] restored %s/%s to version %d (userNote=%s)", row.agentId, row.filename, row.id, userNote ? "yes" : "no");
@@ -9316,20 +9412,20 @@ function registerProvidersGateway(api, state2) {
9316
9412
  }));
9317
9413
  respond(true, { providers });
9318
9414
  } catch (err) {
9319
- respond(false, { error: err?.message ?? String(err) });
9415
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9320
9416
  }
9321
9417
  }, { scope: "operator.read" });
9322
9418
  api.registerGatewayMethod("agentlife.providers.set", async ({ params, respond }) => {
9323
9419
  const provider = typeof params?.provider === "string" ? params.provider.trim() : "";
9324
9420
  const apiKey = typeof params?.apiKey === "string" ? params.apiKey.trim() : "";
9325
9421
  if (!provider)
9326
- return respond(false, { error: "provider is empty" });
9422
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "provider is empty" });
9327
9423
  if (!apiKey)
9328
- return respond(false, { error: "apiKey is empty" });
9424
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "apiKey is empty" });
9329
9425
  try {
9330
9426
  const catalog = await ensureCatalog(run ?? null);
9331
9427
  if (catalog.length > 0 && !catalog.some((e) => e.provider === provider)) {
9332
- return respond(false, { error: `unknown provider: ${provider}` });
9428
+ return respond(false, undefined, { code: "NOT_FOUND", message: `unknown provider: ${provider}` });
9333
9429
  }
9334
9430
  } catch {}
9335
9431
  try {
@@ -9350,13 +9446,13 @@ function registerProvidersGateway(api, state2) {
9350
9446
  writeConfig(nextCfg);
9351
9447
  respond(true, { provider, configured: true, profileId });
9352
9448
  } catch (err) {
9353
- respond(false, { error: err?.message ?? String(err) });
9449
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9354
9450
  }
9355
9451
  }, { scope: "operator.write" });
9356
9452
  api.registerGatewayMethod("agentlife.providers.delete", ({ params, respond }) => {
9357
9453
  const provider = typeof params?.provider === "string" ? params.provider.trim() : "";
9358
9454
  if (!provider)
9359
- return respond(false, { error: "provider is empty" });
9455
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "provider is empty" });
9360
9456
  try {
9361
9457
  const cfg = readConfig();
9362
9458
  const profiles = cfg?.auth?.profiles;
@@ -9395,14 +9491,14 @@ function registerProvidersGateway(api, state2) {
9395
9491
  }
9396
9492
  respond(true, { provider, configured: false });
9397
9493
  } catch (err) {
9398
- respond(false, { error: err?.message ?? String(err) });
9494
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9399
9495
  }
9400
9496
  }, { scope: "operator.write" });
9401
9497
  api.registerGatewayMethod("agentlife.providers.test", async ({ params, respond }) => {
9402
9498
  const provider = typeof params?.provider === "string" ? params.provider.trim() : "";
9403
9499
  const apiKey = typeof params?.apiKey === "string" ? params.apiKey.trim() : "";
9404
9500
  if (!provider)
9405
- return respond(false, { error: "provider is empty" });
9501
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "provider is empty" });
9406
9502
  if (!apiKey)
9407
9503
  return respond(true, { ok: false, error: "apiKey required — type a key before testing" });
9408
9504
  try {
@@ -9428,7 +9524,7 @@ function registerProvidersGateway(api, state2) {
9428
9524
  });
9429
9525
  respond(true, { models: deduped });
9430
9526
  } catch (err) {
9431
- respond(false, { error: err?.message ?? String(err) });
9527
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9432
9528
  }
9433
9529
  }, { scope: "operator.read" });
9434
9530
  api.registerGatewayMethod("agentlife.providers.health", ({ respond }) => {
@@ -9458,23 +9554,23 @@ function registerProvidersGateway(api, state2) {
9458
9554
  }
9459
9555
  respond(true, { probes });
9460
9556
  } catch (err) {
9461
- respond(false, { error: err?.message ?? String(err) });
9557
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9462
9558
  }
9463
9559
  }, { scope: "operator.read" });
9464
9560
  api.registerGatewayMethod("agentlife.providers.loginCodex", async ({ respond }) => {
9465
9561
  if (!run)
9466
- return respond(false, { error: "runCommand unavailable" });
9562
+ return respond(false, undefined, { code: "UNAVAILABLE", message: "runCommand unavailable" });
9467
9563
  try {
9468
9564
  const result = await run(["openclaw", "capability", "model", "auth", "login", "--provider", "openai-codex"], { timeoutMs: 180000 });
9469
9565
  const combined = `${result?.stdout ?? ""}
9470
9566
  ${result?.stderr ?? ""}`;
9471
9567
  const urlMatch = combined.match(/https:\/\/\S+/);
9472
9568
  if ((result?.code ?? 0) !== 0 && !urlMatch) {
9473
- return respond(false, { error: (result?.stderr ?? "").trim() || "codex login failed" });
9569
+ return respond(false, undefined, { code: "FAILED", message: (result?.stderr ?? "").trim() || "codex login failed" });
9474
9570
  }
9475
9571
  respond(true, { url: urlMatch?.[0] ?? null, completed: (result?.code ?? 0) === 0 });
9476
9572
  } catch (err) {
9477
- respond(false, { error: err?.message ?? String(err) });
9573
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9478
9574
  }
9479
9575
  }, { scope: "operator.write" });
9480
9576
  }
@@ -9510,13 +9606,13 @@ function registerModelsConfigGateway(api) {
9510
9606
  fallbacks: Array.isArray(m.fallbacks) ? m.fallbacks.filter((x) => typeof x === "string") : []
9511
9607
  });
9512
9608
  } catch (err) {
9513
- respond(false, { error: err?.message ?? String(err) });
9609
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9514
9610
  }
9515
9611
  }, { scope: "operator.read" });
9516
9612
  api.registerGatewayMethod("agentlife.models.defaults.setPrimary", ({ params, respond }) => {
9517
9613
  const model = typeof params?.model === "string" ? params.model.trim() : "";
9518
9614
  if (!model)
9519
- return respond(false, { error: "model is required" });
9615
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "model is required" });
9520
9616
  try {
9521
9617
  const cfg = readConfig();
9522
9618
  cfg.agents = cfg.agents ?? {};
@@ -9526,26 +9622,26 @@ function registerModelsConfigGateway(api) {
9526
9622
  writeConfig(cfg);
9527
9623
  respond(true, { ok: true });
9528
9624
  } catch (err) {
9529
- respond(false, { error: err?.message ?? String(err) });
9625
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9530
9626
  }
9531
9627
  }, { scope: "operator.write" });
9532
9628
  api.registerGatewayMethod("agentlife.models.defaults.setInternal", ({ params, respond }) => {
9533
9629
  const model = typeof params?.model === "string" ? params.model.trim() : "";
9534
9630
  if (!model)
9535
- return respond(false, { error: "model is required" });
9631
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "model is required" });
9536
9632
  try {
9537
9633
  const cfg = readPluginConfig2();
9538
9634
  cfg.internalModel = model;
9539
9635
  writePluginConfig2(cfg);
9540
9636
  respond(true, { ok: true });
9541
9637
  } catch (err) {
9542
- respond(false, { error: err?.message ?? String(err) });
9638
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9543
9639
  }
9544
9640
  }, { scope: "operator.write" });
9545
9641
  api.registerGatewayMethod("agentlife.models.fallbacks.set", ({ params, respond }) => {
9546
9642
  const models = Array.isArray(params?.models) ? params.models.filter((x) => typeof x === "string") : null;
9547
9643
  if (!models)
9548
- return respond(false, { error: "models must be an array of strings" });
9644
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "models must be an array of strings" });
9549
9645
  try {
9550
9646
  const cfg = readConfig();
9551
9647
  cfg.agents = cfg.agents ?? {};
@@ -9555,7 +9651,7 @@ function registerModelsConfigGateway(api) {
9555
9651
  writeConfig(cfg);
9556
9652
  respond(true, { ok: true });
9557
9653
  } catch (err) {
9558
- respond(false, { error: err?.message ?? String(err) });
9654
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9559
9655
  }
9560
9656
  }, { scope: "operator.write" });
9561
9657
  api.registerGatewayMethod("agentlife.agents.list", ({ respond }) => {
@@ -9569,23 +9665,23 @@ function registerModelsConfigGateway(api) {
9569
9665
  })).filter((a) => a.id !== null && a.name !== null);
9570
9666
  respond(true, { agents });
9571
9667
  } catch (err) {
9572
- respond(false, { error: err?.message ?? String(err) });
9668
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9573
9669
  }
9574
9670
  }, { scope: "operator.read" });
9575
9671
  api.registerGatewayMethod("agentlife.agents.setModel", ({ params, respond }) => {
9576
9672
  const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : "";
9577
9673
  if (!agentId)
9578
- return respond(false, { error: "agentId is required" });
9674
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "agentId is required" });
9579
9675
  const model = params?.model;
9580
9676
  if (model !== null && typeof model !== "string") {
9581
- return respond(false, { error: "model must be string or null" });
9677
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "model must be string or null" });
9582
9678
  }
9583
9679
  try {
9584
9680
  const cfg = readConfig();
9585
9681
  const list = Array.isArray(cfg?.agents?.list) ? cfg.agents.list : [];
9586
9682
  const idx = list.findIndex((a) => a?.id === agentId);
9587
9683
  if (idx < 0)
9588
- return respond(false, { error: `agent not found: ${agentId}` });
9684
+ return respond(false, undefined, { code: "NOT_FOUND", message: `agent not found: ${agentId}` });
9589
9685
  if (model === null) {
9590
9686
  delete list[idx].model;
9591
9687
  } else {
@@ -9594,19 +9690,19 @@ function registerModelsConfigGateway(api) {
9594
9690
  writeConfig(cfg);
9595
9691
  respond(true, { ok: true });
9596
9692
  } catch (err) {
9597
- respond(false, { error: err?.message ?? String(err) });
9693
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9598
9694
  }
9599
9695
  }, { scope: "operator.write" });
9600
9696
  api.registerGatewayMethod("agentlife.agents.setModel.bulk", ({ params, respond }) => {
9601
9697
  const updates = Array.isArray(params?.updates) ? params.updates : null;
9602
9698
  if (!updates)
9603
- return respond(false, { error: "updates must be an array" });
9699
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "updates must be an array" });
9604
9700
  for (const u of updates) {
9605
9701
  if (!u || typeof u.agentId !== "string" || !u.agentId.trim()) {
9606
- return respond(false, { error: "each update needs agentId (string)" });
9702
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "each update needs agentId (string)" });
9607
9703
  }
9608
9704
  if (u.model !== null && typeof u.model !== "string") {
9609
- return respond(false, { error: "each update model must be string or null" });
9705
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "each update model must be string or null" });
9610
9706
  }
9611
9707
  }
9612
9708
  try {
@@ -9627,7 +9723,7 @@ function registerModelsConfigGateway(api) {
9627
9723
  writeConfig(cfg);
9628
9724
  respond(true, { ok: true, applied });
9629
9725
  } catch (err) {
9630
- respond(false, { error: err?.message ?? String(err) });
9726
+ respond(false, undefined, { code: "INTERNAL", message: err?.message ?? String(err) });
9631
9727
  }
9632
9728
  }, { scope: "operator.write" });
9633
9729
  }
@@ -9818,13 +9914,25 @@ function register(api) {
9818
9914
  const serverUrl = typeof params?.serverUrl === "string" ? params.serverUrl.trim() : "";
9819
9915
  const apiKey = typeof params?.apiKey === "string" ? params.apiKey.trim() : "";
9820
9916
  if (!serverUrl || !apiKey) {
9821
- return respond(false, { error: "missing serverUrl or apiKey" });
9917
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "missing serverUrl or apiKey" });
9822
9918
  }
9919
+ let parsedUrl;
9823
9920
  try {
9824
- const { writeFileSync: writeFileSync12, mkdirSync: mkdirSync10 } = __require("node:fs");
9825
- mkdirSync10(path20.dirname(notifyConfigPath), { recursive: true });
9826
- writeFileSync12(notifyConfigPath, JSON.stringify({ serverUrl, apiKey }));
9827
- } catch {}
9921
+ parsedUrl = new URL(serverUrl);
9922
+ } catch {
9923
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "serverUrl must be a valid URL" });
9924
+ }
9925
+ if (parsedUrl.protocol !== "https:") {
9926
+ return respond(false, undefined, { code: "INVALID_REQUEST", message: "serverUrl must use https://" });
9927
+ }
9928
+ try {
9929
+ const { writeFileSync: writeFileSync12, mkdirSync: mkdirSync10, chmodSync: chmodSync4 } = __require("node:fs");
9930
+ mkdirSync10(path20.dirname(notifyConfigPath), { recursive: true, mode: 448 });
9931
+ writeFileSync12(notifyConfigPath, JSON.stringify({ serverUrl, apiKey }), { mode: 384 });
9932
+ chmodSync4(notifyConfigPath, 384);
9933
+ } catch (e) {
9934
+ return respond(false, undefined, { code: "INTERNAL", message: `failed to persist notification config: ${e?.message ?? "write error"}` });
9935
+ }
9828
9936
  respond(true, { registered: true });
9829
9937
  }, { scope: "operator.write" });
9830
9938
  }