Skip to content

feat(backend): agentflare-backend CRUD/webhooks/MCP tools + agentflare-db-kit - #162

Merged
getappz merged 5 commits into
masterfrom
feat/agentflare-backend
Jul 13, 2026
Merged

feat(backend): agentflare-backend CRUD/webhooks/MCP tools + agentflare-db-kit#162
getappz merged 5 commits into
masterfrom
feat/agentflare-backend

Conversation

@getappz

@getappz getappz commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • New agentflare-backend crate: workspace/project/item/label/state/webhook/asset domain model ported from Plane, full CRUD, soft deletes, per-project sequence IDs, 6-group state machine, synchronous webhook delivery (HMAC-signed, SSRF-guarded).
  • New backend_* MCP tools on AgentflareMcp (item create/get/list/update/update_state/delete, label create + attach/detach, webhook create/list/delete, project_info) — all added inside the single existing #[tool_router]-tagged impl block, guarded by a test asserting exactly one such block exists.
  • Workspace and project are fully automatic — no setup tools, nothing exposed as an MCP parameter:
    • One workspace per system, auto-created on first use.
    • The current repo auto-links to a project via .agentflare/project.json (Vercel-style), derived from the git remote or directory name.
    • If the link file is lost, recovery is keyed by repo identity (normalized git remote, or canonicalized absolute path) rather than the derived display name — so two differently-located repos that happen to share a directory basename are never silently merged into one project.
  • New agentflare-db-kit shared crate: open_file/open_memory (SQLite connection setup backed by rusqlite_migration), ids::now/ids::new_id, and a generic ClaimLedger (composite- or single-column-key leased-claim upsert) factored out of the duplicated boilerplate in src/claims.rs. src/claims.rs is retrofitted onto it — its public API and all 8 existing tests are unchanged.

Test plan

  • cargo test --workspace — all green (338 in the main binary, 44 in agentflare-backend, 9 in agentflare-db-kit, 0 failures)
  • cargo clippy --workspace --all-targets — clean
  • cargo fmt --check — clean
  • cargo deny check — advisories/bans/licenses/sources all ok
  • New tests specifically cover: item create end-to-end, empty-name rejection, state-transition timestamps, workspace reuse, project relinking after a deleted link file, and two differently-keyed repos with the same derived name never sharing a project

Summary by CodeRabbit

  • New Features
    • Added a local persistent backend with automatic SQLite setup, migrations, and CRUD support for workspaces, projects, items, states, labels, and assets.
    • Added MCP tools for work items (including state transitions), labels, and webhooks with signed delivery and delivery logging.
    • Introduced lease-based claim coordination to better handle concurrent operations.
  • Bug Fixes
    • Prevented unsafe asset path traversal.
    • Blocked deletion of default states and improved project relinking to reuse existing projects without conflating repositories.
  • Chores
    • Updated ignore rules for local backend state and per-repo project link files.

…lare-db-kit shared crate

Ports Plane's workspace/project/item domain model into a new agentflare-backend
crate (schema, CRUD, state machine, webhook delivery) and exposes it as
backend_* MCP tools on AgentflareMcp. Workspace and project are fully
automatic: one workspace per system, auto-created on first use; the current
repo auto-links to a project via .agentflare/project.json (Vercel-style),
recovering by repo identity (git remote, or canonicalized path) rather than
derived name if the link file is lost, so two differently-located repos that
happen to share a directory name are never conflated into one project.

Also factors the connection-open/migration boilerplate and the leased-claim
upsert pattern duplicated across src/claims.rs and this new crate into a
shared agentflare-db-kit crate (open_file/open_memory via rusqlite_migration,
ids::now/new_id, a generic ClaimLedger over composite or single-column keys),
and retrofits src/claims.rs onto it.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c40594b5-9fff-439d-8484-8363572dfecc

📥 Commits

Reviewing files that changed from the base of the PR and between 00b1274 and 200f543.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • Cargo.toml
  • crates/agentflare-backend/Cargo.toml
  • crates/agentflare-db-kit/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (3)
  • Cargo.toml
  • crates/agentflare-db-kit/Cargo.toml
  • crates/agentflare-backend/Cargo.toml

📝 Walkthrough

