Tags: airutorg/airut
Tags
fix(slack): stop re-uploading outbox files on every reply (#636) * fix(slack): stop re-uploading outbox files on every reply The outbox is mounted for the whole life of a conversation, but only the email adapter deleted its contents after delivery. On Slack every follow-up in an engaged thread collected the files left behind by earlier turns and uploaded them again. Move the cleanup out of the email adapter and into the gateway delivery path, so the outbox is cleared once send_reply() returns for any channel. A failed delivery raises ChannelSendError and skips the cleanup, leaving the files for the next attempt. * fix(scheduler): clear outbox after delivering a scheduled result Address code review: the periodic-task path had the same defect — its conversation outlives the run (recipients reply to continue it), so delivered attachments came back with every reply. Also from review: - skip sub-directories when collecting outbox files, so adapters are never handed a path they cannot send - email adapter attaches the paths the core passed (read_outbox_files) instead of re-scanning the directory via outbox_files[0].parent - Slack names files it failed to upload in the thread, since the core clears them either way - document the outbox contract on ChannelAdapter.send_reply() - drop the redundant conversation_id parameter from _deliver_reply() * refactor(outbox): single source of truth for outbox delivery Second review round: - list_outbox_files() in conversation_layout defines what is deliverable (root files, sorted) and clear_outbox() removes exactly that set; the rule no longer lives in three places - read_outbox_files() returns OutboxContent, naming files it could not read; email and scheduled delivery put that in the body, matching the contract Slack already follows - collect_outbox_files() deleted (thin wrapper, one caller left) - Slack upload also catches SlackRequestError/OSError so a file that vanished mid-flight is reported instead of failing the task - fix send_reply/complete_task ordering in the gateway data-flow spec * fix(outbox): never deliver symlinks placed in the outbox Third review round. The gateway reads the outbox on the host, so a symlink written inside the container resolves against a different filesystem: a task could link the server config into /outbox and have it mailed or uploaded verbatim. Skip links at listing time and unlink them with the rest of the outbox (the link, never its target). Also: cover the documented TIMEOUT contract with a test, make unreadable_note() empty-safe, and keep _report_failed_uploads from escaping (which would skip the cleanup). --------- Co-authored-by: airut-app[bot] <269787008+airut-app[bot]@users.noreply.github.com>
fix(proxy): block Query.resource(url:) GraphQL repo-scope bypass (#626) check_repo_scope() inspected repository(owner,name), the plural repositories/forks connections, Query.search, and *Id node-ID fields, but not GitHub's Query.resource(url:) — a global URL resolver that returns the node (Repository/Issue/Commit/…) addressed by a URL. Because the target repository is bound only by the opaque URL string, none of the existing layers matched it, so an in-scope github-app surrogate token could read ANY repository the installation token can see (confirmed live: resource(url:"https://github.com/airutorg/website") returned repo contents with HTTP 200 while the equivalent repository(owner,name) selection was correctly 403'd). The spec listed resource(url:) as a residual risk 'mitigated only by the operation allowlist', but the deployed allowlist uses queries:["*"], which does not constrain queries at all — so it was live-exploitable. Add Layer 0c: fail-secure block any 'resource' field selection carrying a 'url' argument, returning OUT_OF_SCOPE <url-addressed:resource>. A field merely named 'resource' with no 'url' argument is not blocked. Tests-first: TestResourceUrlLookup covers the block, aliases, variable URLs, in-scope URLs, co-selection with an in-scope repository, and the no-url false-positive guard. Spec updated (Layer 0c documented; residual risk list updated). Co-authored-by: airut-app[bot] <269787008+airut-app[bot]@users.noreply.github.com>
fix(sandbox): force CLAUDE_CODE_DISABLE_BACKGROUND_TASKS in headless … …runs (#617) airut runs Claude headlessly (`claude -p`), which is single-shot: the process prints its result and exits when the model ends its turn. A foreground Bash command that blocks longer than Claude's ~15s "assistant-mode blocking budget" is auto-moved to the background and its result is delivered as an async notification -- but `-p` exits (killing the task) before that notification can be processed. The agent then reports an interim "standing by" message and never sees, e.g., the CI verdict. Force CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 into the container env for every AgentTask (Claude) execution, overriding any caller/repo value. This makes Bash commands and subagents run synchronously, so the turn blocks until they finish. Subagents still work; they just run inline rather than being backgrounded. Ensuring `claude -p` behaves correctly is the sandbox's responsibility, not the repo's, so it is set here rather than in the repo Dockerfile. CommandTask (non-Claude commands) is intentionally left unchanged. Co-authored-by: airut-app[bot] <269787008+airut-app[bot]@users.noreply.github.com>
fix(email): preserve attachments on coalesced follow-up emails (#611) EmailParsedMessage carried the raw email for deferred attachment extraction but did not override coalesce(), so the base coalesce merged only the rendered bodies. When follow-up emails coalesced into a busy conversation's pending message, save_attachments() saw only the survivor's email and the follow-ups' attachments were silently dropped — the email mirror of the Slack coalescing bug fixed in #610. Retain every coalesced email: _raw_message (single) becomes _raw_messages (list), seeded at parse and extended in a coalesce() override; save_attachments() extracts from all of them. Duplicate names across coalesced emails are uniquified by the existing unique_inbox_path helper (filesystem-based, so it dedupes across the per-message extract calls). Document the channel-specific coalesce contract in gateway-architecture. Co-authored-by: airut-app[bot] <269787008+airut-app[bot]@users.noreply.github.com>
Run web_search unrestricted when allowed_domains trims empty (#608) The Anthropic tool-domain trim injects/leaves allowed_domains: [] as a default-deny for covered server-side tools. web_search rejects an empty list with a 400 ("Empty list of domains is ambiguous"), which fails the entire /v1/messages request — so the trim broke all unrestricted web searches. Add _UNRESTRICTED_ON_EMPTY_PREFIXES (web_search_*): for these tools an empty effective allow-list removes the allowed_domains key (runs unrestricted) instead of injecting/leaving []. A non-empty trim still narrows to the reachable subset. web_fetch and other full-content fetchers keep []-as-deny-all. Accepted trade-off: web_search returns engine snippets, not full page bodies, so snippet leakage already exists regardless of the trim. Update spec/anthropic-tool-domain-trim.md (contract table, empty-list handling, maintenance, resolved open questions). Co-authored-by: airut-app[bot] <269787008+airut-app[bot]@users.noreply.github.com>
Fix duplicate header handling in proxy token replacement and AWS re-s… …igning (#566) * Fix duplicate header handling in proxy token replacement and AWS re-signing Two security findings in the proxy's handling of mitmproxy's multidict headers: 1. _replace_tokens only inspected the first value for each header name (via headers[name]), missing surrogates in duplicate entries. An attacker could inject a header before the surrogate to prevent replacement. Now scans all values per header name via items(). 2. _prepare_signing_context and _resign_presigned used dict(headers) which silently drops all but the last value for duplicate header names. AWS SigV4 requires multi-value headers to be comma-joined and sorted. New _collect_signing_headers helper handles this correctly, with Host header excluded from comma-joining (HTTP requires exactly one Host value). Both issues are fail-secure (worst case: request rejection, never credential leakage), but fixing them improves correctness and removes attack surface for request manipulation. * Address code review findings - Add empty-Host fallback to _resign_presigned (consistency with _prepare_signing_context for HTTP/2 connections) - Update spec/masked-secrets.md to describe entry-level header scanning - Update spec/aws-sigv4-resigning.md to document multi-value header comma-joining in canonical request construction - Improve test_mixed_case_names_grouped to use unsorted values - Add test_empty_host_header_replaced for presigned URL path --------- Co-authored-by: airut-app[bot] <269787008+airut-app[bot]@users.noreply.github.com>
PreviousNext