Agents, server-side tools and MCP#34
Conversation
Greptile SummaryThis PR adds server-side agent capabilities to Hadrian: a Responses API tool-execution loop with background mode (queued + persisted), container sessions with a server-side shell tool, and server-side MCP with per-call approval gating. All three previous P1 findings from the prior review are addressed in this revision.
Confidence Score: 5/5The three blocking issues from the prior review are all addressed; the only remaining finding is a non-blocking suggestion around hash stability for inline skill IDs. All the significant correctness issues flagged in earlier reviews are resolved. The new code correctly defers terminal-state writes to after retry exhaustion, redacts inline secrets on retrieval, and guards both upload path branches against traversal. The one open finding (DefaultHasher for inline skill synthetic IDs) only matters during rolling binary upgrades and does not affect correctness within a single binary deployment. src/services/responses_pipeline.rs — inline_skill_synthetic_id uses DefaultHasher whose stability across Rust versions is unspecified. Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant Handler as POST /v1/responses
participant DB
participant Worker as Background Worker
participant LLM as LLM Provider
participant Shell as Shell Runtime (Sandbox)
participant MCP as MCP Server
Client->>Handler: "POST /v1/responses (background=true)"
Handler->>DB: "INSERT responses (status=queued)"
Handler-->>Client: "202 Accepted { id, status: queued }"
Worker->>DB: claim_queued (SELECT FOR UPDATE SKIP LOCKED)
DB-->>Worker: ResponseRecord
Worker->>LLM: "execute_with_fallback (stream=true)"
LLM-->>Worker: SSE stream
loop Tool execution loop
LLM-->>Worker: response.output_item.done (shell_call)
Worker->>Shell: ExecRequest (commands, egress_policy, secrets)
Shell-->>Worker: ExecEvent stream (stdout/stderr/exit_code)
Worker->>LLM: continuation (function_call_output)
LLM-->>Worker: response.output_item.done (mcp_call)
Worker->>MCP: tools/call (with auth headers)
MCP-->>Worker: CallToolResult
Worker->>LLM: continuation (function_call_output)
end
LLM-->>Worker: response.completed
Worker->>DB: "UPDATE responses (status=completed, last_seq_no)"
Client->>Handler: "GET /v1/responses/{id}?stream=true"
Handler->>DB: list_events_after(cursor)
DB-->>Handler: SSE event log
Handler-->>Client: "SSE replay -> data: [DONE]"
Prompt To Fix All With AIFix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
src/services/responses_pipeline.rs:342-348
**Unstable hash may break inline-skill mount paths across binary versions**
`DefaultHasher::new()` seeds to zero and is deterministic within a single binary, but Rust's documentation explicitly states the algorithm "is subject to change at any point in the future" — it has already changed once (SipHash 2-4 → SipHash 1-3). During a rolling deployment where a queued background response is created by binary V1 but executed by binary V2, the LLM's instructions would reference `/skills/hash_A` while the runtime mounts files at `/skills/hash_B`, causing skill files to be unreachable to every shell command the model issues in that request. A stable, content-addressed hash such as a truncated SHA-256 (or `uuid::Uuid::new_v5` with a fixed namespace) carries an explicit stability contract and costs roughly the same.
Reviews (7): Last reviewed commit: "Improve testing instructions" | Re-trigger Greptile |
| fn serialize_payload_for_storage( | ||
| payload: &crate::api_types::CreateResponsesPayload, | ||
| ) -> serde_json::Value { | ||
| let mut value = serde_json::to_value(payload).unwrap_or(serde_json::Value::Null); | ||
| strip_input_file_data(&mut value); | ||
| strip_mcp_credentials(&mut value); | ||
| value | ||
| } |
There was a problem hiding this comment.
Inline domain secret values stored and echoed in plaintext
serialize_payload_for_storage strips MCP authorization/headers credentials but does not strip ShellDomainSecretInline.value. A caller who sends a shell tool request with network_policy.domain_secrets[].type = "inline" (carrying a raw secret value like an API key) will have that value persisted verbatim in responses.request_payload. The field is then echoed back via GET /v1/responses/{id} because "tools" is in ECHO_FIELDS at responses_lookup.rs:93. The same MCP-credential-stripping pattern should apply here: redact (or omit) inline.value from the stored payload, mirroring how authorization is removed from mcp entries.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/routes/api/chat.rs
Line: 309-316
Comment:
**Inline domain secret values stored and echoed in plaintext**
`serialize_payload_for_storage` strips MCP `authorization`/`headers` credentials but does not strip `ShellDomainSecretInline.value`. A caller who sends a shell tool request with `network_policy.domain_secrets[].type = "inline"` (carrying a raw secret value like an API key) will have that value persisted verbatim in `responses.request_payload`. The field is then echoed back via `GET /v1/responses/{id}` because `"tools"` is in `ECHO_FIELDS` at `responses_lookup.rs:93`. The same MCP-credential-stripping pattern should apply here: redact (or omit) `inline.value` from the stored payload, mirroring how `authorization` is removed from `mcp` entries.
How can I resolve this? If you propose a fix, please make it concise.
Adds: