@mono-agent/agent-runtime 0.18.1 → 0.18.3

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/MIGRATION.md CHANGED
@@ -17,6 +17,31 @@ the configuration schema.
17
17
 
18
18
  ---
19
19
 
20
+ ## 0.18.2
21
+
22
+ - **Normalized native-subagent activity:** Claude SDK/CLI and Codex app-server
23
+ children now emit the exact `subagent_activity` lifecycle. Treat
24
+ `subagent.id` as the parent attachment key, `nativeId` only as provider
25
+ correlation metadata, and `phase: "message"` as child prose rather than
26
+ parent answer text or a completed tool call.
27
+ - **Explicit native configuration trust:** Claude SDK filesystem settings stay
28
+ disabled unless `settingSources` opts into `user`, `project`, or `local`.
29
+ Those sources may execute hooks and plugins, not only load agents. Codex
30
+ repository instructions remain disabled unless `codexLoadProjectDocs` is
31
+ true; explicit app-server arguments remain authoritative.
32
+ - **Provider-owned Codex agents:** non-empty caller-defined `nativeSubagents`
33
+ teammates now fail direct Codex startup with `skipped_capability_mismatch` so
34
+ a router may continue to Claude. Do not synthesize `collaborationMode` or
35
+ assume Claude profile definitions are portable to Codex.
36
+ - **Per-attempt policy projection:** `resolveAttempt().policyOptions` may replace
37
+ only `allowedTools`, `disallowedTools`, and `permissionMode` for the active
38
+ route. General resolver `options` still cannot replace protected request
39
+ fields.
40
+ - **Pi inline helper ceiling:** the runtime-owned `general-purpose` profile is
41
+ limited to its read-only defaults intersected with `subagents.inline.allowedTools`.
42
+ An empty intersection disables that fallback profile rather than restoring
43
+ wider defaults.
44
+
20
45
  ## 0.15.2
21
46
 
22
47
  - **Tool-policy capability discovery:** built-in bridge capabilities now report
@@ -282,6 +307,13 @@ a `supports_native_subagents` requirement when a run passes
282
307
  native-subagent runs, ensure at least one entry supports native subagents, or the
283
308
  run reports exhausted instead of degrading silently.
284
309
 
310
+ Caller-defined `nativeSubagents.teammates` are a Claude-only projection. Codex
311
+ still advertises and reports its provider-owned native collaboration surface,
312
+ but a direct Codex attempt now rejects configured teammate/profile definitions
313
+ with `codex_native_subagent_definitions_unsupported` before transport; a router
314
+ may then continue to Claude. Use `codexLoadProjectDocs: true` to enable Codex's
315
+ repository instructions, not to define Codex collaboration profiles.
316
+
285
317
  ### 6. Diagnostics & internal behavior changes (no API change)
286
318
 
287
319
  - **Pi multimodal**: image inputs are delivered to the model as image content
package/README.md CHANGED
@@ -750,6 +750,9 @@ Per-call options (a non-exhaustive selection):
750
750
  | `cwd` | `string` | Working directory for the agent's tools. |
751
751
  | `allowedTools` | `string[]` | Built-in tool allowlist. Default: all. |
752
752
  | `disallowedTools` | `string[]` | Block list. |
753
+ | `nativeSubagents` | `object` | Caller-defined Claude native `Task` profiles. Direct Codex rejects configured teammate definitions because Codex owns its collaboration agents. |
754
+ | `settingSources` | `("user" \| "project" \| "local")[]` | Claude Agent SDK filesystem settings opt-in. Omitted/empty disables those three sources; Anthropic managed settings still apply. |
755
+ | `codexLoadProjectDocs` | `boolean` | Codex app-server repository-instruction opt-in. Omitted/false sets `project_doc_max_bytes=0`; true restores Codex defaults. Explicit `codexAppServerArgs` wins. |
753
756
  | `mcpServers` | `Record<string, McpServerConfig>` | Configured MCP servers (stdio / sse / http); on direct Codex, each forwarded server authorizes its own tool calls. |
