@ethosagent/core 0.4.0 → 0.4.1

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 = [
@@ -1568,11 +1590,22 @@ function mcpServerName(toolName) {
1568
1590
  }
1569
1591
  function passesFilter(entry, filterOpts) {
1570
1592
  if (!filterOpts) return true;
1571
- const { allowedMcpServers, allowedPlugins } = filterOpts;
1593
+ const { allowedMcpServers, allowedPlugins, allowedMcpTools } = filterOpts;
1594
+ const toolName = entry.tool.name;
1572
1595
  if (allowedMcpServers !== void 0) {
1573
- const server = mcpServerName(entry.tool.name);
1596
+ const server = mcpServerName(toolName);
1574
1597
  if (server !== void 0 && !allowedMcpServers.includes(server)) return false;
1575
1598
  }
1599
+ if (allowedMcpTools !== void 0) {
1600
+ const server = mcpServerName(toolName);
1601
+ if (server !== void 0) {
1602
+ const allowed = allowedMcpTools[server];
1603
+ if (allowed !== void 0) {
1604
+ const bareName = toolName.split("__").slice(2).join("__");
1605
+ if (!allowed.includes(bareName)) return false;
1606
+ }
1607
+ }
1608
+ }
1576
1609
  if (allowedPlugins !== void 0 && entry.pluginId !== void 0) {
1577
1610
  if (!allowedPlugins.includes(entry.pluginId)) return false;
1578
1611
  }
@@ -1859,6 +1892,8 @@ var AgentLoop = class {
1859
1892
  requestDumpStore;
1860
1893
  /** Phase 3 — team id stamped onto ToolContext when loop runs inside a team. */
1861
1894
  teamId;
1895
+ /** Per-personality MCP tool policy from mcp.yaml (NOT on PersonalityConfig). */
1896
+ mcpPolicy;
1862
1897
  /** Per-session accumulated spend in USD. Keyed by sessionKey. Reset via resetSessionCost(). */
1863
1898
  sessionCosts = /* @__PURE__ */ new Map();
1864
1899
  /** FW-28 — per-session mtime registry. Keyed by sessionKey → (absPath → record). */
@@ -1890,6 +1925,7 @@ var AgentLoop = class {
1890
1925
  if (config.watcher) this.watcher = config.watcher;
1891
1926
  if (config.clarifyBridge) this.clarifyBridge = config.clarifyBridge;
1892
1927
  if (config.requestDumpStore) this.requestDumpStore = config.requestDumpStore;
1928
+ if (config.mcpPolicy) this.mcpPolicy = config.mcpPolicy;
1893
1929
  this.contextEngines = config.contextEngines ?? new DefaultContextEngineRegistry();
1894
1930
  }
1895
1931
  /**
@@ -1998,11 +2034,19 @@ var AgentLoop = class {
1998
2034
  model: effectiveModel,
1999
2035
  source: modelSource
2000
2036
  };
2001
- const allowedTools = personality.toolset ?? void 0;
2037
+ const allowedTools = opts.toolsetOverride ?? personality.toolset ?? void 0;
2002
2038
  const allowedPlugins = personality.plugins ?? [];
2039
+ const mcpServers = this.mcpPolicy?.servers;
2040
+ const allowedMcpTools = mcpServers ? Object.fromEntries(
2041
+ Object.entries(mcpServers).filter(([, v]) => v.tools !== void 0).map(([k, v]) => {
2042
+ const tools = v.tools;
2043
+ return [k, tools ?? []];
2044
+ })
2045
+ ) : void 0;
2003
2046
  const filterOpts = {
2004
2047
  allowedMcpServers: personality.mcp_servers ?? [],
2005
- allowedPlugins
2048
+ allowedPlugins,
2049
+ ...allowedMcpTools && Object.keys(allowedMcpTools).length > 0 ? { allowedMcpTools } : {}
2006
2050
  };
2007
2051
  await this.hooks.fireVoid(
2008
2052
  "session_start",
@@ -2027,7 +2071,7 @@ ${text}` : text;
2027
2071
  const activeMemory = personality.memory?.provider ? await this.memoryProviders.get(personality.memory.provider)?.(
2028
2072
  personality.memory.options
2029
2073
  ) ?? this.memory : this.memory;
2030
- const memScopeId = personality.memoryScope === "per-personality" ? `personality:${personality.id}` : "global";
2074
+ const memScopeId = `personality:${personality.id}`;
2031
2075
  const memCtx = {
2032
2076
  scopeId: memScopeId,
2033
2077
  sessionId,
@@ -2042,6 +2086,35 @@ ${text}` : text;
2042
2086
  memSnapshot = { entries: hits.map((h) => ({ key: h.key, content: h.content })) };
2043
2087
  }
2044
2088
  }
2089
+ const userScopeId = opts.userId ? `user:${opts.userId}` : void 0;
2090
+ if (userScopeId) {
2091
+ const userCtx = {
2092
+ scopeId: userScopeId,
2093
+ sessionId,
2094
+ sessionKey,
2095
+ platform: this.platform,
2096
+ workingDir: this.workingDir
2097
+ };
2098
+ const userEntry = await activeMemory.read("USER.md", userCtx);
2099
+ if (userEntry?.content.trim()) {
2100
+ const userSnapshot = {
2101
+ entries: [{ key: "USER.md", content: userEntry.content }]
2102
+ };
2103
+ if (memSnapshot) {
2104
+ memSnapshot = { entries: [...userSnapshot.entries, ...memSnapshot.entries] };
2105
+ } else {
2106
+ memSnapshot = userSnapshot;
2107
+ }
2108
+ }
2109
+ }
2110
+ if (memSnapshot) {
2111
+ memSnapshot = {
2112
+ entries: memSnapshot.entries.map((e) => ({
2113
+ key: e.key,
2114
+ content: sanitize(e.content)
2115
+ }))
2116
+ };
2117
+ }
2045
2118
  const promptCtx = {
2046
2119
  sessionId,
2047
2120
  sessionKey,
@@ -2447,8 +2520,8 @@ ${rendered.slice(-MEMORY_MAX_CHARS)}`;
2447
2520
  workingDir: this.workingDir,
2448
2521
  agentId: opts.agentId,
2449
2522
  personalityId: personality.id,
2450
- memoryScope: personality.memoryScope,
2451
2523
  memoryScopeId: memScopeId,
2524
+ ...userScopeId ? { userScopeId } : {},
2452
2525
  ...this.teamId !== void 0 && { teamId: this.teamId },
2453
2526
  ...opts.dryRun ? { dryRun: true } : {},
2454
2527
  currentTurn: turnCount,
@@ -2527,6 +2600,30 @@ ${rendered.slice(-MEMORY_MAX_CHARS)}`;
2527
2600
  continue;
2528
2601
  }
2529
2602
  const effectiveArgs = beforeResult.args ?? tc.args;
2603
+ const rejectError = checkMcpRejectArgs(this.mcpPolicy, tc.toolName, effectiveArgs);
2604
+ if (rejectError) {
2605
+ this.observability?.recordSafetyBlock({
2606
+ traceId,
2607
+ code: "tool_blocked",
2608
+ cause: rejectError
2609
+ });
2610
+ observe({ type: "tool_end", toolName: tc.toolName, ok: false });
2611
+ yield {
2612
+ type: "tool_end",
2613
+ toolCallId: tc.toolCallId,
2614
+ toolName: tc.toolName,
2615
+ ok: false,
2616
+ durationMs: 0,
2617
+ result: rejectError
2618
+ };
2619
+ prepped.push({
2620
+ toolCallId: tc.toolCallId,
2621
+ name: tc.toolName,
2622
+ args: effectiveArgs,
2623
+ rejected: rejectError
2624
+ });
2625
+ continue;
2626
+ }
2530
2627
  const spanId = this.observability?.startSpan({
2531
2628
  traceId: traceId ?? "",
2532
2629
  kind: "tool_call",
@@ -3036,6 +3133,25 @@ function extractFilePath(args) {
3036
3133
  if (typeof a.cwd === "string" && a.cwd.length > 0) return a.cwd;
3037
3134
  return void 0;
3038
3135
  }
3136
+ function checkMcpRejectArgs(mcpPolicy, toolName, args) {
3137
+ const servers = mcpPolicy?.servers;
3138
+ if (!servers || !toolName.startsWith("mcp__")) return void 0;
3139
+ const firstSep = toolName.indexOf("__");
3140
+ const secondSep = toolName.indexOf("__", firstSep + 2);
3141
+ if (secondSep === -1) return void 0;
3142
+ const server = toolName.slice(firstSep + 2, secondSep);
3143
+ const bareTool = toolName.slice(secondSep + 2);
3144
+ const argRules = servers[server]?.reject_args?.[bareTool];
3145
+ if (!argRules) return void 0;
3146
+ const typedArgs = args;
3147
+ for (const [argName, forbiddenValues] of Object.entries(argRules)) {
3148
+ const value = typedArgs[argName];
3149
+ if (typeof value === "string" && forbiddenValues.includes(value)) {
3150
+ return `MCP policy: argument '${argName}' value '${value}' is rejected for tool '${bareTool}' on server '${server}'`;
3151
+ }
3152
+ }
3153
+ return void 0;
3154
+ }
3039
3155
  function describeSource(toolName, args) {
3040
3156
  if (!args || typeof args !== "object") return void 0;
3041
3157
  const a = args;