Skip to content

Tags: airutorg/airut

Tags

v0.25.6

Toggle v0.25.6's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

v0.25.5

Toggle v0.25.5's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

v0.25.4

Toggle v0.25.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(proxy): decode path before request-body filters to close tool-dom…

…ain trim bypass (#623)

v0.25.3

Toggle v0.25.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

v0.25.2

Toggle v0.25.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

v0.25.1

Toggle v0.25.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

v0.25.0

Toggle v0.25.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
docs: 0.25.0 documentation cleanup (#606)

v0.24.2

Toggle v0.24.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Bump dependencies to latest upstream versions (#581)

v0.24.1

Toggle v0.24.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Drop GCS bucket fallback for Claude Code binary downloads (#575)

v0.24.0

Toggle v0.24.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>