754
757
  | `sandboxPolicy` | `SandboxPolicy` | Optional fail-closed sandbox policy for built-in tools and stdio MCP process startup. |
755
758
  | `webSearchConfig` | `{ backend?, endpoint? }` | Run-scoped local SearXNG/keyless WebSearch backend selection. |
@@ -789,6 +792,49 @@ second applied event. A throwing host `acknowledge` or `reject` callback cannot
789
792
  change the native steering outcome; the Codex bridge reports it as a bounded
790
793
  `live_input_callback_failed` runtime warning.
791
794
 
795
+ ### Provider-native subagents and project instructions
796
+
797
+ Claude SDK runs are filesystem-isolated by default: mono-agent passes
798
+ `settingSources: []`, which disables user, project, and local settings sources,
799
+ including their `CLAUDE.md`, hooks, plugins, and `.claude/agents` profiles.
800
+ Anthropic managed settings remain in force and may still configure hooks or
801
+ plugins; `settingSources` is not a managed-policy bypass. Opt into only the
802
+ needed sources, for example `settingSources: ["project"]`. User, project, and
803
+ local settings may execute configured hooks and plugins, so enable only trusted
804
+ settings and avoid opting in while running in an untrusted checkout. This
805
+ option is SDK only. The Claude Code CLI performs its own settings discovery,
806
+ and mono-agent does not pass it a `--setting-sources` value.
807
+
808
+ Codex app-server owns its native collaboration agents and their profiles. The
809
+ bridge observes and normalizes their lifecycle, but it does not synthesize a
810
+ `collaborationMode` payload or inject caller-defined `nativeSubagents`
811
+ teammates. A non-empty configured teammate list fails before app-server startup
812
+ with `skipped_capability_mismatch`, allowing a fallback router to continue to a
813
+ Claude route.
814
+
815
+ Codex app-server runs disable automatic repository-instruction discovery by
816
+ default with `project_doc_max_bytes=0`. Set `codexLoadProjectDocs: true` when
817
+ Codex and its own collaboration agents should load repository instructions. If
818
+ `codexAppServerArgs` is supplied, that explicit argument vector is authoritative
819
+ and `codexLoadProjectDocs` does not alter it.
820
+
821
+ Provider-native and in-process delegation share `subagent_activity` telemetry.
822
+ `subagent.id` is the canonical parent attachment key: the initiating parent
823
+ tool-use id whenever the provider exposes it, or a stable synthetic key for an
824
+ orphan lifecycle record. `nativeId` is an optional provider task/thread id for
825
+ correlation only and never replaces that key.
826
+ The normalized phases are `agent_started`, `started`, `completed`, `message`,
827
+ and `agent_completed`. A `message` belongs to the child and must not be treated
828
+ as parent answer text or as a completed tool call.
829
+
830
+ Restrictive allowlists must still authorize the delegation surface. Include
831
+ `Agent` for the in-process built-in. Claude-native teammate definitions add
832
+ `Task` to an explicit allowed list automatically; filesystem profiles enabled
833
+ only through `settingSources` require callers to include `Task` themselves.
834
+ An explicit deny still wins. Direct Codex remains an allow-all-only bridge, so
835
+ a named restrictive allowlist fails before provider startup rather than being
836
+ silently widened.
837
+
792
838
  Returns:
793
839
 
