@ethosagent/core 0.4.0 → 0.4.2

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.d.ts CHANGED
@@ -326,6 +326,12 @@ interface AgentLoopConfig {
326
326
  * reports `CLARIFY_NO_SURFACE` and the agent falls back to plain prose.
327
327
  */
328
328
  clarifyBridge?: ClarifyBridge;
329
+ /**
330
+ * Per-personality MCP tool policy loaded from mcp.yaml. NOT part of
331
+ * PersonalityConfig (frozen schema). Passed through from wiring so
332
+ * AgentLoop can build per-tool MCP allowlists in filterOpts.
333
+ */
334
+ mcpPolicy?: _ethosagent_types.McpPolicy;
329
335
  /**
330
336
  * Optional request dump store. When provided, AgentLoop appends a full
331
337
  * record of each LLM request/response for offline analysis and debugging.
@@ -396,8 +402,15 @@ interface RunOptions {
396
402
  * Consumed once; does not persist across runs.
397
403
  */
398
404
  tierOverride?: _ethosagent_types.ModelTierName;
405
+ /** Opaque user id (from IdentityMap). When present, USER.md is read from `user:<userId>` scope. */
406
+ userId?: string;
399
407
  dryRun?: boolean;
400
408
  dryRunMaxToolCalls?: number;
409
+ /**
410
+ * Override the personality's toolset for this run. Used by cron to exclude
411
+ * the `cron` tool from cron-spawned sessions (recursion guard).
412
+ */
413
+ toolsetOverride?: string[];
401
414
  }
