@mono-agent/agent-runtime 0.18.0 → 0.18.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/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
@@ -63,7 +88,23 @@ the configuration schema.
63
88
  authoritative and the runtime emits a bounded
64
89
  `live_input_callback_failed` warning.
65
90
 
66
- ## 0.18.x
91
+ ## 0.18.1
92
+
93
+ - ACP provider-session ids and session-list cursors are now confidential,
94
+ authenticated v2 handles. Hosts must persist one exact 32-byte binary
95
+ `acpSessionTokenKey` and pass it to ACP task runs, list/delete helpers, and
96
+ `validateAcpProviderSessionId(value, expectedProfileId, key)`. A changed or
97
+ missing key fails before profile resolution or process spawn. Existing v1
98
+ handles are rejected; discard them and obtain fresh v2 handles. Preserve the
99
+ complete returned value for resume, pagination, validation, and delete, but
100
+ do not compare ciphertexts for equality or parse/substitute the remote
101
+ agent's raw session id or cursor.
102
+ - Payload-bearing diagnostics from the pinned ACP SDK are scoped to the owned
103
+ ACP receive loop and reduced to content-free labels. Malformed or hostile
104
+ agent notifications cannot copy elicitation values or URL secrets into
105
+ process-wide console diagnostics.
106
+
107
+ ## 0.18.0
67
108
 
68
109
  - `acp:<profile-id>` is now a canonical runtime model reference when paired
69
110
  with `executionMode: "acp"`. Hosts must provide `resolveAcpProfile`; profiles
@@ -266,6 +307,13 @@ a `supports_native_subagents` requirement when a run passes
266
307
  native-subagent runs, ensure at least one entry supports native subagents, or the
267
308
  run reports exhausted instead of degrading silently.
268
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
+
269
317
  ### 6. Diagnostics & internal behavior changes (no API change)
270
318
 
271
319
  - **Pi multimodal**: image inputs are delivered to the model as image content
package/README.md CHANGED
@@ -615,19 +615,30 @@ transport frame is too structurally complex for the bounded host sanitizer,
615
615
  the turn fails explicitly as `provider_protocol` instead of emitting a partial
616
616
  tool, plan, or message event.
617
617
 
618
- ACP provider-session ids and list cursors are opaque, profile-bound runtime
619
- handles. Preserve them byte-for-byte and pass them back only to the matching
620
- high-level resume, list, validation, or delete operation; raw protocol session
621
- ids, cursors, and transport connections are private runtime state. Under the
622
- default `auto` recovery policy, the client prefers `session/resume`, then
623
- `session/load`, and finally a fresh session when neither capability is
624
- advertised. Explicit `resume` or `load` policies fail closed if missing. Stable
625
- usage comes from the latest typed `usage_update` notification; unstable
626
- `PromptResponse.usage` is ignored.
618
+ ACP provider-session ids and list cursors are confidential, authenticated v2
619
+ handles bound to their token kind and profile. The host must supply an exact
620
+ 32-byte binary `acpSessionTokenKey` for every task run, list, validation, and
621
+ delete operation. Call
622
+ `validateAcpProviderSessionId(handle, expectedProfileId, key)` at untrusted
623
+ ingress. Keep the key stable and secret across host restarts; changing it
624
+ invalidates every outstanding handle. Legacy `acp:v1:` and `acp-cursor:v1:`
625
+ values are rejected.
626
+
627
+ Preserve each returned handle byte-for-byte and pass it back only to the
628
+ matching high-level resume, list, validation, or delete operation. Encryption
629
+ uses a fresh nonce, so two handles for the same remote id are not equality
630
+ keys. Raw protocol session ids, cursors, token keys, and transport connections
631
+ remain private runtime state and are omitted from profile resolver context,
632
+ callbacks, and diagnostics. Under the default `auto` recovery policy, the
633
+ client prefers `session/resume`, then `session/load`, and finally a fresh
634
+ session when neither capability is advertised. Explicit `resume` or `load`
635
+ policies fail closed if missing. Stable usage comes from the latest typed
636
+ `usage_update` notification; unstable `PromptResponse.usage` is ignored.
627
637
 
628
638
  ### `createRuntime(host)`
629
639
 
630
- Pass host-level integration once at boot. All keys are optional.
640
+ Pass host-level integration once at boot. Keys are optional unless the selected
641
+ backend contract requires them.
631
642
 
632
643
  ```js
633
644
  createRuntime({
@@ -636,6 +647,7 @@ createRuntime({
636
647
  resolvePiApiKey, // async (provider) => string | undefined
637
648
  resolveAcpProfile, // async (profileId, context) => AcpProfileDescriptor
638
649
  onAcpInteractionRequest, // async permission/elicitation fallback callback
650
+ acpSessionTokenKey, // Uint8Array(32), required for ACP task/session-handle operations
639
651
  persistArtifact, // ({ filename, buffer, toolName, toolUseId }) => path | null
640
652
  onCompactionRecorded, // (compactionRow) => void — fired when the pi bridge
641
653
  // runs an automatic compaction (proactive or reactive
@@ -738,6 +750,9 @@ Per-call options (a non-exhaustive selection):
738
750
  | `cwd` | `string` | Working directory for the agent's tools. |
739
751
  | `allowedTools` | `string[]` | Built-in tool allowlist. Default: all. |
740
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. |
741
756
  | `mcpServers` | `Record<string, McpServerConfig>` | Configured MCP servers (stdio / sse / http); on direct Codex, each forwarded server authorizes its own tool calls. |
742
757
  | `sandboxPolicy` | `SandboxPolicy` | Optional fail-closed sandbox policy for built-in tools and stdio MCP process startup. |
743
758
  | `webSearchConfig` | `{ backend?, endpoint? }` | Run-scoped local SearXNG/keyless WebSearch backend selection. |
@@ -751,6 +766,7 @@ Per-call options (a non-exhaustive selection):
751
766
  | `onEvent` | `(event) => void` | Fired for every runtime event (assistant text, tool calls/results, applied live input, runtime warnings, structured output). |
752
767
  | `runId` | `string` | Tag this run for downstream callbacks (e.g. `onCompactionRecorded`). |
753
768
  | `providerSessionId` | `string` | Resume a prior provider session. |
769
+ | `acpSessionTokenKey` | `Uint8Array(32)` | Required for ACP task runs when not bound at `createRuntime()`; keep it secret and stable across restarts. |
754
770
  | `runArtifactDir` | `string` | Used by some providers as the Playwright MCP filename target. |
755
771
  | `codexAppServerCommand` | `string` | Override the Codex CLI binary. |
756
772
  | `codexAppServerArgs` | `string[]` | Override the Codex CLI arguments. |
@@ -776,6 +792,49 @@ second applied event. A throwing host `acknowledge` or `reject` callback cannot
776
792
  change the native steering outcome; the Codex bridge reports it as a bounded
777
793
  `live_input_callback_failed` runtime warning.
778
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
+
779
838
  Returns:
780
839
 
781
840
  ```ts
@@ -942,6 +1001,10 @@ Behaviour:
942
1001
  - `resolveAttempt` runs once per attempt — including every same-model retry — and
943
1002
  receives `{ attemptIndex, retryIndex }`, where `attemptIndex` stays the chain
944
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.
945
1008
 
946
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"`).
947
1010
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mono-agent/agent-runtime",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
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
  }