794
840
  ```ts
@@ -955,6 +1001,10 @@ Behaviour:
955
1001
  - `resolveAttempt` runs once per attempt — including every same-model retry — and
956
1002
  receives `{ attemptIndex, retryIndex }`, where `attemptIndex` stays the chain
957
1003
  index. Its `cleanup` runs after each attempt.
1004
+ - `resolveAttempt().policyOptions` is the narrow host seam for translating one
1005
+ logical tool policy into the active provider's representation. It may replace
1006
+ only `allowedTools`, `disallowedTools`, and `permissionMode`; the resolver's
1007
+ general `options` bag still cannot replace protected request fields.
958
1008
 
959
1009
  Chain entries can require backend capabilities via `requires: { structured_output: true, supports_mcp: true, ... }`; entries that don't satisfy the requirements are skipped (logged in `failoverHistory` as `failureKind: "skipped_capability_mismatch"`).
960
1010
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mono-agent/agent-runtime",
3
- "version": "0.18.1",
3
+ "version": "0.18.3",
4
4
  "description": "Agent runtime supporting Claude SDK/CLI, Codex, OpenCode, Pi SDK, and ACP v1 bridges out of the box",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-only",
@@ -84,7 +84,7 @@ State exactly what you want back ("return a bullet list of file:line and a one-l
84
84
  /**
85
85
  * @param {RuntimeSubagentsOptions} subagents
86
86
  * @param {ReadonlyArray<RuntimeSubagentDefinition>} definitions
87
- * @param {ReadonlyArray<string>|null} ceiling Tools an authored subagent may request, or null when authoring is off.
87
+ * @param {ReadonlyArray<string>|null} ceiling Tools available to the runtime-owned general-purpose or authored profiles, or null when authoring is off.
88
88
  * @returns {string}
89
89
  */
90
90
  function toolDescription(subagents, definitions, ceiling) {
@@ -309,7 +309,7 @@ export function createAgentTool(subagents, context = {}) {
309
309
  }
310
310
  const { profile, droppedTools } = authored
311
311
  ? buildInlineProfile(params, ceiling, names)
312
- : { profile: resolveProfile(definitions, params?.name), droppedTools: [] };
312
+ : { profile: resolveProfile(definitions, params?.name, ceiling), droppedTools: [] };
313
313
  if (profile === null) {
314
314
  const available = [...names, GENERAL_PURPOSE_SUBAGENT].join(", ");
315
315
  throw new Error(`Error: unknown subagent "${params?.name}". Available: ${available}.`);
@@ -482,7 +482,8 @@ export function createAgentTool(subagents, context = {}) {
482
482
  }
483
483
 
484
484
  /**
485
- * The tools an authored subagent may be granted, or null when authoring is off.
485
+ * The ceiling for the runtime-owned general-purpose helper and authored
486
+ * subagents, or null when authoring is off.
486
487
  *
487
488
  * A host that enables authoring without stating a ceiling gets the read-only
488
489
  * default rather than every built-in: the same reasoning as `normalizeProfile`,
@@ -548,14 +549,25 @@ function buildInlineProfile(params, ceiling, configuredNames) {
548
549
  /**
549
550
  * @param {ReadonlyArray<RuntimeSubagentDefinition>} definitions
550
551
  * @param {string|undefined} name
552
+ * @param {ReadonlyArray<string>|null} ceiling Tool ceiling for the runtime-owned general-purpose profile.
551
553
  * @returns {RuntimeSubagentDefinition|null}
552
554
  */
553
- function resolveProfile(definitions, name) {
555
+ function resolveProfile(definitions, name, ceiling) {
554
556
  if (name === undefined || name === null || name === GENERAL_PURPOSE_SUBAGENT) {
557
+ const allowedTools = ceiling === null
558
+ ? DEFAULT_SUBAGENT_TOOLS
559
+ : DEFAULT_SUBAGENT_TOOLS.filter((tool) => ceiling.includes(tool));
560
+ // `normalizeProfile` treats an empty allow-list as "use the read-only
561
+ // default", which would silently widen this runtime-owned helper past a
562
+ // parent-policy ceiling that contains only write-capable tools.
563
+ if (allowedTools.length === 0) {
564
+ throw new Error(`Error: no read-only tools available to ${GENERAL_PURPOSE_SUBAGENT} within this agent's subagent ceiling (${ceiling?.join(", ") || "none"}). Use a configured profile, or do this yourself.`);
565
+ }
555
566
  return normalizeProfile({
556
567
  name: GENERAL_PURPOSE_SUBAGENT,
557
568
  description: "Read-only researcher inheriting the main model.",
558
569
  systemPrompt: "You are a focused research subagent. Work only from the task you were given — you cannot see the parent conversation and cannot ask anyone anything. Investigate with the tools you have, then finish with a written answer in exactly the shape the task requested. Cite file:line where relevant. Never modify files.",
570
+ allowedTools,
559
571
  });