402
415
  declare class AgentLoop {
403
416
  private readonly llm;
@@ -434,6 +447,8 @@ declare class AgentLoop {
434
447
  private readonly requestDumpStore?;
435
448
  /** Phase 3 — team id stamped onto ToolContext when loop runs inside a team. */
436
449
  private readonly teamId?;
450
+ /** Per-personality MCP tool policy from mcp.yaml (NOT on PersonalityConfig). */
451
+ private readonly mcpPolicy?;
437
452
  /** Per-session accumulated spend in USD. Keyed by sessionKey. Reset via resetSessionCost(). */
438
453
  private readonly sessionCosts;
439
454
  /** FW-28 — per-session mtime registry. Keyed by sessionKey → (absPath → record). */
package/dist/index.js CHANGED
@@ -182,6 +182,28 @@ function resolveDowngradedTools(spec) {
182
182
  }
183
183
  var DOWNGRADE_REJECTION_MESSAGE = "Tool blocked: an `outputIsUntrusted` tool just read external content. Dangerous tools are paused for the next turn or two. Send a new user message to clear, or re-run after acknowledging the prior content.";
184
184
 
185
+ // ../safety/injection/src/prompt-sanitize.ts
186
+ var INJECTION_PATTERNS = [
187
+ /ignore\s+(all\s+)?(previous|prior|above)\s+instructions/i,
188
+ /disregard\s+(your|all|any|the)\s+(previous|prior|above|system|original)\s+/i,
189
+ /you\s+are\s+now\s+(a|an|the)\s+/i,
190
+ /forget\s+(everything|all)\s+(you|above|prior)/i,
191
+ /override\s+(your|all|the)\s+(instructions|rules|constraints|guidelines)/i,
192
+ /new\s+(system\s+)?prompt\s*:/i,
193
+ /\[SYSTEM\]/i,
194
+ /<\s*system\s*>/i
195
+ ];
196
+ function sanitize(content) {
197
+ const lines = content.split("\n");
198
+ const cleaned = lines.map((line) => {
199
+ if (INJECTION_PATTERNS.some((re) => re.test(line))) {
200
+ return "[line removed by injection guard]";
201
+ }
202
+ return line;
203
+ });
204
+ return cleaned.join("\n");
205
+ }
206
+
185
207
  // ../safety/injection/src/sanitize.ts
186
208
  var PLACEHOLDER = "[STRIPPED-TEMPLATE-TOKEN]";
187
209
  var TEMPLATE_TOKEN_PATTERNS = [
@@ -299,6 +321,7 @@ function defaultAlwaysDeny() {
299
321
  import { readFileSync } from "fs";
300
322
  var ENV_TO_REF = {
301
323
  ANTHROPIC_API_KEY: "providers/anthropic/apiKey",
324
+ AZURE_API_KEY: "providers/azure/apiKey",
302
325
  OPENAI_API_KEY: "providers/openai/apiKey",
303
326
  OPENROUTER_API_KEY: "providers/openrouter/apiKey",
304
327
  GEMINI_API_KEY: "providers/gemini/apiKey",
@@ -1560,6 +1583,7 @@ function validateRegistration(tool, personality) {
1560
1583
 
1561
1584
  // src/tool-registry.ts
1562
1585
  function needsBackends(caps) {
1586
+ if (!caps) return false;
1563
1587
  return !!(caps.network || caps.secrets || caps.storage || caps.fs_reach || caps.process || caps.attachments);
1564
1588
  }
1565
1589
  function mcpServerName(toolName) {
@@ -1568,11 +1592,22 @@ function mcpServerName(toolName) {
1568
1592
  }
1569
1593
  function passesFilter(entry, filterOpts) {
1570
1594
  if (!filterOpts) return true;
1571
- const { allowedMcpServers, allowedPlugins } = filterOpts;
1595
+ const { allowedMcpServers, allowedPlugins, allowedMcpTools } = filterOpts;
1596
+ const toolName = entry.tool.name;
1572
1597
  if (allowedMcpServers !== void 0) {
1573
- const server = mcpServerName(entry.tool.name);
1598
+ const server = mcpServerName(toolName);
1574
1599
  if (server !== void 0 && !allowedMcpServers.includes(server)) return false;
1575
1600
  }
1601
+ if (allowedMcpTools !== void 0) {
1602
+ const server = mcpServerName(toolName);
1603
+ if (server !== void 0) {
1604
+ const allowed = allowedMcpTools[server];
1605
+ if (allowed !== void 0) {
1606
+ const bareName = toolName.split("__").slice(2).join("__");
1607
+ if (!allowed.includes(bareName)) return false;
1608
+ }
1609
+ }
1610
+ }
1576
1611
  if (allowedPlugins !== void 0 && entry.pluginId !== void 0) {
1577
1612
  if (!allowedPlugins.includes(entry.pluginId)) return false;
1578
1613
  }
@@ -1859,6 +1894,8 @@ var AgentLoop = class {
1859
1894
  requestDumpStore;
1860
1895
  /** Phase 3 — team id stamped onto ToolContext when loop runs inside a team. */
1861
1896
  teamId;
1897
+ /** Per-personality MCP tool policy from mcp.yaml (NOT on PersonalityConfig). */
1898
+ mcpPolicy;
1862
1899
  /** Per-session accumulated spend in USD. Keyed by sessionKey. Reset via resetSessionCost(). */
1863
1900
  sessionCosts = /* @__PURE__ */ new Map();
1864
1901
  /** FW-28 — per-session mtime registry. Keyed by sessionKey → (absPath → record). */
@@ -1890,6 +1927,7 @@ var AgentLoop = class {
1890
1927
  if (config.watcher) this.watcher = config.watcher;
1891
1928
  if (config.clarifyBridge) this.clarifyBridge = config.clarifyBridge;
1892
1929
  if (config.requestDumpStore) this.requestDumpStore = config.requestDumpStore;
1930
+ if (config.mcpPolicy) this.mcpPolicy = config.mcpPolicy;
1893
1931
  this.contextEngines = config.contextEngines ?? new DefaultContextEngineRegistry();
1894
1932
  }
1895
1933
  /**
@@ -1998,11 +2036,20 @@ var AgentLoop = class {
1998
2036
  model: effectiveModel,
1999
2037
  source: modelSource
2000
2038
  };
2001
- const allowedTools = personality.toolset ?? void 0;
2039
+ const allowedTools = opts.toolsetOverride ?? personality.toolset ?? void 0;
2002
2040
  const allowedPlugins = personality.plugins ?? [];
2041
+ const mcpServers = this.mcpPolicy?.servers;
2042
+ const allowedMcpTools = mcpServers ? Object.fromEntries(
2043
+ Object.entries(mcpServers).filter(([, v]) => v.tools !== void 0 || v.enabled === false).map(([k, v]) => {
2044
+ if (v.enabled === false) return [k, []];
2045
+ const tools = v.tools;
2046
+ return [k, tools ?? []];
2047
+ })
2048
+ ) : void 0;
2003
2049
  const filterOpts = {
2004
2050
  allowedMcpServers: personality.mcp_servers ?? [],
2005
- allowedPlugins
2051
+ allowedPlugins,
2052
+ ...allowedMcpTools && Object.keys(allowedMcpTools).length > 0 ? { allowedMcpTools } : {}
2006
2053
  };
2007
2054
  await this.hooks.fireVoid(
2008
2055
  "session_start",
@@ -2027,7 +2074,7 @@ ${text}` : text;
2027
2074
  const activeMemory = personality.memory?.provider ? await this.memoryProviders.get(personality.memory.provider)?.(
2028
2075
  personality.memory.options
2029
2076
  ) ?? this.memory : this.memory;
2030
- const memScopeId = personality.memoryScope === "per-personality" ? `personality:${personality.id}` : "global";
2077
+ const memScopeId = `personality:${personality.id}`;
2031
2078
  const memCtx = {
2032
2079
  scopeId: memScopeId,
2033
2080
  sessionId,
@@ -2042,6 +2089,35 @@ ${text}` : text;
2042
2089
  memSnapshot = { entries: hits.map((h) => ({ key: h.key, content: h.content })) };
2043
2090
  }
2044
2091
  }
2092
+ const userScopeId = opts.userId ? `user:${opts.userId}` : void 0;
2093
+ if (userScopeId) {
2094
+ const userCtx = {
2095
+ scopeId: userScopeId,
2096
+ sessionId,
2097
+ sessionKey,
2098
+ platform: this.platform,
2099
+ workingDir: this.workingDir
2100
+ };
2101
+ const userEntry = await activeMemory.read("USER.md", userCtx);
2102
+ if (userEntry?.content.trim()) {
2103
+ const userSnapshot = {
2104
+ entries: [{ key: "USER.md", content: userEntry.content }]
2105
+ };
2106
+ if (memSnapshot) {
2107
+ memSnapshot = { entries: [...userSnapshot.entries, ...memSnapshot.entries] };
2108
+ } else {
2109
+ memSnapshot = userSnapshot;
2110
+ }
2111
+ }
2112
+ }
2113
+ if (memSnapshot) {
2114
+ memSnapshot = {
2115
+ entries: memSnapshot.entries.map((e) => ({
2116
+ key: e.key,
2117
+ content: sanitize(e.content)
2118
+ }))
2119
+ };
2120
+ }
2045
2121
  const promptCtx = {
2046
2122
  sessionId,
2047
2123
  sessionKey,
@@ -2447,8 +2523,8 @@ ${rendered.slice(-MEMORY_MAX_CHARS)}`;
2447
2523
  workingDir: this.workingDir,
2448
2524
  agentId: opts.agentId,
2449
2525
  personalityId: personality.id,
2450
- memoryScope: personality.memoryScope,
2451
2526
  memoryScopeId: memScopeId,
2527
+ ...userScopeId ? { userScopeId } : {},
2452
2528
  ...this.teamId !== void 0 && { teamId: this.teamId },
2453
2529
  ...opts.dryRun ? { dryRun: true } : {},
2454
2530
  currentTurn: turnCount,
@@ -2527,6 +2603,54 @@ ${rendered.slice(-MEMORY_MAX_CHARS)}`;
2527
2603
  continue;
2528
2604
  }
2529
2605
  const effectiveArgs = beforeResult.args ?? tc.args;
2606
+ const enabledError = checkMcpEnabled(this.mcpPolicy, tc.toolName);
2607
+ if (enabledError) {
2608
+ this.observability?.recordSafetyBlock({
2609
+ traceId,
2610
+ code: "tool_blocked",
2611
+ cause: enabledError
2612
+ });
2613
+ observe({ type: "tool_end", toolName: tc.toolName, ok: false });
2614
+ yield {
2615
+ type: "tool_end",
2616
+ toolCallId: tc.toolCallId,
2617
+ toolName: tc.toolName,
2618
+ ok: false,
2619
+ durationMs: 0,
2620
+ result: enabledError
2621
+ };
2622
+ prepped.push({
2623
+ toolCallId: tc.toolCallId,
2624
+ name: tc.toolName,
2625
+ args: effectiveArgs,
2626
+ rejected: enabledError
2627
+ });
2628
+ continue;
2629
+ }
2630
+ const rejectError = checkMcpRejectArgs(this.mcpPolicy, tc.toolName, effectiveArgs);
2631
+ if (rejectError) {
2632
+ this.observability?.recordSafetyBlock({
2633
+ traceId,
2634
+ code: "tool_blocked",
2635
+ cause: rejectError
2636
+ });
2637
+ observe({ type: "tool_end", toolName: tc.toolName, ok: false });
2638
+ yield {
2639
+ type: "tool_end",
2640
+ toolCallId: tc.toolCallId,
2641
+ toolName: tc.toolName,
2642
+ ok: false,
2643
+ durationMs: 0,
2644
+ result: rejectError
2645
+ };
2646
+ prepped.push({
2647
+ toolCallId: tc.toolCallId,
2648
+ name: tc.toolName,
2649
+ args: effectiveArgs,
2650
+ rejected: rejectError
2651
+ });
2652
+ continue;
2653
+ }
2530
2654
  const spanId = this.observability?.startSpan({
2531
2655
  traceId: traceId ?? "",
2532
2656
  kind: "tool_call",
@@ -3036,6 +3160,37 @@ function extractFilePath(args) {
3036
3160
  if (typeof a.cwd === "string" && a.cwd.length > 0) return a.cwd;
3037
3161
  return void 0;
3038
3162
  }
3163
+ function checkMcpRejectArgs(mcpPolicy, toolName, args) {
3164
+ const servers = mcpPolicy?.servers;
3165
+ if (!servers || !toolName.startsWith("mcp__")) return void 0;
3166
+ const firstSep = toolName.indexOf("__");
3167
+ const secondSep = toolName.indexOf("__", firstSep + 2);
3168
+ if (secondSep === -1) return void 0;
3169
+ const server = toolName.slice(firstSep + 2, secondSep);
3170
+ const bareTool = toolName.slice(secondSep + 2);
3171
+ const argRules = servers[server]?.reject_args?.[bareTool];
3172
+ if (!argRules) return void 0;
3173
+ const typedArgs = args;
3174
+ for (const [argName, forbiddenValues] of Object.entries(argRules)) {
3175
+ const value = typedArgs[argName];
3176
+ if (typeof value === "string" && forbiddenValues.includes(value)) {
3177
+ return `MCP policy: argument '${argName}' value '${value}' is rejected for tool '${bareTool}' on server '${server}'`;
3178
+ }
3179
+ }
3180
+ return void 0;
3181
+ }
3182
+ function checkMcpEnabled(mcpPolicy, toolName) {
3183
+ const servers = mcpPolicy?.servers;
3184
+ if (!servers || !toolName.startsWith("mcp__")) return void 0;
3185
+ const firstSep = toolName.indexOf("__");
3186
+ const secondSep = toolName.indexOf("__", firstSep + 2);
3187
+ if (secondSep === -1) return void 0;
3188
+ const server = toolName.slice(firstSep + 2, secondSep);
3189
+ if (servers[server]?.enabled === false) {
3190
+ return `MCP policy: server '${server}' is disabled for this personality`;
3191
+ }
3192
+ return void 0;
3193
+ }
3039
3194
  function describeSource(toolName, args) {
3040
3195
  if (!args || typeof args !== "object") return void 0;
3041
3196
  const a = args;