feat(host-resources): inbound resource handlers + resolver (phase 2a)#263
Merged
Conversation
mgoldsborough
added a commit
that referenced
this pull request
May 21, 2026
…ement (QA round 5 on #263) ## Critical fixes QA round 5 found three real bugs in the Phase 2a wiring: 1. **manage_app install bypass.** `lifecycle.installNamed/Local/Remote` were threaded but have zero production callers — the agent's actual install path is `system-tools` → `workspace-ops` → `startBundleSource`, which I added the param to but never threaded from the caller. Fix: `Runtime.getBundleMcpDeps(wsId)` getter, threaded through both `installBundleInWorkspaceViaCtx` (manage_app install) and `installConnectorBundle` (connector install). 2. **isTextMime byte-for-byte duplicated** with my own "keep in sync" comment between `src/host-resources/resolver.ts` and `src/tools/platform/files.ts`. Extracted to `src/files/mime.ts` — one source of truth, both paths import. The next person who adds `application/jsonl` updates one file, both surfaces pick it up. 3. **Boot-reload regression risk.** `workspace-runtime.ts:247` called `startBundleSource` without `bundleMcp`. Bundles installed with host-resources support would silently lose the capability across every platform restart. Threaded through via a `getBundleMcpDeps` factory in `startWorkspaceBundles` opts. ## Polish (all five QA suggestions) - Rate-limit error code: `-32603 InternalError` → `-32004` (impl-defined server-error range). Rate limiting is a deliberate quota response, not a server fault. - `list()` now rejects non-empty `cursor` with `-32602` instead of silently ignoring it — pagination isn't supported in v1, fail loudly so bundle SDKs can detect. - `composeBundleMcpContext` exported from `startup.ts` and reused in `lifecycle.installRemote`'s direct `new McpSource(...)` site. Drops 8 lines of duplicated four-field composition. - FileStore memoization in the Runtime factory closure — bounded by active workspace count, future-proofs against stateful FileStore. - Comment on `TokenBucketRateLimit.buckets` non-eviction. ## Code style — extensible quality-control system QA also pointed out that I'd used `import("path").TypeName` inline-type- imports in my Phase 2a edits (matching a pre-existing pattern in `runtime.ts`). The shortcut reads exactly like a runtime dynamic `import()` and trips every reader. Rather than just fix my edits, this PR introduces a structural fix: - `CODE_STYLE.md` — extensible doc for project-specific rules beyond what Biome / tsc catch. Each rule documents anti-example, good example, rationale, detection, override. - `scripts/check-code-style.ts` — AST-based check (uses TypeScript's `ImportTypeNode` kind) so it can't false-positive on real runtime dynamic imports. Wired into `verify:static` as `check:code-style`. - AGENTS.md gains a one-line pointer; the rest of the convention text lives in `CODE_STYLE.md`. - Refactored all 40 existing inline-type-import usages across 13 files to top-level `import type { X } from "..."`. Zero violations remain; the check passes. New violations fail CI from now on. The framework is set up so future rules (no-magic-numbers, etc.) land as one new function in `check-code-style.ts` + one section in `CODE_STYLE.md`, with their cleanup PR. ## Verify 3041 unit + 482 integration + 17 smoke green. `check:code-style` runs clean.
Bundle subprocesses can now call `ai.nimblebrain/resources/read` and
`ai.nimblebrain/resources/list` over their MCP transport. Phase 1
advertised the capability; Phase 2a wires the inbound handlers and the
resolver behind them. The capability is now functional end-to-end.
## New modules
- `src/host-resources/resolver.ts` — `HostResourcesResolver` interface
+ `FileBackedHostResourcesResolver` impl. Single chokepoint for
scheme allowlist, workspace isolation, size cap, and audit logging.
Resolves through the same workspace `FileStore` as agent-side
`files__read` — one security surface, not two.
- `src/host-resources/rate-limit.ts` — `TokenBucketRateLimit` per
`(workspaceId, bundleId)`. Defaults: 100 req/sec, 1000 burst.
Throws `-32603 Rate limited` with `retryAfterMs` in error data.
- `src/host-resources/methods.ts` — the two method-name constants.
## McpSource wiring
- `BundleMcpContext` (`workspaceId`, `bundleId`, `hostResources`,
`rateLimit`) threads through optional 4th constructor arg.
- `registerBundleHandlers` registers `setRequestHandler` for the
namespaced read/list schemas, called immediately after `new Client()`
(and again after the OAuth-retry `buildClient()`) so handlers are
ready before connect. URI-scheme schemas extend the standard
`ReadResourceRequestSchema` / `ListResourcesRequestSchema` with the
method literal swapped, so result shapes carry through unchanged
from the spec — Layer 3 migration is `s/ai.nimblebrain\///`.
- In-process platform sources pass no context (they don't need the
surface; they're the platform talking to itself).
## Runtime + lifecycle threading
- `Runtime.start()` constructs one resolver (closure captures the
workspace-scoped file-store factory) and one rate-limit, passes the
pair to `BundleLifecycleManager.setBundleMcpDepsFactory(wsId =>
...)`.
- Lifecycle's `installNamed` / `installLocal` / `installRemote` resolve
per-workspace deps from the factory and pass via
`startBundleSource` opts (or directly for `installRemote`'s
one-off `new McpSource` site).
- `workspace-ops.installBundleInWorkspace` accepts `bundleMcp` in opts
for the agent-facing `manage_app install` path; system-tools calls
pass it through.
- `system-tools.configureBundle`, connector eager-start, and boot
reload are out of scope for Phase 2a and tracked as follow-ups.
## Tests
29 new unit tests across three files:
- `host-resources-resolver.test.ts` (9) — text + blob round-trip,
scheme guard rejects `entities://`, unknown file ID returns -32002,
cross-workspace lookup collapses to -32002 (no info leak),
`maxReadSize` enforcement, list filtering by mime, scheme filter
validation.
- `host-resources-rate-limit.test.ts` (8) — burst admission,
per-`(wsId, bundleId)` isolation, refill at `ratePerSec`, ceiling
cap, `McpError` shape with `retryAfterMs`.
- `host-resources-handlers.test.ts` (2) — end-to-end wire test using
`InMemoryTransport.createLinkedPair`. A fake bundle server's tool
calls back through `server.request("ai.nimblebrain/resources/read",
...)`, the platform-side `setRequestHandler` dispatches to the
resolver, the file content makes it back to the bundle. Second
variant: unknown file ID propagates through as an `isError: true`
tool result. Closes the QA round-4 "no integration test" item
without spawning a real subprocess.
3041 unit + 482 integration + 17 smoke green.
…ement (QA round 5 on #263) QA round 5 found three real bugs in the Phase 2a wiring: 1. **manage_app install bypass.** `lifecycle.installNamed/Local/Remote` were threaded but have zero production callers — the agent's actual install path is `system-tools` → `workspace-ops` → `startBundleSource`, which I added the param to but never threaded from the caller. Fix: `Runtime.getBundleMcpDeps(wsId)` getter, threaded through both `installBundleInWorkspaceViaCtx` (manage_app install) and `installConnectorBundle` (connector install). 2. **isTextMime byte-for-byte duplicated** with my own "keep in sync" comment between `src/host-resources/resolver.ts` and `src/tools/platform/files.ts`. Extracted to `src/files/mime.ts` — one source of truth, both paths import. The next person who adds `application/jsonl` updates one file, both surfaces pick it up. 3. **Boot-reload regression risk.** `workspace-runtime.ts:247` called `startBundleSource` without `bundleMcp`. Bundles installed with host-resources support would silently lose the capability across every platform restart. Threaded through via a `getBundleMcpDeps` factory in `startWorkspaceBundles` opts. - Rate-limit error code: `-32603 InternalError` → `-32004` (impl-defined server-error range). Rate limiting is a deliberate quota response, not a server fault. - `list()` now rejects non-empty `cursor` with `-32602` instead of silently ignoring it — pagination isn't supported in v1, fail loudly so bundle SDKs can detect. - `composeBundleMcpContext` exported from `startup.ts` and reused in `lifecycle.installRemote`'s direct `new McpSource(...)` site. Drops 8 lines of duplicated four-field composition. - FileStore memoization in the Runtime factory closure — bounded by active workspace count, future-proofs against stateful FileStore. - Comment on `TokenBucketRateLimit.buckets` non-eviction. QA also pointed out that I'd used `import("path").TypeName` inline-type- imports in my Phase 2a edits (matching a pre-existing pattern in `runtime.ts`). The shortcut reads exactly like a runtime dynamic `import()` and trips every reader. Rather than just fix my edits, this PR introduces a structural fix: - `CODE_STYLE.md` — extensible doc for project-specific rules beyond what Biome / tsc catch. Each rule documents anti-example, good example, rationale, detection, override. - `scripts/check-code-style.ts` — AST-based check (uses TypeScript's `ImportTypeNode` kind) so it can't false-positive on real runtime dynamic imports. Wired into `verify:static` as `check:code-style`. - AGENTS.md gains a one-line pointer; the rest of the convention text lives in `CODE_STYLE.md`. - Refactored all 40 existing inline-type-import usages across 13 files to top-level `import type { X } from "..."`. Zero violations remain; the check passes. New violations fail CI from now on. The framework is set up so future rules (no-magic-numbers, etc.) land as one new function in `check-code-style.ts` + one section in `CODE_STYLE.md`, with their cleanup PR. 3041 unit + 482 integration + 17 smoke green. `check:code-style` runs clean.
76bce05 to
0bed1b6
Compare
…drift (QA round 6 on #263) ## Critical fixes QA round 6 found two real bugs: 1. **Rate-limit error code drift in docs.** Round 5 changed the implementation from `-32603 InternalError` to `-32004` (impl-defined server-error range — semantically correct for rate limiting), but four doc surfaces still said `-32603`. A bundle SDK author matching on `error.code === -32603` to detect rate limiting would silently fail to back off. Updated `rate-limit.ts` docstring, `mcp-source.ts` handler comment, the test file header, and the test now asserts on `caught?.code === -32004` so future drift fails CI rather than silently shifting the contract. 2. **`ensureSourceRegistered` bypass.** Composio OAuth reconnect path (`composio-auth.ts` → `lifecycle.ts:1454`) re-spawned the bundle via `startBundleSource` without `bundleMcp`, silently dropping host-resources handlers on every reconnect. One-line fix: thread `this.resolveBundleMcpDeps(wsId)` through opts. ## Polish - **Third `isTextMime` copy in `src/files/extract.ts`** — round 5 only caught two of three. The third had stricter semantics (no `text/*` wildcard, no parameter stripping), but param stripping already happens at the call site (`mimeType.split(";", 1)[0]` upstream). Swap to the shared `mime.ts`; minor behavior change — `text/sql`, `text/css`, and other un-curated `text/*` mimes are now treated as extractable. Strict improvement: those bytes ARE text. If extraction ever fails the function still returns null via the existing catch. - **List handler filter-fallback comment was lying.** Code only reads `params._meta?.filter` but comment promised a top-level fallback for forward-compat. Trimmed the comment to match reality plus a "if the spec ever adds `filter` to top-level, accept it here" note. 3041 unit (assert-on-code addition) + 482 integration + 17 smoke green.
… code + doc honesty (QA round 7 on #263) ## Critical fixes QA round 7 found two real bugs: 1. **3 respawn paths drop host-resources handlers.** Same pattern as `ensureSourceRegistered` (round 6): credential-change respawn paths re-spawn the bundle via `startBundleSource` without `bundleMcp`, silently dropping handlers. Threaded through all three: - `system-tools.configureBundle` (configure restart) — threaded via `ManageBundleContext.bundleMcpDepsFactory` - `connector-tools.ts:1289` (Composio install eager-start) — `ctx.runtime.getBundleMcpDeps(wsId)` - `connector-tools.ts:2391` (`respawnBundleAfterCredentialChange`) — same The PR-body argument ("no current consumer depends on host-resources") held for synapse-research today (env-only), but the moment a Phase-2b bundle uses both user_config and host-resources, set_user_config on it would silently disable host-resources until the next platform restart. Closing now while threading is fresh. 2. **False config-claim in rate-limit docstring.** Promised operators `nimblebrain.json`'s `hostResources.rateLimit` block — but no schema exists and `Runtime.start()` calls `new TokenBucketRateLimit()` with no args. Operators reading the comment would set a config block that does nothing. Stripped the false claim; the defaults remain hard-coded and operator-tunable rate-limit is a tracked follow-up. ## Polish - **`ResponseTooLarge` error code inconsistency.** Rate limiting moved to `-32004` in round 6 with explicit rationale ("a deliberate quota response, not a server fault"). Size-cap was still `-32603 InternalError` — same kind of deliberate quota response. Moved to `-32005` (impl-defined server-error range). Test pinned via `expect(caught?.code).toBe(-32005)` so future drift fails CI. - **CHANGELOG note on `files__read` text-extraction broadening.** The shared `isTextMime` from `mime.ts` (round 5 consolidation) treats any `text/*` mime as UTF-8-decodable; the old `extract.ts` predicate did exact-match on a 9-mime allowlist. Practical effect documented: `text/sql`, `text/css`, etc. now extract from `files__read` instead of returning null. The decode falls back safely on garbage bytes. ## Wire test deferral QA flagged adding LIST + rate-limit cases to the wire test (`InMemoryTransport`-driven dispatch via `host-resources-handlers.test.ts`). Skipping — standalone unit tests cover both behaviors and the dispatch shape is identical to READ. A 20-line addition is the kind of belt-and-suspenders that naturally lands with Phase 2c's synapse-research adoption. ## Verify 2991 unit + 482 integration + 17 smoke green. `check:code-style` clean.
…e test (QA round 8 on #263) ## Critical fix Stale "non-wired follow-ups" claim in CHANGELOG. Round 7 wired all three respawn paths (`configureBundle`, Composio eager-start, `respawnBundleAfterCredentialChange`) plus boot reload from round 5, but the CHANGELOG bullet still listed them as follow-ups. Same code- drift pattern as the rate-limit error code drift in round 6. Rewrote to reflect reality: every production bundle-spawn path threads `bundleMcp`. ## Polish - **`resolver.list` defensive validation of `filter.tags`.** A buggy bundle sending `{ tags: "single-tag" }` (string) instead of `{ tags: ["single-tag"] }` (string array) would throw TypeError on `.every`. Treat non-array as no filter — `Array.isArray` guard at the unwrap. Low blast radius (one bundle, one rate-limited request), but the fix is one line. - **LIST wire test added** (`host-resources-handlers.test.ts`). The read wire test proves dispatch works through `setRequestHandler`, but the list handler has additional logic — it unwraps `params._meta.filter` from a non-standard location (spec `ListResourcesRequest` doesn't carry `filter` at top level). The resolver-level list tests prove the resolver works, but not that the bundle's `_meta.filter` reaches the resolver intact. A future refactor of the param parse could silently swap filtered for unfiltered results — the new test catches that. ## Verify 2992 unit (+1 new wire test) + 482 integration + 17 smoke green. `check:code-style` clean. We are converging — round 8's "critical" is a doc-drift, not a bug.
…tags wire test (QA round 9 on #263) ## Critical fix `HostResourcesReadCapability.maxSize` JSDoc in `capability.ts:32` promised `-32603 ResponseTooLarge`, but round 7 changed the implementation to `-32005`. Same drift pattern flagged in rounds 6 and 8 — Phase 1's forward-looking promise turned into a lie the moment Phase 2's wire code shipped. Bundle SDK authors reading the type declaration to understand the contract would write retry logic against a code the wire never returns. One-line fix. ## Polish - **Log on read failures before the `-32002` collapse.** The cross-tenant-info-leak protection is correct — every store error collapses to "Resource not found" so a workspace can't probe another workspace's inventory. But that also masks disk I/O / permission / corruption errors from operators. Added a `log.warn` before the throw that surfaces the underlying error message with bundle + workspace context. Wire response unchanged. - **Wire test for `_meta.filter.tags`.** The round-8 mimeType wire test covered the generic `_meta.filter` unwrap, but the handler's cast in `mcp-source.ts` also includes `tags?: string[]`. A future refactor that narrows the cast (drops `tags` for "simpler typing") would pass the mimeType test silently and break tags-based filtering. New test seeds two tagged files, calls list with `_meta.filter.tags: ["draft"]`, asserts only the tagged one comes back. ## Deferred (with concurrence) - **Bucket map eviction on the rate limiter.** QA acknowledged "acceptable today" — bounded by installed-bundles × active- workspaces, restart is the pressure-release valve. Lives naturally with the operator-tunable `hostResources.rateLimit` follow-up. ## Verify 2993 unit (+1 new tags wire test) + 482 integration + 17 smoke green. `check:code-style` clean. Round 9's "critical" is doc drift on a JSDoc line. Convergence continues.
… of returning all files (QA round 10 on #263) Round 8's defensive Array.isArray guard prevented a TypeError when a buggy bundle sent `tags: "draft"` (string) instead of `tags: ["draft"]` (string array). But the fallback returned ALL files (no filter applied) — contradicting the comment that promised an empty-result debug signal. A bundle author would see their filter "not working" and might or might not connect the dots. Aligning with the scheme-filter branch (which throws `-32602 InvalidParams` on malformed input): throw the same code with `receivedType` in error data. Same shape, same actionable diagnostic. The bundle author gets an immediate explicit error instead of silently-wrong results. Considered alternatives: - Return empty list. Matches the original comment claim but is still misleading — empty vs invalid input look the same on the wire. - Drop the comment to match silent fallback. Rejected — `-32602` is strictly better than "you have to figure out why all files came back." New resolver test pins the behavior. Wire test for the same path is covered by round-9's `_meta.filter.tags` test. 2994 unit (+1 new validation test) + 482 integration + 17 smoke green.
This was referenced May 22, 2026
mgoldsborough
added a commit
that referenced
this pull request
May 22, 2026
…ase 2b) (#268) * feat(bundle-sdk-py): Python SDK for ai.nimblebrain/host-resources (phase 2b) Bundle-side wrapper for the host-resources extension shipped in PR #263. Bundle authors write `await host(ctx).read("files://fl_abc")` instead of hand-rolling the JSON-RPC call shape. ## New package: `nimblebrain-bundle-sdk` Lives at `packages/bundle-sdk-py/`. First top-level `packages/` directory in this monorepo. Python 3.11+, fastmcp >=3.0, mcp >=1.27. Standard hatchling/ruff/ty/pytest layout matching synapse-research conventions. ## API - `host(ctx)` — factory returning a `HostResources` handle wrapping the bundle's `Context`. - `HostResources.available` — capability probe. Reads `ClientCapabilities.extensions["ai.nimblebrain/host-resources"]` (the spec-blessed location), falls back to `experimental` for older platforms. Returns False on any malformed shape so a buggy host can't crash the bundle's probe. - `HostResources.supports_scheme(scheme)` — per-scheme allowlist check. - `HostResources.read(uri)` — wraps `ai.nimblebrain/resources/read`, returns the MCP-standard `ReadResourceResult`. - `HostResources.list(*, scheme=, mime_type=, tags=)` — wraps `ai.nimblebrain/resources/list` with the platform's `_meta.filter` unwrap convention. - `HostCapabilityMissing` exception — supports the Level-C fallback pattern (catch + return a structured tool error teaching the agent to retry with inline content). - Error-code constants (`RATE_LIMITED = -32004`, `RESPONSE_TOO_LARGE = -32005`, etc.) so bundles match on names, not magic numbers. ## Implementation notes - `ServerSession.send_request` is typed against `ServerRequest`, a closed Pydantic `RootModel` union of the spec's known methods. Our custom-method requests aren't in that union. At runtime, `send_request` only calls `request.model_dump()` — any Pydantic model with the right shape works. We pass typed BaseModel subclasses and `cast` them to `ServerRequest` at the call site (static-type accommodation only). - `_meta` field name has a leading underscore (MCP spec convention). Pydantic forbids leading-underscore attribute names; we use `Field(alias="_meta")` with `populate_by_name=True` so the wire shape stays correct while the Python attribute is `meta`. ## Tests 14 tests, all passing. Covers: - Capability detection: extensions, experimental fallback, read disabled, malformed shape. - Scheme allowlist probes. - `read` dispatch shape (method name + params). - `list` dispatch shape: no filter → empty params; mime_type filter → `_meta.filter.mimeType`; tags filter → `_meta.filter.tags`; combined filters. - `HostCapabilityMissing` raised locally without wire call. ## Release tooling `.github/workflows/release-bundle-sdk-py.yml` triggers on `bundle-sdk-py/v*` tags (distinct from the platform's `v*`). Verifies the tag matches `pyproject.toml`'s version, builds wheel + sdist via `uv build`, publishes to PyPI via Trusted Publishing (OIDC — no long-lived API token), creates a GitHub Release. First-time PyPI setup (one-time per package, by an operator) is documented in the workflow header. ## Platform verify `bun run verify` green — the new `packages/` directory doesn't interact with the platform build path. Static checks, unit, integration, smoke all clean. * fix(bundle-sdk-py): clean up misleading config + widen CI lint (QA round 11 on #268) Three real issues from QA round 11: ## Polish - **`_HostResourcesListFilter`'s `populate_by_name = True` was dead config + lying docstring.** The class declared the Pydantic config and the comment promised snake_case ↔ camelCase aliasing — but no `Field(alias=...)` was ever wired. Serialization actually went through a hand-rolled `to_wire()` dict-build. Removed the dead config and rewrote the docstring to describe what the code actually does: explicit dict-build is simpler than alias plumbing for an internal model whose only caller is `HostResources.list`. - **`supports_scheme` defensive branch was untested.** The `isinstance(schemes, list)` guard in `host.py:146` protects the bundle from a host that advertises `schemes` as a non-list. New test mirrors `test_available_false_on_malformed_shape` — pins the guard so a future "simplification" fails CI. - **CI lint scope didn't include `tests/`.** The release workflow ran `ruff check src/` only, which let an I001 import-order violation in `tests/test_host.py` ship to QA. Widened to `src/ tests/` for both lint and format-check. Release-time lint failures block a publish, so catching scope drift on PR is strictly better. ## Verify 15 unit (+1 new defensive test) + ruff clean on both src/ and tests/. * fix(bundle-sdk-py): per-method capability gates + mcp upper bound (QA round 12 on #268) ## Per-method capability gates (suggestion 1) `HostResources.list()` previously gated on `available`, which only reads `cap["read"]["enabled"]`. A future host that advertises `list.enabled=true` with `read.enabled=false` would have caused `list()` to falsely raise HostCapabilityMissing — the SDK was silently baking in v1's lockstep assumption. Fix: - Add `_method_enabled(method)` helper that gates per-method. - `read()` gates on `_method_enabled("read")`. - `list()` gates on `_method_enabled("list")`. - `available` (the common-case probe for `read`) stays as-is but docstring now explicitly says it's the read-only probe. - New `list_available` property mirrors `available` for the list path. v1 hosts move both flags in lockstep so it's a no-op today, but the shape allows independent flags and the SDK should follow the shape, not the v1 convention. ## mcp upper bound (suggestion 3) The `cast(ServerRequest, request)` shim in host.py relies on `BaseSession.send_request` calling only `.model_dump()` on the input — behavior verified against mcp 1.27.x but not contractually guaranteed. A 2.0 release could plausibly tighten input validation and break us at runtime (not import). Pinned `mcp>=1.27.0,<2.0.0` and documented the contract dependency inline at the cast site + on the dep itself. ## Suggestion 2 (docstring drift) Naturally resolved by suggestion 1's per-method gates: each method's docstring now precisely names the flag it checks (`read.enabled` vs `list.enabled`). ## Tests 21 passing (was 15, +6 new): - list_available reflects list.enabled independently of read.enabled - available reflects read.enabled independently of list.enabled - list_available defaults False when no caps - list_available False on malformed list block - list() raises when only read.enabled - read() raises when only list.enabled Lint + format clean on src/ and tests/. --------- Co-authored-by: Mathew Goldsborough <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Targets
main(Phase 1 / #262 merged; this branch rebased on top).Summary
Phase 1 advertised the
ai.nimblebrain/host-resourcescapability and gated installs onhost_capabilities[k].required. Phase 2a makes the capability functional: bundle subprocesses can now callai.nimblebrain/resources/readandai.nimblebrain/resources/listover their MCP transport, and the platform resolves the URIs against the workspaceFileStoreshared with the agent'sfiles__read.Closes the gap that motivated the host-resources extension: bundles like synapse-research can now reach workspace files without filesystem access, without going through the agent for every byte, without bypassing the platform's workspace isolation.
What changes
New modules
src/host-resources/resolver.ts—HostResourcesResolverinterface +FileBackedHostResourcesResolverimpl. Single chokepoint for scheme allowlist, workspace isolation, size cap, andhost-resourcesdebug logging. Resolves through the sameFileStoreas agent-sidefiles__read.src/host-resources/rate-limit.ts—TokenBucketRateLimitper(workspaceId, bundleId). Defaults: 100 req/s, 1000 burst. Throws-32004Rate limited (impl-defined server-error range) withretryAfterMsin error data.src/host-resources/methods.ts— namespaced method-name constants.src/files/mime.ts— sharedisTextMimepredicate. Single source of truth across the agent-sidefiles__read, the bundle-side resolver, and the file extraction pipeline.McpSourcewiringbundleContext: BundleMcpContextcarrying{ workspaceId, bundleId, hostResources, rateLimit }. In-process platform sources don't pass it.registerBundleHandlersregistersclient.setRequestHandlerfor the two namespaced read/list schemas immediately afternew Client()and afterbuildClient()(OAuth retry rebuild). Schemas extend the standardReadResourceRequestSchema/ListResourcesRequestSchemawith the method literal swapped — result shapes carry through unchanged from spec.Runtime + threading
Runtime.start()constructs one resolver and one rate-limit, exposes them viagetBundleMcpDeps(wsId), hands them toBundleLifecycleManager.setBundleMcpDepsFactory()andManageBundleContext.bundleMcpDepsFactory.bundleMcp: lifecycleinstallNamed/installLocal/installRemote,installBundleInWorkspace(agent-facingmanage_app install),configureBundlerestart, Composio eager-start, post-credential-change respawn,ensureSourceRegistered, and boot reload.Code-style infrastructure (carried over from earlier rounds)
CODE_STYLE.md+scripts/check-code-style.tswired intoverify:static. Rule Support version pinning for bundles in nimblebrain.json #1: no inline type imports. AST-based detection (no regex false-positives on real dynamic imports).Key design decisions
-32002 Resource not found(same response as a missing ID — no inventory enumeration).-32603 InternalError. Rate limiting →-32004, response-too-large →-32005. Bundle SDKs can distinguish deliberate quota from server fault.host_capabilitiesmirrorsClientCapabilities.extensionsexactly.Tests
host-resources-resolver.test.ts(9) — text/blob round-trip, scheme guard, unknown ID →-32002, cross-workspace lookup collapses to-32002(no info leak), size cap (asserts-32005), list filtering, scheme filter validation.host-resources-rate-limit.test.ts(8) — burst admission, per-key isolation, refill, assertion oncode === -32004so future drift fails CI.host-resources-handlers.test.ts(4) — end-to-end wire tests viaInMemoryTransport.createLinkedPair. READ dispatches through resolver and back to bundle; LIST exercises the_meta.filterunwrap end-to-end for bothmimeTypeandtags; unknown ID propagates asisError: true.2994 unit + 482 integration + 17 smoke, all green.
check:code-styleclean.Out of scope (follow-ups)
hostResources.rateLimitconfig. The defaults are conservative (100/sec, 1000 burst). No operator has needed an override.log.warn + continue; the proper fail-closed needs cache repopulation first. Separate from the wiring question (which is done).read.range = false,write.enabled = false. The capability shape reserves them.Test plan
bun run verifygreen locally.bun run check:code-stylefinds zero inline-type-imports).