560
572
  }
561
573
  const found = definitions.find((definition) => definition.name === name);
@@ -1,14 +1,32 @@
1
1
  // @ts-check
2
2
 
3
3
  import { randomUUID } from "node:crypto";
4
+ import { readToolRuntime } from "./shared/runtime-context.js";
5
+ import { resolveSandboxPolicy } from "./shared/tool-context.js";
4
6
  import { performWebFetch } from "./web-fetch.js";
5
7
  import { performWebSearch } from "./web-search.js";
6
8
 
7
9
  const MAX_CACHE_ENTRIES = 64;
10
+ const MAX_SHARED_SEARCH_ENTRIES = 256;
11
+ const SHARED_SEARCH_TTL_MS = 15 * 60_000;
12
+
13
+ // Search results are shared process-wide rather than per model run. A run-scoped
14
+ // cache dies at the end of every turn and is invisible to sibling subagents, so
15
+ // an agent that keeps circling the same topic re-hits the network every time —
16
+ // which is exactly the request volume that gets keyless engines to rate-limit.
17
+ // One process is one agent instance, so the sharing stays inside one tenant.
18
+ //
19
+ // Only completed results are shared. In-flight promises stay per-controller on
20
+ // purpose: they carry the originating run's AbortSignal, and handing that to a
21
+ // sibling would let one turn's cancellation fail another turn's search.
22
+ /** @type {Map<string, {value: any, expiresAt: number}>} */
23
+ const sharedSearchCache = new Map();
8
24
 
9
25
  /**
10
26
  * One ephemeral web-tool controller for one model run. It owns in-memory
11
- * deduplication, result caches, anonymous browser namespaces, and cleanup.
27
+ * deduplication, the fetch result cache, anonymous browser namespaces, and
28
+ * cleanup. Search results are the exception: they live in the process-wide
29
+ * cache above so sibling subagents and later turns can reuse them.
12
30
  *
13
31
  * @param {{searchConfig?: any, fetchConfig?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, fetchImpl?: typeof fetch, browserRenderer?: any}} [options]
14
32
  */
