agentlife 2.6.32 → 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
 
@@ -3222,15 +3236,18 @@ function rescheduleFailedFollowup(state, row, error) {
3222
3236
  function buildFireInstruction(state, row) {
3223
3237
  const view2 = getSurface(state, row.surfaceId);
3224
3238
  const goalText = view2?.goal ?? (view2 ? extractTitleAndDetail({ lines: view2.lines }).title : null);
3225
- 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);
3226
3243
  const recentFires = lookupRecentFiredFollowups(state, row.surfaceId, 5);
3227
3244
  const prior = recentFires.filter((m) => m !== row.instruction);
3228
3245
  const priorBlock = prior.length > 0 ? `
3229
3246
  Prior followups on this surface — your next must not match any of these with parameters rotated:
3230
- ${prior.slice(0, 3).map((m) => ` • "${m}"`).join(`
3247
+ ${prior.slice(0, 3).map((m) => ` • "${sanitizeFire(m).slice(0, 300)}"`).join(`
3231
3248
  `)}` : "";
3232
3249
  return [
3233
- `Widget followup for ${row.surfaceId}${goalLabel}: ${row.instruction}`,
3250
+ `Widget followup for ${row.surfaceId}${goalLabel}: ${instructionSafe}`,
3234
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}`
3235
3252
  ].join(`
3236
3253
  `);
@@ -4163,6 +4180,7 @@ function runInstallBootstrap(home) {
4163
4180
  ["sessionsVisibility", cfg?.tools?.sessions?.visibility ?? null],
4164
4181
  ["agentToAgentEnabled", cfg?.tools?.agentToAgent?.enabled ?? null],
4165
4182
  ["gatewayBind", cfg?.gateway?.bind ?? null],
4183
+ ["gatewayMode", cfg?.gateway?.mode ?? null],
4166
4184
  ["hooksAllowConversationAccess", cfg?.plugins?.entries?.agentlife?.hooks?.allowConversationAccess ?? null],
4167
4185
  ["maxPingPongTurns", cfg?.session?.agentToAgent?.maxPingPongTurns ?? null],
4168
4186
  ["maxConcurrent", cfg?.agents?.defaults?.maxConcurrent ?? null]
@@ -6282,7 +6300,7 @@ function mintBootstrapToken() {
6282
6300
  const devicesDir = path11.join(resolveStateDir(), "devices");
6283
6301
  const bootstrapPath = path11.join(devicesDir, "bootstrap.json");
6284
6302
  if (!fs11.existsSync(devicesDir))
6285
- fs11.mkdirSync(devicesDir, { recursive: true });
6303
+ fs11.mkdirSync(devicesDir, { recursive: true, mode: 448 });
6286
6304
  const registry = fs11.existsSync(bootstrapPath) ? JSON.parse(fs11.readFileSync(bootstrapPath, "utf-8")) : {};
6287
6305
  registry[bootstrapToken] = {
6288
6306
  token: bootstrapToken,
@@ -6292,7 +6310,8 @@ function mintBootstrapToken() {
6292
6310
  },
6293
6311
  issuedAtMs: Date.now()
6294
6312
  };
6295
- fs11.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2));
6313
+ fs11.writeFileSync(bootstrapPath, JSON.stringify(registry, null, 2), { mode: 384 });
6314
+ fs11.chmodSync(bootstrapPath, 384);
6296
6315
  return bootstrapToken;
6297
6316
  }
6298
6317
  function registerWebApp(api) {
@@ -6962,22 +6981,29 @@ function registerWidgetPushTool(api, state2) {
6962
6981
  const trimmed = rawBlock.trim();
6963
6982
  if (!trimmed)
6964
6983
  continue;
6965
- const deleteMatch = trimmed.match(/^delete\s+(\S+)/);
6984
+ const deleteSids = [...rawBlock.matchAll(/^\s*delete\s+(\S+)/gm)].map((m) => m[1]);
6966
6985
  const surfaceMatch = rawBlock.match(/^surface\s+(\S+)/m);
6967
- const sid = deleteMatch?.[1] ?? surfaceMatch?.[1] ?? null;
6968
- 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) {
6969
6991
  filteredRawBlocks.push(rawBlock);
6970
6992
  continue;
6971
6993
  }
6972
- const owner = getAgentId(state2, sid);
6973
- if (owner && owner !== agentId) {
6974
- const action = deleteMatch ? "delete" : "push";
6975
- ownershipViolations.push({ surfaceId: sid, owner, action });
6976
- recordActivity(state2, "quality_warning", sessionKey, agentId, {
6977
- data: JSON.stringify({ issue: "surface_ownership_violation", surfaceId: sid, owner, attempt: agentId, action })
6978
- });
6979
- 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
+ }
6980
7004
  }
7005
+ if (violated)
7006
+ continue;
6981
7007
  filteredRawBlocks.push(rawBlock);
6982
7008
  }