Walkthrough

Adds two Rust crates: a SQLite-backed Agentflare backend and shared database utilities. The backend provides persistence, workflows, assets, webhooks, and MCP tools. Existing claim handling now delegates lease operations to the shared claim ledger.

Changes

Agentflare backend

Layer / File(s) Summary
Backend schema and crate foundation
Cargo.toml, crates/agentflare-backend/*
Adds the backend crate, SQLite schema, public modules, initialization, centralized errors, and changelog.
Workspace, project, and state lifecycle
crates/agentflare-backend/src/{workspace,project,state}.rs
Adds CRUD, soft deletion, default state seeding, lifecycle events, and tests.
Item workflow and relationships
crates/agentflare-backend/src/item.rs
Adds item CRUD, sequencing, state transitions, timestamps, relationships, event emission, and tests.
Labels, assets, and webhook delivery
crates/agentflare-backend/src/{label,asset,webhook,events}.rs
Adds label and asset persistence, filesystem operations, webhook validation, delivery logging, event fan-out, and tests.
MCP backend tools
src/mcp_server.rs, .gitignore
Adds persistent backend access, repository linking, MCP operations, local-state ignores, and integration tests.

Shared database kit and claims

Layer / File(s) Summary
Database utility crate foundation
crates/agentflare-db-kit/*
Adds SQLite opening, migration, timestamp, UUID helpers, public modules, and changelog.
Generic leased-claim ledger
crates/agentflare-db-kit/src/claim.rs
Adds atomic lease acquisition, ownership-scoped lifecycle operations, stale filtering, and tests.
Existing claims integration
src/claims.rs
Refactors claim operations to use ClaimLedger and shared time helpers.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCP
  participant ProjectLink
  participant BackendDB
  participant Backend
  participant WebhookEndpoint
  MCP->>ProjectLink: Resolve repository project
  MCP->>BackendDB: Open or reuse backend connection
  MCP->>Backend: Create or update work item
  Backend->>BackendDB: Persist item and relationships
  Backend->>WebhookEndpoint: Deliver matching event
  Backend->>BackendDB: Record delivery log
  Backend-->>MCP: Return backend result
Loading

Possibly related PRs

  • getappz/agentflare#141: Refactors claim leasing in the same src/claims.rs and builds on the shared ClaimLedger functionality.
  • getappz/agentflare#157: Both changes modify MCP tool registration and the tool_router structure in src/mcp_server.rs.

Suggested labels: enhancement, rust

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately covers the main changes: new backend CRUD/webhook tooling plus the shared db-kit crate.
Description check ✅ Passed The description is mostly complete and includes a solid summary and test plan, but it omits the Notes for reviewers section.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/agentflare-backend

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 10

🧹 Nitpick comments (1)
src/mcp_server.rs (1)

1082-1100: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Backend DB lock spans git and webhook POSTs with_backend_db holds the mutex for the entire closure, so the backend calls that go through it serialize while resolve_project() shells out to git and events::emit() delivers webhooks synchronously with a 5s timeout. Move webhook fan-out off the request path, or at least out of the critical section, if backend throughput matters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp_server.rs` around lines 1082 - 1100, Reduce mutex contention in
with_backend_db by ensuring its lock is not held while callers perform slow git
operations or synchronous webhook delivery. Refactor the affected backend call
paths, including resolve_project and events::emit usage, to copy or access the
needed database state under the lock, then release it before running external
commands or webhook fan-out; preserve existing results and error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/agentflare-backend/src/asset.rs`:
- Around line 41-55: Harden storage_path and every filesystem helper, including
write_file and the related read/delete helpers, against traversal by rejecting
absolute paths and any non-normal components in workspace_id or filename.
Resolve the resulting asset path beneath a non-symlinked base_path, verify
containment before each filesystem operation, and reject symlink escapes rather
than reading, writing, or deleting outside the asset root.

In `@crates/agentflare-backend/src/item.rs`:
- Around line 208-211: Before inserting in the item creation flow, validate any
provided input.state_id against the current project using the same
project-membership check as update_state(). Reuse that existing validation logic
or helper, and reject mismatched states before item::create proceeds; leave
creation unchanged when state_id is absent.

In `@crates/agentflare-backend/src/state.rs`:
- Around line 195-204: Update states::delete to prevent deleting the active
default state unless a replacement default is designated atomically in the same
operation. Preserve the existing NotFound behavior for missing or
already-deleted states, and ensure MCP item creation without state_id can still
resolve a default after deletion.

In `@crates/agentflare-backend/src/webhook.rs`:
- Around line 8-21: Update the Webhook struct’s serialization behavior so
secret_key is never included in serialized create, get, or list responses.
Prefer a redacted response DTO if the existing API supports one; otherwise
configure secret_key to be skipped during serialization while preserving its
ability to deserialize and use the signing secret internally.
- Around line 298-309: Update the response-body handling in the webhook delivery
flow around request.send_bytes and log_delivery to cap the body before storing
it in webhook_logs.response_body. Apply the same bounded-prefix behavior to
successful responses and HTTP status errors, and mark the stored value when
truncation occurs; avoid retaining or logging the full remote response.
- Around line 81-100: Harden validate_webhook_url and the webhook delivery flow
to reject private, link-local, loopback, and other non-public connection
addresses, including targets resolved from hostnames. Resolve the hostname and
re-check the actual peer address immediately before sending, and validate every
redirect destination before following it so existing stored webhook URLs receive
the same protection.

In `@crates/agentflare-db-kit/src/claim.rs`:
- Around line 91-122: Update the claim operation around the UPSERT and follow-up
result handling to derive Acquire directly from the write, using SQLite
RETURNING or a transaction that makes the write and read atomic. Ensure a
successful claim cannot be changed by concurrent release/done activity before
determining whether to return Acquired or Held, while preserving the existing
owner and age_secs values.

In `@src/claims.rs`:
- Around line 7-16: Update the acquire flow around LEDGER.acquire() and the
subsequent git_commit UPDATE so provenance is written only to the lease
acquisition that succeeded. At minimum, add the acquired owner to the UPDATE
predicate; preferably perform acquisition and provenance update in one
transaction or use a lease generation/token to bind the UPDATE to the exact
acquisition, preventing a stolen lease from receiving the previous owner’s
git_commit.

In `@src/mcp_server.rs`:
- Around line 2236-2268: Add upfront validation in both backend_item_add_label
and backend_item_remove_label to reject blank item_id or label_id values with
the established invalid_params response used by other backend tools. Perform the
checks before with_backend_db, while preserving the existing database operations
and success responses for valid IDs.
- Around line 2270-2301: Update backend_webhook_create to validate url with
validate_webhook_url before creating or persisting the webhook, expanding
validation to reject RFC1918, 169.254.0.0/16, IPv6 link-local and ULA addresses,
plus hostnames resolving to any of those ranges. Return invalid parameters for
blocked targets and preserve the existing project resolution and webhook
creation flow for allowed URLs.

---

Nitpick comments:
In `@src/mcp_server.rs`:
- Around line 1082-1100: Reduce mutex contention in with_backend_db by ensuring
its lock is not held while callers perform slow git operations or synchronous
webhook delivery. Refactor the affected backend call paths, including
resolve_project and events::emit usage, to copy or access the needed database
state under the lock, then release it before running external commands or
webhook fan-out; preserve existing results and error handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 95914542-b368-4751-a3e9-5b00962aedb2

📥 Commits

Reviewing files that changed from the base of the PR and between 0ba8be7 and 2ab7ce4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • .gitignore
  • Cargo.toml
  • crates/agentflare-backend/CHANGELOG.md
  • crates/agentflare-backend/Cargo.toml
  • crates/agentflare-backend/src/asset.rs
  • crates/agentflare-backend/src/db.rs
  • crates/agentflare-backend/src/error.rs
  • crates/agentflare-backend/src/events.rs
  • crates/agentflare-backend/src/item.rs
  • crates/agentflare-backend/src/label.rs
  • crates/agentflare-backend/src/lib.rs
  • crates/agentflare-backend/src/project.rs
  • crates/agentflare-backend/src/schema.sql
  • crates/agentflare-backend/src/state.rs
  • crates/agentflare-backend/src/webhook.rs
  • crates/agentflare-backend/src/workspace.rs
  • crates/agentflare-db-kit/CHANGELOG.md
  • crates/agentflare-db-kit/Cargo.toml
  • crates/agentflare-db-kit/src/claim.rs
  • crates/agentflare-db-kit/src/ids.rs
  • crates/agentflare-db-kit/src/lib.rs
  • crates/agentflare-db-kit/src/open.rs
  • src/claims.rs
  • src/mcp_server.rs

Comment thread crates/agentflare-backend/src/asset.rs
Comment thread crates/agentflare-backend/src/item.rs
Comment thread crates/agentflare-backend/src/state.rs
Comment thread crates/agentflare-backend/src/webhook.rs
Comment thread crates/agentflare-backend/src/webhook.rs
Comment thread crates/agentflare-backend/src/webhook.rs
Comment thread crates/agentflare-db-kit/src/claim.rs
Comment thread src/claims.rs
Comment thread src/mcp_server.rs
Comment thread src/mcp_server.rs
Comment on lines +2270 to +2301
#[tool(
description = "Register a webhook that fires on item/state/project changes in the repo's linked workspace. secret is auto-generated if omitted — save the returned value, it isn't shown again."
)]
fn backend_webhook_create(
&self,
Parameters(BackendWebhookCreateRequest {
url,
secret,
on_item,
on_state,
on_project,
}): Parameters<BackendWebhookCreateRequest>,
) -> Result<String, ErrorData> {
if url.trim().is_empty() {
return Err(ErrorData::invalid_params("url is required", None));
}
self.with_backend_db(|conn| {
let project = self.resolve_project(conn)?;
let secret_key = secret.unwrap_or_else(generate_webhook_secret);
let input = agentflare_backend::webhook::CreateWebhook {
workspace_id: project.workspace_id,
url,
secret_key,
on_item,
on_state,
on_project,
};
let webhook =
agentflare_backend::webhook::create(conn, input).map_err(map_backend_err)?;
Ok(serde_json::to_string_pretty(&webhook).unwrap_or_default())
})?
}

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 & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect webhook creation/delivery for SSRF guards.
rg -nP 'fn (create|deliver|validate)\b' crates/agentflare-backend/src/webhook.rs -A15
rg -nP '169\.254|127\.0\.0\.1|loopback|is_private|is_loopback|localhost' crates/agentflare-backend/src/webhook.rs

Repository: getappz/agentflare

Length of output: 1888


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect URL validation and delivery implementation in context.
FILE="crates/agentflare-backend/src/webhook.rs"

# Show the validation function and nearby lines.
sed -n '70,120p' "$FILE"

# Show the delivery implementation where the outbound request is made.
sed -n '271,360p' "$FILE"

# Find any other URL/IP validation helpers in the webhook module.
rg -n 'is_private|is_loopback|localhost|169\.254|127\.0\.0\.1|0\.0\.0\.0|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.' "$FILE"

Repository: getappz/agentflare

Length of output: 5435


Block private and link-local webhook targets. validate_webhook_url only rejects localhost/loopback today; add checks for RFC1918, 169.254.0.0/16, IPv6 link-local/ULA, and hostnames that resolve there before persisting, or webhook deliveries can still reach internal services.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp_server.rs` around lines 2270 - 2301, Update backend_webhook_create to
validate url with validate_webhook_url before creating or persisting the
webhook, expanding validation to reject RFC1918, 169.254.0.0/16, IPv6 link-local
and ULA addresses, plus hostnames resolving to any of those ranges. Return
invalid parameters for blocked targets and preserve the existing project
resolution and webhook creation flow for allowed URLs.

getappz added 3 commits July 13, 2026 11:34
…l ~/.agentflare dir

Non-git directories had no stable "project root" — repo_root() fell back to
raw cwd, so the same non-git project would silently split into multiple
linked projects depending on which subdirectory a tool was called from.

Fixes that with a git-like directory walk-up for non-git projects (looks for
an existing link, then common project markers). But the walk-up's marker was
named .agentflare — the same name as this codebase's existing global
per-user data directory (~/.agentflare, holding agentflare.db, artifacts,
etc.), which exists on every machine. Since that directory always exists,
the walk would eventually find it from almost any non-git location and treat
the user's home directory as "the project root," collapsing every non-git
project on the machine into one and writing project.json alongside the real
global data files.

Renames the per-repo link marker to .agentflare-project (no collision with
the global dir) and additionally bounds the walk-up so it can never reach
the user's home directory at all, regardless of what any marker is named.
…ely on the home boundary

Reverts the .agentflare-project rename from the previous commit — the
home-directory boundary in find_root_from is sufficient on its own to keep
the per-repo link marker from ever colliding with the global ~/.agentflare
data dir, since that directory only ever exists at exactly one path (home
itself), and the walk now never inspects that path at all.

Also fixes a real test-isolation bug this surfaced: find_root_from was
calling crate::paths::home() internally, which reads the AGENTFLARE_HOME_OVERRIDE
env var other tests mutate concurrently under their own lock. That made the
walk-up's result depend on unrelated tests' timing. home is now passed in by
the caller (repo_root() supplies the real one; tests supply an isolated one),
same principle already applied to start.
- asset.rs: reject path traversal in storage_path (write/read/delete_file)
- item.rs: validate state_id belongs to the item's project on create
- state.rs: block deleting a project's default state
- webhook.rs: redact secret_key from Serialize; reveal it once at creation
  in mcp_server.rs instead
- webhook.rs: harden validate_webhook_url against RFC1918/link-local/ULA
  ranges; disable HTTP redirects on delivery; cap logged response bodies
- agentflare-db-kit claim.rs: acquire() reports its own outcome via
  RETURNING instead of a racy follow-up SELECT
- src/claims.rs: scope the git_commit UPDATE to the acquiring owner
- mcp_server.rs: reject blank item_id/label_id on label attach/detach

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/agentflare-backend/src/item.rs (1)

200-268: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve the project/state invariant in update()
UpdateItem.state_id still goes straight into the dynamic UPDATE, so this path can move an item to any existing state, including one from another project. The items.state_id -> states(id) foreign key does not enforce project ownership; add the same project-membership check used by create()/update_state(), or route state changes through update_state(), and cover it with a regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/agentflare-backend/src/item.rs` around lines 200 - 268, The update
function must prevent UpdateItem.state_id from assigning a state belonging to
another project. Before applying the dynamic UPDATE, validate the requested
state against the item’s project using the existing project-membership check
from create() or update_state(), or delegate the state change through
update_state(); reject invalid cross-project states without modifying the item.
Add a regression test covering this scenario.
♻️ Duplicate comments (1)
crates/agentflare-backend/src/webhook.rs (1)

116-160: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

SSRF hardening is still bypassable for domain names — hostname is never resolved/re-checked, only literal "localhost" string and IP literals are blocked.

validate_webhook_url blocks IP-literal hosts via is_blocked_ip and the literal string "localhost", but any other domain (e.g. internal.attacker.com) passes validation untouched. At deliver() time, ureq resolves that domain via DNS and connects to whatever address it points to — including 169.254.169.254, 10.0.0.1, or a DNS-rebound target that resolves differently between validation and delivery. This is the same class of gap the prior review specifically called out: "Resolve and re-check the actual connection target at send time... DNS-rebound hostnames can slip through too." The redirect-following and IP-literal portions of that feedback are now fixed, but the core recommendation to validate the resolved address at connection time remains unaddressed.

To fully close this, resolve the hostname to its IP(s) either at validation time and again immediately before/at send time (or use a custom resolver that runs is_blocked_ip on the connect address, e.g. via ureq's Resolver trait), rejecting delivery if any resolved address is blocked.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/agentflare-backend/src/webhook.rs` around lines 116 - 160, Extend
validate_webhook_url and the deliver flow to resolve domain hosts and apply
is_blocked_ip to every resolved address, both during validation and immediately
before connection. Reject the webhook if any address is blocked, and ensure
delivery uses the checked resolution or a resolver that revalidates the actual
connection target to prevent DNS rebinding.
🧹 Nitpick comments (2)
crates/agentflare-backend/src/webhook.rs (2)

398-422: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good added coverage for private/link-local IP rejection.

Consider adding a case for an IPv4-mapped IPv6 literal (http://[::ffff:127.0.0.1]/) once the bypass above is fixed, to lock in the regression.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/agentflare-backend/src/webhook.rs` around lines 398 - 422, Add an
assertion in validate_url_rejects_private_and_link_local for
http://[::ffff:127.0.0.1]/, expecting validate_webhook_url to return an error,
after fixing the IPv4-mapped IPv6 bypass.

42-56: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

secret_key no longer serialized — resolves prior secret-leak finding.

#[serde(skip_serializing)] prevents secret_key from being emitted by get/list/create JSON responses, matching the MCP layer's create-time-only reveal pattern.

One residual gap: Webhook still derives Debug, and skip_serializing has no effect on {:?} formatting — any future tracing::debug!("{:?}", webhook) or panic-message would still print the secret. Consider a manual Debug impl (or a wrapper) that redacts secret_key to fully close this off defensively.

Example manual Debug redaction
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Clone, Serialize, Deserialize)]
 pub struct Webhook {
     pub id: String,
     pub workspace_id: String,
     pub url: String,
     pub is_active: bool,
     #[serde(skip_serializing)]
     pub secret_key: String,
     pub on_item: bool,
     pub on_state: bool,
     pub on_project: bool,
     pub created_at: i64,
     pub updated_at: i64,
     pub deleted_at: Option<i64>,
 }
+
+impl std::fmt::Debug for Webhook {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Webhook")
+            .field("id", &self.id)
+            .field("workspace_id", &self.workspace_id)
+            .field("url", &self.url)
+            .field("is_active", &self.is_active)
+            .field("secret_key", &"[redacted]")
+            .field("on_item", &self.on_item)
+            .field("on_state", &self.on_state)
+            .field("on_project", &self.on_project)
+            .field("created_at", &self.created_at)
+            .field("updated_at", &self.updated_at)
+            .field("deleted_at", &self.deleted_at)
+            .finish()
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/agentflare-backend/src/webhook.rs` around lines 42 - 56, Replace the
derived Debug implementation on Webhook with a manual implementation that omits
or redacts secret_key while preserving useful fields for diagnostics. Keep serde
serialization behavior unchanged, and ensure all Debug formatting of Webhook
cannot expose the secret.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/agentflare-backend/src/item.rs`:
- Around line 124-132: Update the item creation flow around state::get and
conn.unchecked_transaction so state validation and item insertion occur within
the same transaction. Begin the transaction before validating the state, and use
an immediate or conditional write strategy that prevents a concurrent
soft-delete from allowing insertion of an inactive state.

In `@crates/agentflare-backend/src/webhook.rs`:
- Around line 129-137: Update the IPv6 handling branch in the IP validation
logic to detect mapped addresses with v6.to_ipv4_mapped(), then re-run the
existing IPv4 validation path for the resulting address. Preserve the current
IPv6 checks for non-mapped addresses so loopback and link-local mapped literals
such as ::ffff:127.0.0.1 and ::ffff:169.254.169.254 are rejected.

---

Outside diff comments:
In `@crates/agentflare-backend/src/item.rs`:
- Around line 200-268: The update function must prevent UpdateItem.state_id from
assigning a state belonging to another project. Before applying the dynamic
UPDATE, validate the requested state against the item’s project using the
existing project-membership check from create() or update_state(), or delegate
the state change through update_state(); reject invalid cross-project states
without modifying the item. Add a regression test covering this scenario.

---

Duplicate comments:
In `@crates/agentflare-backend/src/webhook.rs`:
- Around line 116-160: Extend validate_webhook_url and the deliver flow to
resolve domain hosts and apply is_blocked_ip to every resolved address, both
during validation and immediately before connection. Reject the webhook if any
address is blocked, and ensure delivery uses the checked resolution or a
resolver that revalidates the actual connection target to prevent DNS rebinding.

---

Nitpick comments:
In `@crates/agentflare-backend/src/webhook.rs`:
- Around line 398-422: Add an assertion in
validate_url_rejects_private_and_link_local for http://[::ffff:127.0.0.1]/,
expecting validate_webhook_url to return an error, after fixing the IPv4-mapped
IPv6 bypass.
- Around line 42-56: Replace the derived Debug implementation on Webhook with a
manual implementation that omits or redacts secret_key while preserving useful
fields for diagnostics. Keep serde serialization behavior unchanged, and ensure
all Debug formatting of Webhook cannot expose the secret.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9016ff60-19cb-4681-83ba-4779bd0eba20

📥 Commits

Reviewing files that changed from the base of the PR and between d0ac9d3 and 00b1274.

📒 Files selected for processing (7)
  • crates/agentflare-backend/src/asset.rs
  • crates/agentflare-backend/src/item.rs
  • crates/agentflare-backend/src/state.rs
  • crates/agentflare-backend/src/webhook.rs
  • crates/agentflare-db-kit/src/claim.rs
  • src/claims.rs
  • src/mcp_server.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/claims.rs
  • crates/agentflare-backend/src/state.rs
  • src/mcp_server.rs
  • crates/agentflare-backend/src/asset.rs
  • crates/agentflare-db-kit/src/claim.rs

Comment on lines +124 to +132
let state = crate::state::get(conn, &input.state_id)?;
if state.project_id != input.project_id {
return Err(crate::error::Error::InvalidTransition(format!(
"state {} belongs to a different project than project {}",
input.state_id, input.project_id
)));
}

let tx = conn.unchecked_transaction()?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make state validation and insertion atomic.

state::get runs before unchecked_transaction(). Since states are soft-deleted, another connection can delete the state after validation but before the item insert. Start the transaction before validation—preferably with an immediate/conditional write—or enforce the active-state predicate in the insert itself.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/agentflare-backend/src/item.rs` around lines 124 - 132, Update the
item creation flow around state::get and conn.unchecked_transaction so state
validation and item insertion occur within the same transaction. Begin the
transaction before validating the state, and use an immediate or conditional
write strategy that prevents a concurrent soft-delete from allowing insertion of
an inactive state.

Comment on lines +129 to +137
std::net::IpAddr::V6(v6) => {
let seg0 = v6.segments()[0];
v6.is_loopback()
|| v6.is_unspecified()
|| v6.is_multicast()
|| (seg0 & 0xffc0) == 0xfe80 // link-local fe80::/10
|| (seg0 & 0xfe00) == 0xfc00 // unique local fc00::/7
}
}

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 & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and nearby context.
git ls-files crates/agentflare-backend/src/webhook.rs
wc -l crates/agentflare-backend/src/webhook.rs
sed -n '1,220p' crates/agentflare-backend/src/webhook.rs

Repository: getappz/agentflare

Length of output: 7670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for related URL/IP validation code and tests.
rg -n "to_ipv4_mapped|is_blocked_ip|validate_webhook_url|Ipv6Addr|Host::Ipv6|localhost|169\.254|127\.0\.0\.1" crates/agentflare-backend/src crates/agentflare-backend -g '!target'

Repository: getappz/agentflare

Length of output: 5792


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the existing tests around webhook validation.
sed -n '380,450p' crates/agentflare-backend/src/webhook.rs

Repository: getappz/agentflare

Length of output: 2567


Block IPv4-mapped IPv6 literals here.
::ffff:127.0.0.1 and ::ffff:169.254.169.254 still arrive as Host::Ipv6, but this branch never converts them to IPv4, so the loopback/private/link-local checks are skipped. Map them with to_ipv4_mapped() and re-run the IPv4 path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/agentflare-backend/src/webhook.rs` around lines 129 - 137, Update the
IPv6 handling branch in the IP validation logic to detect mapped addresses with
v6.to_ipv4_mapped(), then re-run the existing IPv4 validation path for the
resulting address. Preserve the current IPv6 checks for non-mapped addresses so
loopback and link-local mapped literals such as ::ffff:127.0.0.1 and
::ffff:169.254.169.254 are rejected.

…ict with ci/disable-publish

Root Cargo.toml conflicted on the dependency block: master (via #164) had
already dropped version pins from internal path deps. Applied the same
treatment to the newly added agentflare-backend/db_kit deps, and added
publish = false to both crates' own Cargo.toml for consistency.
@getappz
getappz merged commit 731ae3e into master Jul 13, 2026
15 checks passed
@getappz
getappz deleted the feat/agentflare-backend branch July 13, 2026 08:52
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.

1 participant