LearnNewsExamplesServices
Frontmatter
id13106
titleHarness-embedded NL tool projection is client-asserted, not server-bound
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-grace
createdAtJun 13, 2026, 6:29 PM
updatedAtJun 13, 2026, 8:50 PM
githubUrlhttps://github.com/neomjs/neo/issues/13106
authorneo-opus-vega
commentsCount0
parentIssue13056
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 13, 2026, 8:50 PM

Harness-embedded NL tool projection is client-asserted, not server-bound

Closed v13.1.0/archive-v13-1-0-chunk-2 enhancementaiarchitecture
neo-opus-vega
neo-opus-vega commented on Jun 13, 2026, 6:29 PM

Context

V-B-A finding while verifying #13082 against live dev (HEAD 8e545f48b, post #13103/#13084 merge): the Neural Link harness tool-projection mechanism is fully implemented and enforced (#13084 shipped ToolService.isToolAllowedForProjection + the OpenAPI tier contract + POLICY_REFUSED on call_tool), but the projection mode is selected solely from client-supplied request metadata. An embedded agent that simply omits the flag receives the full developer/operator surface — including write-locked and admin tools. This is the "server-side connection-class binding leaf" @neo-gpt flagged as a split-out in the 2026-06-13 team-sunset handoff on #13012.

Release classification: post-release (harness security hardening leaf; non-blocking for v13).

The Problem

The read-only projection for embedded harness agents is opt-in by the client, so it is not a security boundary — it is a convention. The point of the curated tool surface (#13056) is that an embedded agent cannot reach write-locked/admin tools without an explicit capability gate. Today it can, by not asking for the projection.

This is the data-not-code / capability-security discipline the Body already enforces across the worker trust boundary (the App Worker can only emit JSON DOM-ops, never inject code): the surface an actor can reach must bind to what the actor is, not to what the actor claims.

The Architectural Reality

Verified from source on dev@8e545f48b:

  • ai/mcp/server/neural-link/Server.mjs:85-92buildToolProjectionContext({request}) derives the mode from request.params._meta.neoToolProjection; undefined → null.
  • ai/mcp/server/BaseServer.mjs:168-170 — the base buildToolProjectionContext returns null (full surface) by default; a projection is applied only when a non-null context is returned.
  • ai/mcp/ToolService.mjs:222-289getToolsForProjection(null) returns allToolsForListing (every tool); assertToolProjectionAllows(name, null) is a no-op. isToolAllowedForProjection is fail-closed for an unknown mode, but the mode itself is client-chosen, so omitting _meta never reaches that gate.
  • ai/mcp/server/neural-link/Server.mjs:128-151 — the NL server boots a stdio transport (single-tenant); BaseServer.buildRequestContext(reqAuth) is SSE-only (BaseServer.mjs:183-191). So there is no per-request connection identity to bind to in the shipping topology — the binding must live at the server-instance level.

Net: the read-only projection is bypassable by an embedded agent that does not send _meta.neoToolProjection.

The Fix

Bind the projection to the server instance, not the request:

  1. Add a server-instance "forced projection mode" knob — a resolved AiConfig leaf (read at the use-site per ADR 0019; never re-implemented/mutated) and/or a boot CLI flag (e.g. --tool-projection-mode harness-embedded).
  2. In buildToolProjectionContext (base + NL override), when a forced mode is set, return {mode: forcedMode} and ignore (or take the more-restrictive of) the client _meta. A client must never be able to widen its own surface.
  3. When no forced mode is set, preserve today's behavior (null → full surface) for trusted dev/operator-launched servers — backward compatible.
  4. The Fleet Manager (#13015) is the natural injector: when it spawns an embedded NL server for an embedded agent, it sets the forced mode at spawn (alongside the identity-env + wake-sub injection it already owns).

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Evidence
buildToolProjectionContext({request}) (BaseServer + NL Server) this leaf When server-instance forced mode is set, returns {mode: forcedMode} regardless of client _meta; client cannot widen the surface No forced mode → existing null/full-surface behavior (dev/operator) Unit: forced mode + client omitting _meta → only read tools listed; write-locked/admin call_toolPOLICY_REFUSED
Server-instance projection-mode config (new) ADR 0019 (AiConfig SSOT) + boot CLI Resolved leaf / CLI flag that pins the embedded NL server to harness-embedded Unset = full surface (back-compat) Unit: config/flag set → forced; unset → null
_meta.neoToolProjection (existing client hint, Server.mjs:85) #13084 Demoted to a narrowing-only hint; cannot escalate past the server-instance ceiling Server forced mode is the ceiling Test: client sending no _meta under forced mode still gets the restricted surface

Decision Record impact

aligned-with ADR 0020 (the curated harness tool surface is part of the harness concept) and aligned-with ADR 0019 (the projection-mode knob is a resolved AiConfig leaf, read at the use-site). No amend/supersede.

Acceptance Criteria

  • A server-instance forced projection mode exists (AiConfig leaf and/or boot flag), defaulting unset (full surface, back-compat).
  • When forced to harness-embedded, the NL server applies the read-only projection to every request regardless of client _metatools/list lists only read-tier tools; call_tool on a write-locked/admin tool returns POLICY_REFUSED.
  • A client cannot widen its surface above the server-instance ceiling (_meta is narrowing-only).
  • No forced mode set → existing dev/operator behavior preserved (full surface).
  • Tests prove the omitted-_meta bypass is closed under forced mode, and that the unset default is unchanged.

Out of Scope

  • Fleet-manager spawn-time injection of the forced mode (#13015 owns that; this leaf provides the knob it sets).
  • Per-connection / multi-tenant SSE auth binding (no SSE NL transport ships today; revisit when it does).
  • Topological Locking and the write-locked capability gate (#13056 siblings) — write-locked stays withheld here.
  • Changing the tier taxonomy or the OpenAPI contract (#13064).

Avoided Traps

  • Trusting client-asserted capability — rejected; capability binds to what the server instance is, not what the client claims.
  • Per-request auth binding — rejected for the shipping topology; stdio is single-tenant with no per-request identity. Server-instance binding is the correct grain.
  • Failing open when the mode config is missing at an embedded spawn — the embedded spawn path (#13015) must set the mode explicitly; unset=full-surface is preserved only for the trusted dev/operator launch.

Related

  • Parent: #13056 (Extended-NL coordination: curated tool surface)
  • Builds on: #13084 / PR #13103 (projection boundary + mechanism), #13064 / PR #13078 (tier contract)
  • Consumer/injector: #13015 (Fleet Manager MVP — spawn-time provisioning)
  • Sibling (closed; verified done by #13103): #13082
  • Concept: ADR 0020 (learn/agentos/decisions/0020-agent-harness-concept.md)

Live latest-open sweep: checked latest 25 open issues at 2026-06-13 (post-#13103 merge); only #13056 (parent) overlaps — no equivalent leaf. A2A in-flight sweep: turn-start list_messages (50, recency-bound) shows no [lane-claim]/[lane-intent] on this scope; #13082/#13084 released/merged.

Retrieval Hint: "harness-embedded neural link tool projection client-asserted server-instance forced mode bypass _meta neoToolProjection"

Verification anchor: dev@8e545f48b (#13103) — ai/mcp/ToolService.mjs, ai/mcp/server/BaseServer.mjs, ai/mcp/server/neural-link/Server.mjs