Agents#34
Conversation
Greptile SummaryThis PR adds a server-side agent execution loop to Hadrian, including: a background
Confidence Score: 5/5The core retry, persistence, and security paths are well-implemented and explicitly tested; only a documentation discrepancy in the MCP executor was found. The retry logic correctly avoids writing terminal state on transient stream errors, the path-traversal guard covers both upload branches (with a test for each), and the MCP approval gate fails closed rather than bypassing when persistence is unavailable. The sole finding is a stale "warn-and-run" doc comment that contradicts tested fail-closed behavior in the MCP executor. src/services/mcp/executor.rs — the field and constructor doc comments describing the no-persistence approval behavior should be updated to reflect the actual fail-closed semantics. Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant Route as /v1/responses
participant DB
participant Worker as Background Worker
participant Executor as Background Executor
participant Persister
participant Provider as LLM Provider
participant Shell as Shell Runtime
participant MCP as MCP Server
Client->>Route: "POST /v1/responses (background=true)"
Route->>DB: "INSERT row (status=queued)"
Route-->>Client: "202 {id, status: queued}"
Worker->>DB: claim_queued (SELECT FOR UPDATE SKIP LOCKED)
DB-->>Worker: ResponseRecord
Worker->>Executor: execute_persisted_response(record)
Executor->>Provider: stream request
Provider-->>Executor: SSE stream
Executor->>Persister: wrap_streaming_with_persistence
Persister->>DB: write events (sequence++)
loop Tool loop (max_iterations)
Provider-->>Executor: function_call (shell/mcp)
alt shell tool
Executor->>Shell: exec(commands)
Shell-->>Executor: stdout/stderr
else MCP tool (requires_approval)
Executor->>DB: INSERT mcp_pending_approvals
Executor-->>Client: mcp_approval_request event
Client->>Route: POST /v1/responses (mcp_approval_response)
Route->>MCP: tools/call
MCP-->>Route: result
else MCP tool (no approval)
Executor->>MCP: tools/call
MCP-->>Executor: result
end
Executor->>Provider: continuation request
end
Provider-->>Persister: response.completed event
Persister->>DB: "UPDATE status=completed"
Client->>Route: "GET /v1/responses/{id}?stream=true"
Route-->>Client: replay event log
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/mcp/executor.rs:177-222
**Approval gate doc says "warn-and-run" but code fails closed**
Two doc comments (on the `response_id` field and the `with_persistence` constructor) state the approval gate "degrades to a warn-and-run" when persistence prerequisites are missing. The actual code calls `synthesize_failed_call`, which emits a `response.mcp_call.failed` item and returns an error — it does *not* run the tool. The behavior is correct and even tested by `park_for_approval_fails_closed_without_persistence`, but the stale "warn-and-run" wording could lead an operator to believe tool calls proceed without approval in no-DB or `store=false` deployments, discouraging them from investigating why calls are silently failing.
Reviews (6): Last reviewed commit: "Review fixes" | 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: