Skip to content

feat(tool-broker): extract connector execution into standalone aegis-tool-broker binary - #1856

Merged
lavkushry merged 1 commit into
mainfrom
feat/tool-broker-extraction-phase1
Jul 12, 2026
Merged

feat(tool-broker): extract connector execution into standalone aegis-tool-broker binary#1856
lavkushry merged 1 commit into
mainfrom
feat/tool-broker-extraction-phase1

Conversation

@lavkushry

@lavkushry lavkushry commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 1 of the tool-broker extraction (roadmap: docs/current-vs-roadmap.md "Tool broker" partial item; design rationale in AegisAgent_Runtime_Data_Plane.md/AegisAgent_Agent_Cage.md: "sensor/cage/egress/broker must ship as separate binaries... never inside the gateway process").

  • New standalone aegis-tool-broker binary (bins/aegis-tool-broker/) owns connector execution: EnvCredentialResolver, ShellConnector/GithubConnector/etc, and the actual provider API calls. It exposes POST /v1/execute behind a required bearer token (AEGIS_TOOL_BROKER_API_TOKEN, no default — this binary's entire surface is the privileged action) plus /livez//readyz.
  • The gateway (src/) no longer links aegis-tool-broker-connectors in production (removed from src/Cargo.toml's [dependencies]; kept as a [dev-dependencies]-only exception so routes/broker.rs's tests can spin up a real BrokerExecutor behind a mock HTTP server). It never resolves a real credential or shells out itself anymore.
  • New ToolBrokerClient (src/src/tool_broker_client.rs) calls the new binary over HTTP; AppState.tool_broker: Option<Arc<ToolBrokerClient>> replaces the old in-process broker_executor. With no broker configured, POST /v1/broker/execute fails closed with 501 — same precedent as OIDC and signed policy bundles, no in-process fallback.
  • Everything before the connector call is unchanged: gateway-side authorization, hash-bound atomic approval consumption, and receipt emission all still happen in the gateway process exactly as before.
  • Ships with a Helm chart (helm/aegis-tool-broker/, hardened securityContext copied from aegis-egress-proxy, httpGet probes against /livez//readyz) and a docker-compose.full.yml service wiring AEGIS_TOOL_BROKER_URL/AEGIS_TOOL_BROKER_API_TOKEN into the gateway service.
  • Docs (Tool_Broker.md, Implementation_Status.md, current-vs-roadmap.md) corrected to describe the actual Phase 1 topology instead of the aspirational reversed-topology/scoped-token end-state, which is not built here and remains a named follow-up.

Known Phase-1 gap, documented not glossed over (Tool_Broker.md §6 Honest Scope): the gateway consumes the approval before the broker HTTP call. If the broker is unreachable at that point, the approval is burned without the action executing — fail-closed (not fail-open), but a real UX gap. Regression test: execute_fails_closed_when_the_broker_is_unreachable.

Test plan

  • cargo check --workspace
  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace -- --test-threads=1 (new binary's own unit tests: auth middleware, executor config, handlers incl. credential-never-leaks; adapted routes/broker.rs execute-path tests against a mock HTTP broker; new execute_returns_501_when_tool_broker_is_unconfigured / execute_fails_closed_when_the_broker_is_unreachable)
  • helm lint helm/aegis-tool-broker/
  • helm template helm/aegis-tool-broker/ renders correctly
  • cargo tree -p gateway -i aegis-tool-broker-connectors confirms the crate is dev-dependency-only now
  • End-to-end: run bins/aegis-tool-broker standalone against a real gateway + docker-compose.full.yml tool-broker service

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a standalone tool-broker service for executing configured tools through a secured HTTP API.
    • Added bearer-token authentication, health checks, and sanitized error responses.
    • Added optional filesystem, shell, HTTP, and GitHub connector configuration.
    • Gateway execution now supports the external broker and preserves approval and receipt controls.
  • Deployment

    • Added Docker Compose and Helm deployment support, including secrets, probes, service configuration, and security defaults.
  • Bug Fixes

    • Broker communication now fails closed when unavailable, unauthorized, or incorrectly configured.

…tool-broker binary

Phase 1 of the tool-broker extraction: the gateway process no longer links
aegis-tool-broker-connectors (removed from src/Cargo.toml production deps,
kept as a dev-dependency for the one deliberate mock-server test exception).
It never resolves a real provider credential or runs a shell command itself
anymore. POST /v1/broker/execute now calls the new standalone
aegis-tool-broker binary over HTTP with a required service-to-service
bearer token (AEGIS_TOOL_BROKER_API_TOKEN); with no broker configured it
fails closed with 501, matching the OIDC/signed-policy-bundle precedent.

The gateway is unchanged for authorization, hash-bound approval consumption,
and receipt emission -- only the connector-execution step moved process.
Ships with a Helm chart (helm/aegis-tool-broker/) and a docker-compose.full.yml
service, both wired for local/dev use.

Known Phase-1 gap (documented in Tool_Broker.md's Honest Scope, not glossed
over): the gateway consumes the approval before the broker HTTP call, so a
broker-unreachable failure burns the approval without executing the action.
Covered by execute_fails_closed_when_the_broker_is_unreachable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 12, 2026 15:36
@ecc-tools

ecc-tools Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a66a093-0c5f-4db0-9598-94f5623bf91d

📥 Commits

Reviewing files that changed from the base of the PR and between 41bf4e0 and f027d07.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • Cargo.toml
  • bins/aegis-tool-broker/Cargo.toml
  • bins/aegis-tool-broker/Dockerfile
  • bins/aegis-tool-broker/src/auth.rs
  • bins/aegis-tool-broker/src/dto.rs
  • bins/aegis-tool-broker/src/error.rs
  • bins/aegis-tool-broker/src/executor_config.rs
  • bins/aegis-tool-broker/src/handlers.rs
  • bins/aegis-tool-broker/src/lib.rs
  • bins/aegis-tool-broker/src/main.rs
  • docker-compose.full.yml
  • docs/Implementation_Status.md
  • docs/components/Tool_Broker.md
  • docs/current-vs-roadmap.md
  • helm/aegis-tool-broker/Chart.yaml
  • helm/aegis-tool-broker/templates/_helpers.tpl
  • helm/aegis-tool-broker/templates/deployment.yaml
  • helm/aegis-tool-broker/templates/secret.yaml
  • helm/aegis-tool-broker/templates/service.yaml
  • helm/aegis-tool-broker/templates/serviceaccount.yaml
  • helm/aegis-tool-broker/values.yaml
  • src/Cargo.toml
  • src/src/lib.rs
  • src/src/main.rs
  • src/src/routes/authorize.rs
  • src/src/routes/broker.rs
  • src/src/routes/mod.rs
  • src/src/tool_broker_client.rs

📝 Walkthrough

Walkthrough

Changes

Standalone tool broker

Layer / File(s) Summary
Broker service and execution contract
Cargo.toml, bins/aegis-tool-broker/*
Adds the broker crate, authenticated /v1/execute API, executor configuration, error mapping, health endpoints, tests, and binary entry point.
Gateway broker client
src/src/lib.rs, src/src/tool_broker_client.rs
Adds an environment-configured HTTP client with bearer authentication and fail-closed error handling.
Gateway routing and state migration
src/src/main.rs, src/src/routes/*, src/Cargo.toml
Replaces in-process execution with optional external broker calls, preserves approval and receipt handling, and adds mock-broker tests.
Container and Kubernetes deployment
bins/aegis-tool-broker/Dockerfile, docker-compose.full.yml, helm/aegis-tool-broker/*
Adds container packaging, Compose wiring, and Helm resources for the broker service.
Architecture documentation
docs/Implementation_Status.md, docs/components/Tool_Broker.md, docs/current-vs-roadmap.md
Documents the standalone broker topology, deployment status, responsibility split, and remaining limitations.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Gateway
  participant ToolBrokerClient
  participant ToolBroker
  participant Connector
  Gateway->>ToolBrokerClient: Submit authenticated execute request
  ToolBrokerClient->>ToolBroker: POST /v1/execute
  ToolBroker->>Connector: Resolve credentials and execute action
  Connector-->>ToolBroker: Return sanitized output
  ToolBroker-->>ToolBrokerClient: Return JSON response
  ToolBrokerClient-->>Gateway: Return output or mapped error
Loading

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tool-broker-extraction-phase1

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.44.1)
src/src/main.rs

ast-grep timed out on this file


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lavkushry
lavkushry merged commit 1dba9bc into main Jul 12, 2026
28 of 29 checks passed
@lavkushry
lavkushry deleted the feat/tool-broker-extraction-phase1 branch July 12, 2026 15:37

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request extracts the tool broker execution engine from the gateway into a standalone binary (aegis-tool-broker) to isolate credentials and connector execution. The gateway now communicates with the broker over HTTP using a shared service-to-service bearer token. The feedback highlights several critical issues and improvements: a non-existent Rust version (1.96) in the Dockerfile that will break the build, a required CLI token argument that breaks the container health check, missing HTTP timeouts in both the health check and the gateway client that could cause hangs, a need for defense-in-depth checks against empty tokens, and a Helm chart configuration that should fail fast if the API token secret is missing.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

# extraction). A separate binary/image from the gateway — the gateway no
# longer links aegis-tool-broker-connectors at all, so this is the only
# process that resolves a real provider credential or executes a connector.
FROM rust:1.96-bookworm AS builder

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The Rust version 1.96 specified in the builder stage does not exist (the current stable version is in the 1.8x range). This will cause the Docker build to fail immediately. Please use a valid stable Rust version, such as 1.82-bookworm.

FROM rust:1.82-bookworm AS builder

Comment on lines +29 to +30
#[arg(long, env = "AEGIS_TOOL_BROKER_API_TOKEN", hide_env_values = true)]
api_token: String,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Making api_token a required non-optional string in clap will cause the --healthcheck command to fail if the AEGIS_TOOL_BROKER_API_TOKEN environment variable is not set during the health check. Additionally, we should explicitly validate that the token is not empty or whitespace-only to prevent starting the privileged service with an insecure empty token. Changing api_token to Option and validating it in main resolves both issues.

Suggested change
#[arg(long, env = "AEGIS_TOOL_BROKER_API_TOKEN", hide_env_values = true)]
api_token: String,
#[arg(long, env = "AEGIS_TOOL_BROKER_API_TOKEN", hide_env_values = true)]
api_token: Option<String>,

Comment on lines +58 to +59
let executor = build_broker_executor();
let app = router(executor, cli.api_token.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Validate that the API token is present and non-empty before starting the server, and pass the validated token to the router.

Suggested change
let executor = build_broker_executor();
let app = router(executor, cli.api_token.clone());
let Some(api_token) = cli.api_token.filter(|t| !t.trim().is_empty()) else {
return Err("AEGIS_TOOL_BROKER_API_TOKEN is required and cannot be empty".into());
};
let executor = build_broker_executor();
let app = router(executor, api_token);

Comment on lines +67 to +74
pub fn new(base_url: String, api_token: String, timeout_secs: u64) -> Self {
Self {
execute_url: format!("{base_url}/v1/execute"),
api_token,
http: reqwest::Client::new(),
timeout: Duration::from_secs(timeout_secs),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The reqwest::Client is created without a timeout. While tokio::time::timeout is used around the .send() call, it only protects the initial request sending and header receipt. The subsequent response body reading (via .json().await) is executed outside the timeout and can hang indefinitely if the connection stalls during streaming. Configuring the timeout on the reqwest::Client itself ensures that both the request and the body read are safely bounded.

    pub fn new(base_url: String, api_token: String, timeout_secs: u64) -> Self {
        let timeout = Duration::from_secs(timeout_secs);
        let http = reqwest::Client::builder()
            .timeout(timeout)
            .build()
            .unwrap_or_else(|_| reqwest::Client::new());
        Self {
            execute_url: format!("{base_url}/v1/execute"),
            api_token,
            http,
            timeout,
        }
    }

Comment on lines +50 to +56
if cli.healthcheck {
let url = format!("http://{}/livez", cli.listen);
return match reqwest::get(&url).await {
Ok(resp) if resp.status().is_success() => Ok(()),
_ => std::process::exit(1),
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The healthcheck request uses reqwest::get which has no timeout configured. If the server is deadlocked or hanging, the healthcheck process will hang indefinitely, potentially leading to resource exhaustion. Configuring a client with a reasonable timeout (e.g., 5 seconds) prevents this.

Suggested change
if cli.healthcheck {
let url = format!("http://{}/livez", cli.listen);
return match reqwest::get(&url).await {
Ok(resp) if resp.status().is_success() => Ok(()),
_ => std::process::exit(1),
};
}
if cli.healthcheck {
let url = format!("http://{}/livez", cli.listen);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
return match client.get(&url).send().await {
Ok(resp) if resp.status().is_success() => Ok(()),
_ => std::process::exit(1),
};
}

Comment on lines +18 to +24
pub fn tokens_match(configured: &str, provided: &str) -> bool {
fn hash(token: &str) -> String {
use sha2::{Digest, Sha256};
hex::encode(Sha256::digest(token.as_bytes()))
}
hash(configured) == hash(provided)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

As a defense-in-depth measure, tokens_match should explicitly return false if the configured token is empty. This prevents any potential authentication bypass if the server is misconfigured with an empty token.

pub fn tokens_match(configured: &str, provided: &str) -> bool {
    if configured.is_empty() {
        return false;
    }
    fn hash(token: &str) -> String {
        use sha2::{Digest, Sha256};
        hex::encode(Sha256::digest(token.as_bytes()))
    }
    hash(configured) == hash(provided)
}

Comment on lines +63 to +68
fn two_empty_tokens_are_equal_but_the_server_never_configures_an_empty_one() {
// Documents the invariant this relies on: Cli::api_token has no
// default_value, so an empty configured token is unreachable in
// production -- clap itself refuses to start the process.
assert!(tokens_match("", ""));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the test to assert that an empty configured token never matches, reflecting the new defense-in-depth behavior.

    #[test]
    fn empty_configured_token_never_matches() {
        assert!(!tokens_match("", ""));
    }

Comment on lines +47 to +52
- name: AEGIS_TOOL_BROKER_API_TOKEN
valueFrom:
secretKeyRef:
name: {{ include "aegis-tool-broker.secretName" . }}
key: AEGIS_TOOL_BROKER_API_TOKEN
optional: true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Setting optional: true for the AEGIS_TOOL_BROKER_API_TOKEN secret reference allows the pod to start even if the secret or the key is missing, which will result in the container running without a token (or failing to start at runtime). Setting optional: false (or omitting it) ensures Kubernetes fails fast with a clear CreateContainerConfigError if the required secret configuration is missing.

            - name: AEGIS_TOOL_BROKER_API_TOKEN
              valueFrom:
                secretKeyRef:
                  name: {{ include "aegis-tool-broker.secretName" . }}
                  key: AEGIS_TOOL_BROKER_API_TOKEN
                  optional: false

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants