Repository navigation
Conversation
|
Assessment: Request Changes At Review Categories
Assumptions and rules checked that held
Reusing one backend shape and one validation path for both collections keeps this change small and easy to review. |
…whole-value Addresses the strands-agents#413 review. - `read_remote_mcp_servers` (the offline `policy generate-schema` path) now refuses a `[mcp.<name>]` that declares a backend, by name, instead of silently discovering its ORIGINAL host — which would read the wrong server's tools and resolve the phantom-only destination `secret` against that host. The re-route and its backend auth apply only at run time, in the egress gateway, which the offline command does not have. Documented in mcp.md#backends. - `a_remote_mcp_backend_reroutes_through_the_egress_path` now asserts `mcp_servers()` and `backend_dials()` by whole value, so a regression that classified the backend host or dialed a wrong port/scheme is caught (the previous `any`/per-field checks were not tight enough). - New `offline_schema_generation_refuses_a_re_routed_remote_mcp_server`.
|
Thanks — both findings addressed in Blocking ( Non-blocking (test tightness) — |
|
Assessment: Approve
Re-checked on 30d5bcc and held
Thanks for the quick, well-tested follow-up. |
…sal actionable Addresses the non-blocking strands-agents#413 follow-up. The backend refusal aborts the whole generate-schema run (there is no partial schema), so read the remote servers BEFORE the stdio discovery loop — the run now fails before spawning any stdio server rather than spawning them and discarding the result. The refusal message and mcp.md now say what to do: generate a schema from a box.toml without the re-routed server for the others, then author its mcp:call rules by hand.
|
Addressed the non-blocking follow-up in |
|
Assessment: Approve
Re-checked on b73c0a6 and held
|
|
Addressed the three additional follow-ups:
The #1 (full projection merge) and #2 (route-kind collapse) suggestions are left as-is with the reasoning on their threads. |
|
Assessment: Approve I re-reviewed Re-checked on 21c268b and held
|
|
Assessment: Approve
Re-checked on de9b417 and held
|
An operator can re-route an egress destination to a backend, but a remote (`type = "http"`) MCP server had no way to do the same. A box that must reach an MCP server through a gateway (for example an AgentCore gateway that signs with SigV4 and injects the server credential) could not express that: the backend fields existed only on `[egress.<name>]`, not on `[mcp.<name>]`. Accept `[[mcp.<name>.backends]]` on an `http` MCP entry and thread it through the existing egress backend path, so a re-routed MCP server reuses one config shape, one validation path, and one gateway dial. - `McpEntry::Http` carries `backends: Vec<EgressBackend>`; a `stdio` entry rejects the key as unknown (`serde(deny_unknown_fields)`). - The config fan-out builds the remote-MCP `EgressEntry` with the declared backend, so `backend_dials`, `backend_credentials`, and `mcp_servers` all key on the original destination host. Policy still judges the original host; the backend is never the judged resource. - Every egress backend refusal applies unchanged (one backend per destination, no path on a backend destination, an `aws://` auth needs an AWS backend host, a destination secret beside a backend must be `env://`). - `has_backend` already covers `remote_mcp`, so a re-routed MCP server selects the existing `BACKEND_RECORD_VERSION`; no new record version. Docs: `mcp.md` gains a Backends section with the keys and refusals; `egress.md` and `config.md` cross-reference it. `cargo test -p strands-box --all-features` green. New, in `record/config`: - `an_http_entry_carries_a_backend` — the key parses onto an http entry. - `a_stdio_entry_refuses_a_backends_key` — a stdio entry rejects it. - `a_remote_mcp_backend_reroutes_through_the_egress_path` — the dial target and credential vault key on the original host (fails if the fan-out drops the backend). - `a_remote_mcp_server_with_no_backend_does_not_reroute` — negative guard. - `a_remote_mcp_backend_stores_the_backend_record_version` — version selection. - `a_remote_mcp_backend_inherits_every_egress_refusal` — each egress refusal still fires for an MCP backend. Manually verified end to end with the real binary: a box declaring an MCP backend to an AgentCore gateway re-routed a live MCP `initialize` (SigV4 accepted, the gateway reached GitHub's MCP endpoint), and the box's telemetry decided `net:connect` and `mcp:call` on the original host, never the backend.
…ection ## Problem strands-agents#311 review (r4504995822): the backend credential path duplicated the ordinary one — `BackendProjection` mirrored `Projection`, and `backend_capabilities` restated the vault-open boilerplate. The root cause was that `workspace` read "no provisioned name" as "this route signs", which is false for a box-owned backend token (no name, no phantom, does not sign) — so backends needed a separate path to avoid being handed bogus signing placeholders. ## Solution - Add `EgressRoute::signs()` (true only for a `SignedInBoundary` placement). `workspace` now asks it directly, so a box-owned token is no longer mistaken for a signed route; its `(None, [])` arm places nothing instead of emitting `SIGNING_PLACEHOLDERS`. - Collapse `BackendProjection` into `Projection`; `backend_capabilities` returns `Projection`. - Extract `open_vault`, the fail-closed vault-open, so both projection paths share one home. - Keep the two projection functions: an entry's strip + box-owned routes must stay in ONE vault, so backends group per entry, while the ordinary path groups by provisioned variable. Collapsing the groupings would merge or split credentials, so that split stays, now documented. No interface moved: `RouteKind` stays `pub(crate)`, the public `RouteSpec` constructors are unchanged, and the stored record and wire are untouched. ## Tests `cargo test -p strands-box --all-features` green, including the `box_credentials` e2e suite and `a_signed_route_places_nothing_and_still_credentials_the_workload`, `a_backend_entry_opens_one_capability_and_provisions_only_its_strip_phantom`, `a_shared_variable_keeps_the_ordinary_phantom_and_drops_the_backend_duplicate`.
…whole-value Addresses the strands-agents#413 review. - `read_remote_mcp_servers` (the offline `policy generate-schema` path) now refuses a `[mcp.<name>]` that declares a backend, by name, instead of silently discovering its ORIGINAL host — which would read the wrong server's tools and resolve the phantom-only destination `secret` against that host. The re-route and its backend auth apply only at run time, in the egress gateway, which the offline command does not have. Documented in mcp.md#backends. - `a_remote_mcp_backend_reroutes_through_the_egress_path` now asserts `mcp_servers()` and `backend_dials()` by whole value, so a regression that classified the backend host or dialed a wrong port/scheme is caught (the previous `any`/per-field checks were not tight enough). - New `offline_schema_generation_refuses_a_re_routed_remote_mcp_server`.
…sal actionable Addresses the non-blocking strands-agents#413 follow-up. The backend refusal aborts the whole generate-schema run (there is no partial schema), so read the remote servers BEFORE the stdio discovery loop — the run now fails before spawning any stdio server rather than spawning them and discarding the result. The refusal message and mcp.md now say what to do: generate a schema from a box.toml without the re-routed server for the others, then author its mcp:call rules by hand.
… to 443 A backend `url` is not range-checked by `DestinationPattern::parse` the way a destination is, so `https://gw.example.com:65536` and `:not-a-port` silently became port 443. `target()` now reads the port text explicitly: an omitted port takes the scheme default, an unparseable or out-of-range one (and `:0`, and an empty `:`) fails at load. Adds `declared_port_str` (present-vs-absent, no parse) and three refusal cases to `a_backend_url_must_be_scheme_host_port_and_optional_prefix`.
…efix path limit - egress.md: `secret.inject` is refused beside a backend; add the destination-secret-beside-a-backend behavior (phantom-only, real variable may be unset, `secret.inject` refused, signed secret refused). - egress.md: document that a backend `url` with a path prefix refuses an `%2F`/`%5C` in the request path (a GitLab encoded-path case), and that a dedicated backend host with no prefix is the workaround; note the 1..=65535 port rule on `backends.url`. - decisions.md: a backend `aws://` signs under the service/region the auth states, not one "derived from the backend host" (the clause contradicted the explicit-scope requirement two lines down). - credentials README: a route is one of four kinds, not two — add `box_owned` and `strip_phantom`.
Review found the two comments implied a box-owned backend route flows through `workspace()`. It does not: `to_bindings()` skips a backend entry, so box-owned/strip routes open through `backend_capabilities`. The `(None, [])` arms handle only ordinary routes (the signed case), with the non-signing arm there for exhaustiveness. Comment-only.
de9b417 to
cf41e19
Compare
|
Assessment: Approve
Re-checked after the rebase and held
|
Description
Lets a remote (
type = "http") MCP server re-route to a backend, reusing the egress backendmechanism from #311. A box that must reach an MCP server through a gateway — for example an
AgentCore gateway that SigV4-signs and injects the server credential — can now express it:
Two commits:
feat(mcp): re-route a remote MCP server to a backend— accept[[mcp.<name>.backends]]on an
httpMCP entry and thread it through the existing egress backend path, so a re-routedMCP server reuses one config shape, one validation path, and one gateway dial.
McpEntry::Httpcarriesbackends: Vec<EgressBackend>; astdioentry rejects the key(
deny_unknown_fields).EgressEntrywith the declared backend, sobackend_dials,backend_credentials, andmcp_serversall key on the originaldestination host. Policy still judges
net:connect/mcp:callon the original host; thebackend appears only on the box-controlled wire. The backend is never a policy resource.
on a backend destination,
aws://needs explicitaws.service/aws.region, a destinationsecret beside a backend must be
env://,credsd://and plaintext-backend auth refused).has_backendalready chainsremote_mcp, so a re-routed MCP server selects the existingBACKEND_RECORD_VERSION; no new record version.refactor(box): make a route's signing explicit, unify credential projection— addressesa feat(egress): re-route a destination to a backend #311 review comment that was deferred to this follow-up. Adds
EgressRoute::signs()so thecredential projection no longer infers "this route signs" from an absent provisioned name (a
box-owned backend token also has no name and does not sign); collapses
BackendProjectionintoProjection; extracts a sharedopen_vault. The two projection functions stay separate onpurpose — a backend entry keeps its strip + box-owned routes in one vault, so it groups per
entry while the ordinary path groups by provisioned variable.
Interface freeze
The
backendskey onMcpEntry::Httpis an additive change to the frozenbox.tomlsurface,spec-approved before implementation. No other frozen surface moves:
RouteKindstayspub(crate),the public
RouteSpecconstructors are unchanged, and the stored record (BACKEND_RECORD_VERSION),broker protocol, CLI, and composed environment are untouched.
Not in scope
A reviewer-suggested collapse of the three
RouteKindvariants into one with optionalphantom/attach was deliberately declined: it would make the
(no attach, no phantom)invalidstate representable, which the credentials crate's "invalid states unrepresentable" rule forbids,
and would not reduce the request-leg complexity (three distinct binding tables remain).
Related Issues
Builds on #311 (egress destination re-route) and #342 (bare
aws://default credential chain).Type of Change
New feature
Testing
Verified end to end against a live AgentCore gateway: a proper MCP client (initialize →
notifications/initialized → tools/list → tools/call) completed a real
WebSearchtool call throughthe re-route, SigV4-signed with a bare
aws://. Box telemetry confirmednet:connectandmcp:callwere decided on the original host, never the backend.New tests (
crates/box/src/record/config/):an_http_entry_carries_a_backend,a_stdio_entry_refuses_a_backends_keya_remote_mcp_backend_reroutes_through_the_egress_path(dial target + credential vault key on theoriginal host)
a_remote_mcp_server_with_no_backend_does_not_reroutea_remote_mcp_backend_stores_the_backend_record_versiona_remote_mcp_backend_inherits_every_egress_refusal(each egress backend refusal via the MCP path)an_egress_wildcard_covering_an_mcp_backend_host_is_refused(cross-collection backend-host cover)B.1:
a_signed_route_places_nothing_and_still_credentials_the_workloadand the backend/credsdprojection tests still green (mutation-pinned behavior-preserving).
I ran the relevant suites (
cargo test -p strands-box --all-features,--test site_manifest)If I touched Rust, I ran
cargo fmtandcargo clippy --all-targets --all-features(0 owned warnings)Checklist
mcp.md,egress.md,config.md)Known limitation
policy generate-schemacannot discover a re-routed MCP server (the re-route and its backend auth apply only atruntime, in the gateway; the offline command has no gateway). It refuses a[mcp.<name>]with a backend by name, so author that server'smcp:callrules by hand. Full offline backend discovery (incl. SigV4) is a follow-up.