---
id: call/from-call-forwarded-for
name: Wire from_call forwarding handler to populate forwarded_for and use peer-keyed registration (ADR-029 §5, ADR-032)
status: completed
depends_on: [call/operation-context-forwarded-for, call/peer-composite-env]
scope: narrow
risk: low
impact: component
level: implementation
---

## Description

Update the `from_call` forwarding handler to populate `forwarded_for` on the
`call.requested` payload it constructs, and update `from_call` registration to
use the peer-keyed overlay model. Per ADR-029 §5 (peer-keyed registration,
collision rule change) and ADR-032 §3 (from_call populates forwarded_for).

### forwarded_for population (ADR-032 §3)

The hub's `from_call` forwarding handler constructs the `call.requested` payload
to send to the spoke. It populates `forwarded_for` with the end user's identity
— read from the hub's `OperationContext.identity` (the caller the hub
authenticated) when the hub forwards the call.

```rust
// In the from_call forwarding handler:
let mut payload = serde_json::json!({
    "operationId": operation_id,
    "input": input,
});
// Populate forwarded_for from the hub's context.identity (ADR-032)
if let Some(originator) = &context.identity {
    payload["forwarded_for"] = serde_json::to_value(originator).ok().unwrap_or(Value::Null);
}
// The hub authenticates as itself (its own auth_token)
if let Some(token) = &credentials.auth_token {
    payload["auth_token"] = serde_json::Value::String(token);
}
```

The hub may set `forwarded_for: None` (omit the field) if it doesn't want to
disclose the originator. The spoke receives it as metadata on its
`OperationContext` — available for logging, auditing, per-user rate limiting,
but never used by `AccessControl::check` (the spoke authorizes the hub, its
direct caller).

### Peer-keyed registration (ADR-029 §5)

`from_call` registers into the specific peer's sub-overlay (via
`CallConnection::register_imported`), not a flat overlay. Cross-peer collision
dissolves: same name on different peers is fine (separate sub-overlays, no
collision, no prefix needed). Same-peer collision stays an error (a peer
shouldn't expose two ops with the same name).

`FromCallConfig::namespace_prefix` becomes optional local-naming sugar for
when the importing node wants to expose a peer's ops under a different name
*locally* — a local-naming concern, not a disambiguation concern. It defaults
to `None`.

### Collision rule change

- **Same-peer collision** = error (a peer shouldn't expose two ops with the
  same name). `AdapterError::SamePeerCollision`.
- **Cross-peer collision** dissolves (same name on different peers is fine —
  separate sub-overlays, ADR-029 §5).

The existing `from_call` namespace-collision check (which was flat-namespace)
changes to same-peer-only. The `AdapterError::Conflict` variant (if it exists)
renames to `SamePeerCollision` (OQ-26).

### What this task does NOT do

- Does NOT build `PeerCompositeEnv` (that's `call/peer-composite-env`, a
  dependency) — `from_call` registers into the connection's overlay, which
  `PeerCompositeEnv` aggregates by peer.
- Does NOT add `forwarded_for` to `OperationContext` (that's
  `call/operation-context-forwarded-for`, a dependency) — this task populates
  the wire field.

## Acceptance Criteria

- [ ] `from_call` forwarding handler populates `forwarded_for` on the `call.requested` payload from `context.identity`
- [ ] `forwarded_for` omitted (None) when the hub chooses not to disclose the originator
- [ ] `from_call` registers into the connection's overlay (peer-keyed via `PeerCompositeEnv`)
- [ ] Same-peer collision = error (`AdapterError::SamePeerCollision`)
- [ ] Cross-peer collision dissolves (same name on different peers is fine)
- [ ] `FromCallConfig::namespace_prefix` defaults to `None` (optional local-naming sugar)
- [ ] `AdapterError::Conflict` renamed to `SamePeerCollision` (OQ-26)
- [ ] Unit test: `forwarded_for` populated from `context.identity` on forwarding
- [ ] Unit test: `forwarded_for` omitted when context.identity is None
- [ ] Unit test: same-peer collision returns `SamePeerCollision` error
- [ ] Unit test: cross-peer same name does not collide (separate sub-overlays)
- [ ] `cargo test -p alknet-call` succeeds
- [ ] `cargo clippy -p alknet-call` succeeds with no warnings

## References

- docs/architecture/crates/call/client-and-adapters.md — from_call, forwarded_for, namespace collision
- docs/architecture/decisions/029-peer-graph-routing-model.md — ADR-029 §5 (peer-keyed registration, collision rule)
- docs/architecture/decisions/032-forwarded-for-identity.md — ADR-032 §3 (from_call populates forwarded_for)

## Notes

> The `from_call` handler is the hub's forwarding path. It populates
> `forwarded_for` from the hub's `context.identity` (the end user) so the spoke
> has the originator as metadata. The spoke authorizes the hub (its direct
> caller), not the end user — `AccessControl::check` never reads
> `forwarded_for`. Cross-peer collision dissolves under the peer-keyed model
> (separate sub-overlays); same-peer collision stays an error.

## Summary

Wired the from_call forwarding handler to populate forwarded_for on the call.requested payload from context.identity (ADR-032 §3), added CallConnection::call_with_payload for caller-constructed payloads, renamed AdapterError::Conflict → SamePeerCollision (OQ-26, ADR-029 §5 same-peer-only collision rule), updated FromCallConfig::namespace_prefix doc to reflect it's optional local-naming sugar (defaults None), and extracted build_bundles/build_forwarded_payload for unit-testable collision and forwarding logic. 9 new unit tests covering forwarded_for populated/omitted, same-peer collision (with and without prefix), cross-peer same-name no-collision, distinct names, prefix application, and operation filter. 241 unit + 2 integration tests pass, clippy clean, fmt clean.