@@ -22,7 +40,6 @@ export function createWebToolController({
22
40
  browserRenderer,
23
41
  } = {}) {
24
42
  const namespace = `mono-agent-web-${randomUUID()}`;
25
- const searchCache = new Map();
26
43
  const fetchCache = new Map();
27
44
  const searchInFlight = new Map();
28
45
  const fetchInFlight = new Map();
@@ -66,15 +83,59 @@ export function createWebToolController({
66
83
  }
67
84
  }
68
85
 
86
+ /**
87
+ * @param {string} key
88
+ * @param {() => Promise<any>} execute
89
+ */
90
+ async function cachedSearch(key, execute) {
91
+ if (closed) return closedResult();
92
+ const cached = readSharedSearch(key);
93
+ if (cached) return withCacheHit(cached);
94
+ const active = searchInFlight.get(key);
95
+ if (active) return withCacheHit(await active);
96
+ const task = Promise.resolve().then(execute);
97
+ searchInFlight.set(key, task);
98
+ try {
99
+ const result = await task;
100
+ // Failures stay uncached; the backend cooldown in web-search.js is what
101
+ // stops a rate-limited engine from being hammered again.
102
+ if (!result.error) writeSharedSearch(key, cloneResult(result));
103
+ return result;
104
+ } finally {
105
+ searchInFlight.delete(key);
106
+ }
107
+ }
108
+
69
109
  return {
70
110
  namespace,
71
111
 
72
112
  async search(params, execution = {}) {
73
- const key = stableKey(params);
74
- return cachedRun(searchCache, searchInFlight, key, async () => performWebSearch(params, {
113
+ // The key must pin the backend, the endpoint AND the network policy the
114
+ // search actually ran under. A params-only key was safe while the cache
115
+ // lived and died with one run; process-wide it would let controllers with
116
+ // different backends read each other's results.
117
+ //
118
+ // The policy has to be the RESOLVED one, computed exactly as
119
+ // performWebSearch computes it: the effective policy is the context
120
+ // policy merged with the request policy, so keying on the request half
121
+ // alone would let a run whose context denies network read entries a
122
+ // network-allowed run had populated. Resolved per call because
123
+ // readToolRuntime() is mutable process state.
124
+ //
125
+ // The snapshot is then handed to performWebSearch as the request policy
126
+ // rather than letting it re-resolve. Execution is deferred by a microtask
127
+ // and updateToolContext mutates the context in place by design, so a
128
+ // re-resolve could enforce a policy the key never described — caching a
129
+ // network-allowed result under a denied key. Merging is monotonic, so
130
+ // passing the snapshot back in means enforcement can only be at least as
131
+ // strict as the key claims.
132
+ const resolvedCtx = ctx ?? readToolRuntime();
133
+ const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
134
+ const key = stableKey({ params, searchConfig, policy });
135
+ return cachedSearch(key, async () => performWebSearch(params, {
75
136
  searchConfig,
76
- sandboxPolicy,
77
- ctx,
137
+ sandboxPolicy: policy,
138
+ ctx: resolvedCtx,
78
139
  fetchImpl,
79
140
  signal: execution.signal,
80
141
  }));
@@ -101,7 +162,9 @@ export function createWebToolController({
101
162
  const pending = [...cleanups];
102
163
  cleanups.clear();
103
164
  await Promise.allSettled(pending.map((cleanup) => Promise.resolve().then(cleanup)));
104
- searchCache.clear();
165
+ // The shared search cache deliberately outlives the controller; only
166
+ // run-owned state (browser namespaces, fetch results, in-flight work) is
167
+ // dropped here.
105
168
  fetchCache.clear();
106
169
  searchInFlight.clear();
107
170
  fetchInFlight.clear();
@@ -109,6 +172,33 @@ export function createWebToolController({
109
172
  };
110
173
  }
111
174
 
175
+ function readSharedSearch(key) {
176
+ const entry = sharedSearchCache.get(key);
177
+ if (!entry) return null;
178
+ if (Date.now() >= entry.expiresAt) {
179
+ sharedSearchCache.delete(key);
180
+ return null;
181
+ }
182
+ return entry.value;
183
+ }
184
+
185
+ function writeSharedSearch(key, value) {
186
+ sharedSearchCache.set(key, { value, expiresAt: Date.now() + SHARED_SEARCH_TTL_MS });
187
+ // Drop expired entries before falling back to insertion-order eviction, so a
188
+ // burst of stale keys cannot push a live one out.
189
+ for (const [entryKey, entry] of sharedSearchCache) {
190
+ if (Date.now() >= entry.expiresAt) sharedSearchCache.delete(entryKey);
191
+ }
192
+ while (sharedSearchCache.size > MAX_SHARED_SEARCH_ENTRIES) {
193
+ sharedSearchCache.delete(sharedSearchCache.keys().next().value);
194
+ }
195
+ }
196
+
197
+ /** Test hook: the shared cache is module state and would leak between cases. */
198
+ export function __resetSharedSearchCacheForTests() {
199
+ sharedSearchCache.clear();
200
+ }
201
+
112
202
  function stableKey(value) {
113
203
  return JSON.stringify(sortValue(value));
114
204
  }