6983
7009
  if (filteredRawBlocks.length === 0 && ownershipViolations.length > 0) {
@@ -6986,13 +7012,15 @@ function registerWidgetPushTool(api, state2) {
6986
7012
  }
6987
7013
  let finalBlocks = filteredRawBlocks;
6988
7014
  const confirmationViolations = [];
7015
+ const domainViolations = [];
6989
7016
  if (agentId === "agentlife-builder") {
6990
7017
  const CONFIRMATION_SUFFIXES = /-(?:created|ready|built|setup|done|welcome)$/i;
7018
+ const BUILDER_ALLOWED = /(?:^agentlife-builder-|^dismiss-alt-|-building$)/i;
6991
7019
  const kept = [];
6992
7020
  for (const block of filteredRawBlocks) {
6993
7021
  const deleteMatch = block.trim().match(/^delete\s+(\S+)/);
6994
- const surfaceMatch = block.match(/^surface\s+(\S+)/m);
6995
- 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;
6996
7024
  if (!sid || deleteMatch) {
6997
7025
  kept.push(block);
6998
7026
  continue;
@@ -7004,10 +7032,25 @@ function registerWidgetPushTool(api, state2) {
7004
7032
  });
7005
7033
  continue;
7006
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
+ }
7007
7043
  kept.push(block);
7008
7044
  }
7009
- if (confirmationViolations.length > 0 && kept.length === 0) {
7010
- 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
+ `) }] };
7011
7054
  }
7012
7055
  finalBlocks = kept;
7013
7056
  }
@@ -7128,6 +7171,10 @@ function registerWidgetPushTool(api, state2) {
7128
7171
  const detail = ownershipViolations.map((v) => `${v.surfaceId} (owner=${v.owner}, attempt=${v.action})`).join("; ");
7129
7172
  errors.push(buildOwnershipError(detail, true));
7130
7173
  }
7174
+ if (confirmationViolations.length > 0)
7175
+ errors.push(buildBuilderConfirmationError(confirmationViolations));
7176
+ if (domainViolations.length > 0)
7177
+ errors.push(buildBuilderDomainWidgetError(domainViolations));
7131
7178
  const summary = pushedSurfaceIds.length === 1 ? `Widget ${pushedSurfaceIds[0]} updated` : `${pushedSurfaceIds.length} widgets updated`;
7132
7179
  if (errors.length > 0) {
7133
7180
  return { content: [{ type: "text", text: `${summary}
@@ -7145,7 +7192,10 @@ function buildOwnershipError(detail, skipped = false) {
7145
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.`;
7146
7193
  }
7147
7194
  function buildBuilderConfirmationError(violations) {
7148
- 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.`;
7149
7199
  }
7150
7200
  function buildGoalChangedError(ids) {
7151
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.`;
@@ -7459,7 +7509,11 @@ function findSiblingAuthProfile(provider, targetAgentId) {
7459
7509
  function writeAgentAuthProfile(agentId, provider, match) {
7460
7510
  if (!match)
7461
7511
  return false;
7462
- 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
+ }
7463
7517
  fs13.mkdirSync(agentDir, { recursive: true });
7464
7518
  const credential = { ...match.profile };
7465
7519
  if (credential.type === "api_key" && typeof credential.key !== "string" && typeof credential.apiKey === "string") {
@@ -7483,6 +7537,10 @@ function registerAgentGateway(api, state2) {
7483
7537
  respond(false, undefined, { code: "INVALID_REQUEST", message: "missing agent id" });
7484
7538
  return;
7485
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)" });
7542
+ return;
7543
+ }
7486
7544
  if (!workspace) {
7487
7545
  respond(false, undefined, { code: "INVALID_REQUEST", message: "missing workspace path" });
7488
7546
  return;
@@ -7587,6 +7645,10 @@ function registerAgentGateway(api, state2) {
7587
7645
  respond(false, undefined, { code: "INVALID_REQUEST", message: "missing agent id" });
7588
7646
  return;
7589
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)" });
7650
+ return;
7651
+ }
7590
7652
  const provisionedIds = new Set(PROVISIONED_AGENTS.map((a) => a.id));
7591
7653
  if (provisionedIds.has(id)) {
7592
7654
  respond(false, undefined, { code: "FAILED", message: "cannot delete provisioned agent" });
@@ -8079,11 +8141,12 @@ function registerSurfacesGateway(api, state2) {
8079
8141
  if (guided) {
8080
8142
  const agentId2 = view2.agentId;
8081
8143
  if (agentId2 && state2.runCommand) {
8082
- const goalSnippet = view2.goal ? ` goal="${view2.goal}"` : "";
8083
- 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)}"` : "";
8084
8147
  const sessionKey = buildAgentSessionKey(agentId2);
8085
8148
  state2.pendingReactiveGuidance.set(sessionKey, DISMISS_ALTERNATIVES_GUIDANCE);
8086
- const message = `[system:dismiss-requested] surfaceId=${surfaceId}${goalSnippet}${reasonSnippet}`;
8149
+ const message = `[system:dismiss-requested] surfaceId=${sanitizeMeta(surfaceId)}${goalSnippet}${reasonSnippet}`;
8087
8150
  state2.runCommand([
8088
8151
  "openclaw",
8089
8152
  "gateway",
@@ -8140,8 +8203,19 @@ function registerSurfacesGateway(api, state2) {
8140
8203
  respond(false, undefined, { code: "INVALID_REQUEST", message: "missing surfaceId or event" });
8141
8204
  return;
8142
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}` });
8215
+ return;
8216
+ }
8143
8217
  const agentId = getAgentId(state2, surfaceId) ?? undefined;
8144
- const metadata = typeof params?.metadata === "string" ? params.metadata.trim() : undefined;
8218
+ const metadata = typeof params?.metadata === "string" ? params.metadata.trim().slice(0, 1024) : undefined;
8145
8219
  recordSurfaceEvent(state2, surfaceId, event, undefined, agentId, metadata);
8146
8220
  respond(true, { surfaceId, event });
8147
8221
  }, { scope: "operator.read" });
@@ -8189,11 +8263,15 @@ function registerSurfacesGateway(api, state2) {
8189
8263
  const agentId = view2?.agentId ?? null;
8190
8264
  const ctx = view2?.context;
8191
8265
  if (ctx && ctx.escalation === true && typeof ctx.target === "string") {
8192
- const target = ctx.target;
8193
- const sessionKey2 = buildAgentSessionKey(target);
8194
- console.log("[agentlife] action escalation: surface=%s routing to target=%s session=%s", surfaceId, target, sessionKey2);
8195
- respond(true, { surfaceId, agentId: target, sessionKey: sessionKey2 });
8196
- 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);
8197
8275
  }
8198
8276
  const origin = getOriginSessionKey(state2, surfaceId);
8199
8277
  let sessionKey;
@@ -8277,8 +8355,8 @@ function registerAutomationsGateway(api, state2) {
8277
8355
  return;
8278
8356
  }
8279
8357
  const surfaceId = typeof params?.surfaceId === "string" ? params.surfaceId.trim() : "";
8280
- const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : getAgentId(state2, surfaceId) ?? "";
8281
- 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);
8282
8360
  respond(true, { id, surfaceId, agentId, type, name, path: automationPath, status: "running" });
8283
8361
  } catch (err) {
8284
8362
  respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "register failed" });
@@ -8538,7 +8616,7 @@ function registerAdminGateway(api, state2) {
8538
8616
  snapshot = fromDb;
8539
8617
  }
8540
8618
  if (snapshot.length === 0) {
8541
- 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: [] });
8542
8620
  return;
8543
8621
  }
8544
8622
  respond(true, {
@@ -8681,7 +8759,7 @@ function registerAdminGateway(api, state2) {
8681
8759
  console.error("[agentlife] uninstall failed:", err);
8682
8760
  respond(false, undefined, { code: "INTERNAL", message: err?.message ?? "unknown error during uninstall" });
8683
8761
  }
8684
- });
8762
+ }, { scope: "operator.admin" });
8685
8763
  api.registerGatewayMethod("agentlife.config.get", ({ respond }) => {
8686
8764
  respond(true, readPluginConfig());
8687
8765
  }, { scope: "operator.read" });
@@ -8834,7 +8912,8 @@ function registerEvolutionsGateway(api, state2) {
8834
8912
  userNote
8835
8913
  };
8836
8914
  recordActivity(state2, "quality_warning", null, row.agentId, { data: JSON.stringify(payload) });
8837
- 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}".` : "";
8838
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.`;
8839
8918
  sendToInternalSession(state2, row.agentId, message, `user-reverted-${row.id}-${Date.now()}`);
8840
8919
  console.log("[agentlife:evolutions] restored %s/%s to version %d (userNote=%s)", row.agentId, row.filename, row.id, userNote ? "yes" : "no");
@@ -9837,11 +9916,23 @@ function register(api) {
9837
9916
  if (!serverUrl || !apiKey) {
9838
9917
  return respond(false, undefined, { code: "INVALID_REQUEST", message: "missing serverUrl or apiKey" });
9839
9918
  }
9919
+ let parsedUrl;
9840
9920
  try {
9841
- const { writeFileSync: writeFileSync12, mkdirSync: mkdirSync10 } = __require("node:fs");
9842
- mkdirSync10(path20.dirname(notifyConfigPath), { recursive: true });
9843
- writeFileSync12(notifyConfigPath, JSON.stringify({ serverUrl, apiKey }));
9844
- } 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
+ }
9845
9936
  respond(true, { registered: true });
9846
9937
  }, { scope: "operator.write" });
9847
9938
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlife",
3
- "version": "2.6.32",
3
+ "version": "2.6.33",
4
4
  "description": "Native mobile dashboard for OpenClaw — agents push live widgets, automations, and remote-access surfaces to your phone via the AgentLife plugin relay.",
5
5
  "keywords": [
6
6
  "openclaw",