Skip to content

feat(runnerhub): agent forge-call relay legs (RIG-2170) - #405

Open
rigel-mintaka wants to merge 2 commits into
mainfrom
compass-server-rig-2237-forge-relay
Open

feat(runnerhub): agent forge-call relay legs (RIG-2170)#405
rigel-mintaka wants to merge 2 commits into
mainfrom
compass-server-rig-2237-forge-relay

Conversation

@rigel-mintaka

Copy link
Copy Markdown
Contributor

Add the Runner->Server relay for agent-initiated forge calls, the structural sibling of the board/lifecycle/comms relay legs. Hub.RelayForgeCall resolves the relayed session_id to its bound agent account server-side and delegates to a ForgeCaller under that resolved caller, passing the session id through — the Runner asserts no account. Guard order is fail-closed, identical to the sibling legs: no ForgeCaller wired -> CodeUnavailable (before session resolution); an unbound session -> CodeNotFound (never a stale account, never the bootstrap admin); a tool-level failure rides back in-band on the ForgeCallResult error arm so a single failed call never tears down the transport.

ForgeCaller is defined here as the narrow seam the forge service (T4) implements; it is wired post-construction via Hub.SetForgeCaller (mirrors SetBoardCaller), breaking the hub<->service construction cycle. Gateway.Forge is the in-container agent's door: it maps the container to its one bound session and forwards RelayForgeCall, failing closed CodePermissionDenied before a session is bound (never a forward with an empty session id). The gateway threads a new ForgeRelay client through its Deps struct beside Lifecycle.

Hub tests (mirroring relay_board_test): nil-caller Unavailable for bound and unbound sessions, unbound NotFound, resolved-account attribution delivered to a fake ForgeCaller, in-band error passthrough, call_id echo. Gateway tests (mirroring lifecycle_test): unbound/empty-session PermissionDenied, verbatim session-id forward, nil-result CodeInternal.

Refs RIG-2237.

Co-authored-by: Matt Wilkinson matt@sealedsecurity.com

Add the Runner->Server relay for agent-initiated forge calls, the structural sibling of the board/lifecycle/comms relay legs. `Hub.RelayForgeCall` resolves the relayed session_id to its bound agent account server-side and delegates to a `ForgeCaller` under that resolved caller, passing the session id through — the Runner asserts no account. Guard order is fail-closed, identical to the sibling legs: no ForgeCaller wired -> CodeUnavailable (before session resolution); an unbound session -> CodeNotFound (never a stale account, never the bootstrap admin); a tool-level failure rides back in-band on the ForgeCallResult error arm so a single failed call never tears down the transport.

`ForgeCaller` is defined here as the narrow seam the forge service (T4) implements; it is wired post-construction via `Hub.SetForgeCaller` (mirrors SetBoardCaller), breaking the hub<->service construction cycle. `Gateway.Forge` is the in-container agent's door: it maps the container to its one bound session and forwards RelayForgeCall, failing closed CodePermissionDenied before a session is bound (never a forward with an empty session id). The gateway threads a new `ForgeRelay` client through its Deps struct beside Lifecycle.

Hub tests (mirroring relay_board_test): nil-caller Unavailable for bound and unbound sessions, unbound NotFound, resolved-account attribution delivered to a fake ForgeCaller, in-band error passthrough, call_id echo. Gateway tests (mirroring lifecycle_test): unbound/empty-session PermissionDenied, verbatim session-id forward, nil-result CodeInternal.

Refs RIG-2237.

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
@linear-code

linear-code Bot commented Aug 19, 2026

Copy link
Copy Markdown

RIG-2170

RIG-2237

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-server-rig-2237-forg.compass-eng-docs.pages.dev

Deployed from compass-server-rig-2237-forge-relay at f78a700.

…2237 review)

Review M1: the production gateway construction (runner/host.go) omitted the new Forge dep while AgentGateway.Forge is a now-live route (Gateway.Forge shadows the embedded Unimplemented handler). An in-container agent calling Forge would hit a nil ForgeRelay and nil-panic (fail-to-500) instead of the fail-closed behaviour the design mandates. Wire Forge: h.link.client into the Deps literal, exactly as the sibling Lifecycle leg is wired (h.link.client already satisfies ForgeRelay via RunnerServiceClient.RelayForgeCall).

Review M2: RelayForgeCall's success arm dereferenced the ForgeCaller's result without a nil check. The sibling board/lifecycle legs are immune to a (nil, nil) caller by construction (their internal executor always builds a fresh non-nil result); the forge leg calls the external ForgeCaller directly, so a nil result on the nil-error arm would nil-deref on the security-critical resolution edge. Guard it: a nil result is surfaced in-band as CodeInternal (errForgeNoResult) with the call_id echoed, never a panic.

Review L1: add TestRelayForgeCallNilResultIsInternalErrorInBand pinning the M2 guard (a bound session, fake caller returning (nil, nil), asserts an in-band internal error with call_id round-trip rather than a panic).

Refs RIG-2237.

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant