mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-11 03:40:05 +00:00
2632 lines
No EOL
422 KiB
JSON
2632 lines
No EOL
422 KiB
JSON
{
|
|
"title": "refactor: Remove IP allowlisting",
|
|
"spec": {
|
|
"run_id": "01KSNYVQCKXPCSCEGWMZ1Q574Z",
|
|
"settings": {
|
|
"project": {
|
|
"name": null,
|
|
"description": null,
|
|
"metadata": {}
|
|
},
|
|
"workflow": {
|
|
"name": null,
|
|
"description": null,
|
|
"graph": "workflow.fabro",
|
|
"metadata": {}
|
|
},
|
|
"run": {
|
|
"goal": {
|
|
"type": "inline",
|
|
"value": "---\ntitle: \"refactor: Remove IP allowlisting\"\ntype: refactor\nstatus: active\ndate: 2026-05-27\n---\n\n# refactor: Remove IP allowlisting\n\n## Overview\n\nRemove Fabro's inbound server IP allowlisting feature completely. This removes the\nserver-wide `[server.ip_allowlist]` setting, the GitHub webhook-specific\n`[server.integrations.github.webhooks.ip_allowlist]` overlay, request middleware,\nclient-IP extraction, GitHub `/meta` hook-range expansion, settings API exposure,\ngenerated API client types, and the Settings > Security UI display.\n\nExisting configs that still contain removed IP allowlist keys should fail as\nunknown fields. This is an intentional hard removal, not a compatibility\ndeprecation.\n\n## Problem Frame\n\nThe feature is being removed from Fabro rather than maintained as an in-process\nnetwork access-control layer. Network source restrictions should be handled\noutside Fabro by reverse proxies, firewalls, VPNs, Tailscale, platform ingress,\nor other deployment-layer controls.\n\n## Requirements Trace\n\n- R1. Remove all runtime enforcement of inbound source-IP allowlisting from web,\n API, static asset, and GitHub webhook routes.\n- R2. Remove the server settings schema for global and GitHub webhook IP\n allowlists.\n- R3. Remove `IpAllowEntry` and related API schema/client types from public\n settings payloads.\n- R4. Keep unrelated allowlists intact: GitHub username allowlists, sandbox\n egress CIDR allow lists, and internal policy/test allowlists.\n- R5. Preserve existing authentication, GitHub webhook HMAC verification,\n routing, health checks, request logging, and settings hot-reload behavior.\n- R6. Treat old IP allowlist config as invalid after removal.\n- R7. Update operator-facing docs/changelog so users know to move source-IP\n restrictions upstream.\n\n## Scope Boundaries\n\n- Do not remove `[server.auth.github].allowed_usernames`.\n- Do not remove Daytona/sandbox `cidr_allow_list` network policy support.\n- Do not remove generic uses of \"allowlist\" in policy tests, markdown rendering,\n model controls, or other unrelated domains.\n- Do not add a migration, warning-only parser, or fallback compatibility path for\n old IP allowlist settings.\n- Do not change GitHub webhook signature verification or webhook route strategy.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- Settings use sparse config layers in `lib/crates/fabro-config/src/layers/`\n and dense resolved types in `lib/crates/fabro-types/src/settings/`.\n- The OpenAPI spec at `docs/public/api-reference/fabro-api.yaml` is the source\n of truth for API wire shape; `cargo build -p fabro-api` regenerates Rust API\n code, and `cd lib/packages/fabro-api-client && bun run generate` regenerates\n the TypeScript client.\n- `lib/crates/fabro-server/src/server.rs` builds the router and currently wires\n IP allowlist middleware before other route dispatch behavior.\n- `lib/crates/fabro-server/src/serve.rs` currently resolves server and webhook\n allowlist configs at startup and creates a GitHub `/meta` resolver.\n- `apps/fabro-web/app/routes/settings-security.tsx` displays the settings API's\n `server.ip_allowlist` state.\n\n### Main Removal Surfaces\n\n- Config/types: `fabro-types`, `fabro-config`, config tests.\n- Server runtime: `fabro-server/src/ip_allowlist.rs`, router wiring, startup\n wiring, test helpers, routing/TCP tests.\n- API/contracts: OpenAPI schema, `fabro-api` type replacements/exports, generated\n TypeScript API client models.\n- Frontend/docs: Settings > Security copy, settings navigation copy, changelog.\n\n## Key Technical Decisions\n\n- **Hard removal:** Removed config keys should fail through existing\n `deny_unknown_fields` behavior. This keeps the change simple and makes stale\n deployment config visible immediately.\n- **Delete, do not stub:** Remove `IpAllowlistConfig` and middleware parameters\n instead of passing default empty configs through the router. Empty stubs would\n keep the feature shape alive and make future cleanup harder.\n- **Keep webhook auth unchanged:** GitHub webhook source-IP filtering goes away,\n but HMAC signature verification remains the security boundary for webhook\n payload authenticity.\n- **Generated clients follow OpenAPI:** Update the OpenAPI schemas first, then\n regenerate Rust and TypeScript API outputs rather than hand-editing generated\n client files except as a short-term cleanup if generation leaves stale exports.\n- **Docs mention upstream controls:** The changelog and security guidance should\n direct operators to network-layer controls, not a replacement Fabro setting.\n\n## Implementation Units\n\n- [ ] **Unit 1: Remove config schema and resolved types**\n\n**Goal:** Delete the IP allowlist settings contract from config parsing and dense\nserver settings.\n\n**Requirements:** R2, R3, R4, R6\n\n**Dependencies:** None\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/settings/server.rs`\n- Modify: `lib/crates/fabro-types/src/settings/mod.rs`\n- Modify: `lib/crates/fabro-types/Cargo.toml`\n- Modify: `lib/crates/fabro-config/src/layers/server.rs`\n- Modify: `lib/crates/fabro-config/src/layers/mod.rs`\n- Modify: `lib/crates/fabro-config/src/lib.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/server.rs`\n- Modify: `lib/crates/fabro-config/src/tests/resolve_server.rs`\n\n**Approach:**\n- Remove `ServerNamespace.ip_allowlist` and its `test_default()` population.\n- Delete `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, and\n `IpAllowEntry`.\n- Remove `ServerLayer.ip_allowlist`, `ServerIpAllowlistLayer`,\n `ServerIpAllowlistOverrideLayer`, and `IntegrationWebhooksLayer.ip_allowlist`.\n- Remove resolver functions and validation for global and webhook IP allowlists,\n including `github_meta_hooks` parsing and Unix socket trusted-proxy checks.\n- Remove `ipnet` from `fabro-types`. Keep `ipnet` in `fabro-config` because\n `resolve/environment.rs` still validates sandbox CIDR policy.\n- Replace old positive/negative IP allowlist config tests with unknown-field\n tests for `[server.ip_allowlist]` and\n `[server.integrations.github.webhooks.ip_allowlist]`.\n\n**Patterns to follow:**\n- Existing unknown-field tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`\n for retired server settings.\n- Existing `ServerLayer`/`ServerNamespace` layer-to-resolved pattern for removing\n a server subdomain cleanly.\n\n**Test scenarios:**\n- Error path: TOML with `[server.ip_allowlist]` fails parsing/resolution with an\n unknown-field diagnostic mentioning `ip_allowlist`.\n- Error path: TOML with `[server.integrations.github.webhooks.ip_allowlist]`\n fails with an unknown-field diagnostic mentioning `ip_allowlist`.\n- Happy path: minimal valid server settings still resolve without any\n `ip_allowlist` field.\n- Integration: serialized `ServerSettings` JSON no longer includes\n `server.ip_allowlist`.\n\n**Verification:**\n- `fabro-config` and `fabro-types` compile without removed IP allowlist symbols.\n- Config tests prove stale allowlist settings are rejected.\n\n- [ ] **Unit 2: Remove server middleware and startup resolution**\n\n**Goal:** Remove all runtime source-IP filtering and GitHub `/meta` range\nresolution from `fabro-server`.\n\n**Requirements:** R1, R4, R5\n\n**Dependencies:** Unit 1\n\n**Files:**\n- Delete: `lib/crates/fabro-server/src/ip_allowlist.rs`\n- Modify: `lib/crates/fabro-server/src/lib.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Modify: `lib/crates/fabro-server/src/serve.rs`\n- Modify: `lib/crates/fabro-server/src/test_support.rs`\n- Modify: `lib/crates/fabro-server/Cargo.toml`\n- Modify: `lib/crates/fabro-server/src/auth/translate.rs`\n- Modify: `lib/crates/fabro-server/src/web_auth.rs`\n- Modify: `lib/crates/fabro-cli/tests/it/support/auth_harness.rs`\n\n**Approach:**\n- Remove the public `ip_allowlist` module export.\n- Delete `IpAllowlistConfig`, `IpAllowlist`, `GitHubMetaResolver`, client-IP\n extraction, middleware, and GitHub meta cache helpers.\n- Simplify `build_router_with_options` by removing its `Arc<IpAllowlistConfig>`\n parameter and removing `RouterOptions.github_webhook_ip_allowlist`.\n- Remove the global allowlist middleware layer from the main app router.\n- Simplify `github_webhook_routes` so it only receives the webhook secret and no\n route-specific allowlist config.\n- Remove startup creation of `GitHubMetaResolver`, `default_ip_allowlist`,\n `resolve_github_webhook_ip_allowlist`, and\n `resolve_startup_github_webhook_ip_allowlist`.\n- Replace all test/helper call sites that pass `IpAllowlistConfig::default()`\n with the simplified router signature.\n- Remove `ipnet` and any now-unused HTTP mocking/test-only dependencies from\n `fabro-server` if they are only used by the deleted module.\n- Consider whether `serve.rs` still needs `SocketAddr` for TCP\n `ConnectInfo`; if it was only present for allowlisting tests, remove the\n connect-info service wrapper and imports.\n\n**Patterns to follow:**\n- Keep router layers ordered as they are after the allowlist layer is removed:\n auth translation, demo routing, auth extension, canonical host, security\n headers, HTTP logging, request ID.\n- Existing webhook route HMAC tests and auth tests should remain the source of\n truth for webhook behavior.\n\n**Test scenarios:**\n- Happy path: API requests from any TCP remote address route normally when auth\n requirements are satisfied.\n- Happy path: `/health` remains accessible.\n- Integration: GitHub webhook route still rejects missing/invalid signatures and\n accepts valid signatures exactly as before.\n- Cleanup: there are no references to `IpAllowlistConfig`, `ip_allowlist_middleware`,\n `GitHubMetaResolver`, `github_meta_hooks`, or `github-meta-hooks.json`.\n\n**Verification:**\n- `fabro-server` and CLI auth harness tests compile against the simplified router\n API.\n- Runtime startup no longer performs any GitHub `/meta` fetch for webhook IP\n ranges.\n\n- [ ] **Unit 3: Update OpenAPI and generated API clients**\n\n**Goal:** Remove IP allowlist fields and schemas from public settings API\ncontracts and regenerated clients.\n\n**Requirements:** R3, R4\n\n**Dependencies:** Units 1 and 2\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs`\n- Modify/generated: `lib/packages/fabro-api-client/src/models/*`\n\n**Approach:**\n- Remove `ip_allowlist` from `ServerNamespace.required` and\n `ServerNamespace.properties`.\n- Remove `ServerIpAllowlistSettings`,\n `ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,\n `LiteralIpAllowEntry`, and `GitHubMetaHooksEntry` schemas.\n- Remove `IntegrationWebhooksSettings.ip_allowlist` from required/properties.\n- Remove `with_replacement` entries and public re-exports for removed types in\n `fabro-api`.\n- Regenerate Rust API code by building `fabro-api`.\n- Regenerate TypeScript Axios client and ensure stale allowlist model files and\n index exports are gone.\n- Strengthen round-trip tests to assert settings JSON omits\n `server.ip_allowlist` and webhook `ip_allowlist`.\n\n**Patterns to follow:**\n- Existing OpenAPI-first workflow in `AGENTS.md`.\n- Existing `server_settings_family_reuses_domain_types` assertions for shared\n domain type identity.\n\n**Test scenarios:**\n- Contract: `ServerSettings` round-trips through API types without any IP\n allowlist field.\n- Contract: OpenAPI-generated TypeScript `ServerNamespace` has no\n `ip_allowlist` property.\n- Contract: `IntegrationWebhooksSettings` has only webhook strategy fields after\n removal.\n\n**Verification:**\n- Generated Rust and TypeScript clients match the updated OpenAPI spec.\n- `rg \"ServerIpAllowlist|IpAllowEntry|server-ip-allowlist|literal-ip-allow-entry\"`\n finds no remaining generated/public API references.\n\n- [ ] **Unit 4: Update web settings UI**\n\n**Goal:** Remove IP allowlist display from Settings > Security and align copy\nwith the new settings shape.\n\n**Requirements:** R3, R7\n\n**Dependencies:** Unit 3\n\n**Files:**\n- Modify: `apps/fabro-web/app/routes/settings-security.tsx`\n- Modify: `apps/fabro-web/app/routes/settings.tsx`\n\n**Approach:**\n- Change the Security page description from \"Authentication methods and network\n allowlist\" to authentication-only wording.\n- Remove destructuring and rendering of `settings.server.ip_allowlist`.\n- Remove the unused `Count` and `plural` imports if no longer used.\n- Update the Settings nav description for Security from \"Authentication and\n network allowlist\" to authentication-focused copy.\n\n**Patterns to follow:**\n- Existing `settings-panel` row layout for Auth methods and Allowed usernames.\n\n**Test scenarios:**\n- Typecheck: the route compiles against the regenerated API client with no\n `server.ip_allowlist` property.\n- UI behavior: Security page still renders auth methods and allowed usernames.\n- Cleanup: no frontend references to `ip_allowlist`, `IP allowlist`, or\n `trusted_proxy_count` remain.\n\n**Verification:**\n- `apps/fabro-web` typecheck passes against the new generated client.\n\n- [ ] **Unit 5: Update tests, docs, changelog, and dependency lockfile**\n\n**Goal:** Remove stale references and document the operator-facing behavior\nchange.\n\n**Requirements:** R4, R6, R7\n\n**Dependencies:** Units 1 through 4\n\n**Files:**\n- Modify: `lib/crates/fabro-server/tests/it/api/routing.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/tcp.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/settings.rs`\n- Modify: `docs/public/administration/security.mdx`\n- Create or modify: `docs/public/changelog/2026-05-27.mdx`\n- Modify: `Cargo.lock` if dependency graph changes\n\n**Approach:**\n- Delete route/TCP tests that only prove IP allowlist enforcement or\n `ConnectInfo` behavior.\n- Adjust any router setup helpers after the signature simplification.\n- Add a settings API assertion that `server.ip_allowlist` is absent from\n `/api/v1/settings`.\n- Update security docs to explicitly state that Fabro does not provide inbound\n source-IP allowlisting and operators should use upstream network controls.\n- Add a changelog entry dated 2026-05-27 describing the hard removal and the\n expected replacement at the deployment layer.\n- Run dependency resolution after removing direct `ipnet`/test dependencies;\n keep transitive or unrelated `ipnet` entries needed by sandbox CIDR validation.\n\n**Patterns to follow:**\n- Existing changelog style in `docs/public/changelog/2026-05-26.mdx`.\n- Existing settings API integration test style in\n `lib/crates/fabro-server/tests/it/api/settings.rs`.\n\n**Test scenarios:**\n- Settings API: response contains server auth, listen, storage, scheduler, and\n integrations fields, but not `server.ip_allowlist`.\n- Config compatibility: old IP allowlist TOML fails before startup rather than\n being silently ignored.\n- Docs validation: security docs no longer imply Fabro can restrict inbound\n source IPs internally.\n\n**Verification:**\n- `rg -n \"ip_allowlist|trusted_proxy_count|github_meta_hooks|IP allowlist|ip allowlist\"`\n returns only historical archived plan/brainstorm/spec references or unrelated\n non-server allowlist text.\n\n## System-Wide Impact\n\n- **Public API:** `GET /api/v1/settings` response shape changes by removing\n `server.ip_allowlist` and webhook `ip_allowlist`.\n- **Config compatibility:** Existing `settings.toml` files containing the removed\n keys become invalid. This is intentional.\n- **Runtime security posture:** Fabro no longer blocks requests based on source\n IP. Operators must enforce network source restrictions upstream.\n- **Webhook handling:** GitHub webhook HMAC verification remains unchanged; only\n optional source-IP filtering is removed.\n- **Generated clients:** Downstream TypeScript/Rust consumers that read\n `server.ip_allowlist` must update.\n\n## Risks & Mitigations\n\n| Risk | Mitigation |\n|------|------------|\n| Operators accidentally expose a server that previously relied on Fabro IP allowlisting | Changelog and security docs explicitly call out the removal and direct operators to upstream controls. |\n| Generated API clients retain stale types | Update OpenAPI first, regenerate both Rust and TypeScript clients, and run search checks for stale symbols. |\n| Unrelated allowlist functionality is removed by broad search/replace | Scope searches to `IpAllow`, `ip_allowlist`, `trusted_proxy_count`, and `github_meta_hooks`; preserve sandbox CIDR and GitHub username allowlists. |\n| Router signature cleanup breaks many tests | Update shared test helpers first, then compile-driven cleanup of remaining call sites. |\n| `ipnet` is removed where still needed | Keep `fabro-config` dependency if environment CIDR validation still imports `ipnet::IpNet`. |\n\n## Test Plan\n\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun test`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n\n## Assumptions\n\n- The accepted compatibility policy is hard removal: no warning-only parser, no\n migration, and no custom compatibility error path.\n- Public API consumers can tolerate a breaking settings shape change for this\n removed feature.\n- Historical documents in `docs/plans/`, `docs/brainstorms/`, and\n `docs/superpowers/` are archival and do not need rewriting.\n\n## Sources & References\n\n- Previous feature requirements:\n `docs/brainstorms/2026-04-15-ip-whitelist-requirements.md`\n- Previous implementation plan:\n `docs/plans/2026-04-15-002-feat-ip-allowlist-plan.md`\n- API workflow guidance: `AGENTS.md`\n- OpenAPI source of truth: `docs/public/api-reference/fabro-api.yaml`\n"
|
|
},
|
|
"working_dir": null,
|
|
"metadata": {},
|
|
"inputs": {},
|
|
"model": {
|
|
"provider": "anthropic",
|
|
"name": "claude-sonnet-4-6",
|
|
"fallbacks": [],
|
|
"controls": {
|
|
"reasoning_effort": null,
|
|
"speed": null
|
|
}
|
|
},
|
|
"git": {
|
|
"author": null
|
|
},
|
|
"prepare": {
|
|
"commands": [],
|
|
"timeout_ms": 300000
|
|
},
|
|
"execution": {
|
|
"mode": "normal",
|
|
"approval": "prompt"
|
|
},
|
|
"checkpoint": {
|
|
"exclude_globs": [],
|
|
"skip_git_hooks": false
|
|
},
|
|
"clone": {
|
|
"enabled": true
|
|
},
|
|
"run_branch": {
|
|
"enabled": true,
|
|
"push": true
|
|
},
|
|
"meta_branch": {
|
|
"enabled": true,
|
|
"push": true
|
|
},
|
|
"environment": {
|
|
"id": "fabro-dev",
|
|
"provider": "daytona",
|
|
"image": {
|
|
"docker": null,
|
|
"dockerfile": {
|
|
"type": "inline",
|
|
"value": "FROM ubuntu:24.04\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n curl git ripgrep ca-certificates build-essential pkg-config libssl-dev unzip python3 \\\n xvfb xfce4 xfce4-terminal x11vnc novnc dbus-x11 \\\n libx11-6 libxrandr2 libxext6 libxrender1 libxfixes3 libxss1 libxtst6 libxi6 \\\n && rm -rf /var/lib/apt/lists/*\n\n# Install real Chromium (not the snap stub) via xtradeb PPA\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n software-properties-common curl gnupg \\\n && add-apt-repository -y ppa:xtradeb/apps \\\n && apt-get update \\\n && apt-get install -y --no-install-recommends chromium \\\n && rm -rf /var/lib/apt/lists/*\n\n# Wrapper: Chromium needs --no-sandbox when running as root in a container,\n# and --disable-dev-shm-usage avoids crashes from small /dev/shm\nRUN printf '#!/bin/bash\\nexec /usr/bin/chromium --no-sandbox --disable-dev-shm-usage \"$@\"\\n' \\\n > /usr/local/bin/chromium-wrapper \\\n && chmod +x /usr/local/bin/chromium-wrapper\n\n# Make the wrapper the default in the system .desktop file and via alternatives\nRUN sed -i 's|^Exec=.*|Exec=/usr/local/bin/chromium-wrapper %U|' \\\n /usr/share/applications/chromium.desktop \\\n && update-alternatives --install /usr/bin/x-www-browser x-www-browser \\\n /usr/local/bin/chromium-wrapper 100\n\n# Tell XFCE's exo-open that Chromium is the WebBrowser helper (system-wide)\nRUN mkdir -p /etc/xdg/xfce4 /usr/share/xfce4/helpers \\\n && printf 'WebBrowser=custom-WebBrowser\\n' > /etc/xdg/xfce4/helpers.rc \\\n && printf '[Desktop Entry]\\n\\\nVersion=1.0\\n\\\nType=X-XFCE-Helper\\n\\\nName=Chromium\\n\\\nIcon=chromium\\n\\\nX-XFCE-Category=WebBrowser\\n\\\nX-XFCE-CommandsWithParameter=/usr/local/bin/chromium-wrapper \"%%s\"\\n\\\nX-XFCE-Commands=/usr/local/bin/chromium-wrapper\\n' \\\n > /usr/share/xfce4/helpers/custom-WebBrowser.desktop\n\n# GitHub CLI\nRUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \\\n | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \\\n && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" \\\n | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \\\n && apt-get update && apt-get install -y --no-install-recommends gh \\\n && rm -rf /var/lib/apt/lists/*\n\n# Rust\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\nENV PATH=\"/root/.cargo/bin:${PATH}\"\nRUN rustup toolchain install nightly-2026-04-14 --profile minimal --component clippy,rustfmt\nRUN cargo install cargo-nextest --locked\nENV CARGO_INCREMENTAL=0\n\n# Bun\nRUN curl -fsSL https://bun.sh/install | bash\nENV PATH=\"/root/.bun/bin:${PATH}\"\n\nWORKDIR /root\n"
|
|
}
|
|
},
|
|
"resources": {
|
|
"cpu": 8,
|
|
"memory": "16GB",
|
|
"disk": "20GB"
|
|
},
|
|
"network": {
|
|
"mode": "allow_all",
|
|
"allow": []
|
|
},
|
|
"lifecycle": {
|
|
"preserve": false,
|
|
"stop_on_terminal": true,
|
|
"auto_stop": "30m"
|
|
},
|
|
"labels": {
|
|
"repo": "fabro-sh/fabro"
|
|
},
|
|
"volumes": [],
|
|
"env": {}
|
|
},
|
|
"notifications": {
|
|
"feed": {
|
|
"enabled": true,
|
|
"provider": "slack",
|
|
"events": [
|
|
"run.started",
|
|
"run.completed",
|
|
"run.failed"
|
|
],
|
|
"slack": {
|
|
"channel": "#feed-fabro"
|
|
}
|
|
}
|
|
},
|
|
"interviews": {
|
|
"provider": null,
|
|
"slack": null
|
|
},
|
|
"agent": {
|
|
"fabro_tools": false,
|
|
"permissions": null,
|
|
"mcps": {}
|
|
},
|
|
"hooks": [],
|
|
"scm": {
|
|
"provider": null,
|
|
"owner": null,
|
|
"repository": null,
|
|
"github": null
|
|
},
|
|
"pull_request": {
|
|
"enabled": true,
|
|
"draft": false,
|
|
"auto_merge": false,
|
|
"merge_strategy": "squash"
|
|
},
|
|
"artifacts": {
|
|
"include": []
|
|
},
|
|
"integrations": {
|
|
"github": {
|
|
"permissions": {}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"graph": {
|
|
"name": "ImplementPlan",
|
|
"nodes": {
|
|
"fixup": {
|
|
"id": "fixup",
|
|
"attrs": {
|
|
"prompt": {
|
|
"String": "The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures."
|
|
},
|
|
"max_visits": {
|
|
"Integer": 3
|
|
},
|
|
"label": {
|
|
"String": "Fixup"
|
|
},
|
|
"model": {
|
|
"String": "claude-opus-4-7"
|
|
},
|
|
"provider": {
|
|
"String": "anthropic"
|
|
}
|
|
}
|
|
},
|
|
"simplify_opus": {
|
|
"id": "simplify_opus",
|
|
"attrs": {
|
|
"model": {
|
|
"String": "claude-opus-4-7"
|
|
},
|
|
"prompt": {
|
|
"String": "# Simplify: Code Review and Cleanup\n\nReview changes vs. origin for reuse, quality, and efficiency. Fix any issues found.\n\n## Phase 1: Identify Changes\n\nRun git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.\n\n## Phase 2: Launch Three Review Agents in Parallel\n\nUse the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.\n\n### Agent 1: Code Reuse Review\n\nFor each change:\n\n1. Search for existing utilities and helpers that could replace newly written code. Use Grep to find similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.\n2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.\n3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.\n\nNote: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it.\n\n### Agent 2: Code Quality Review\n\nReview the same changes for hacky patterns:\n\n1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls\n2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones\n3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction\n4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries\n5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase\n\nNote: This is a greenfield app, so be aggressive in optimizing quality.\n\n### Agent 3: Efficiency Review\n\nReview the same changes for efficiency:\n\n1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns\n2. Missed concurrency: independent operations run sequentially when they could run in parallel\n3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths\n4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error\n5. Memory: unbounded data structures, missing cleanup, event listener leaks\n6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one\n\n## Phase 3: Fix Issues\n\nWait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.\n\nWhen done, briefly summarize what was fixed (or confirm the code was already clean)."
|
|
},
|
|
"provider": {
|
|
"String": "anthropic"
|
|
},
|
|
"label": {
|
|
"String": "Simplify (Opus)"
|
|
}
|
|
}
|
|
},
|
|
"exit": {
|
|
"id": "exit",
|
|
"attrs": {
|
|
"shape": {
|
|
"String": "Msquare"
|
|
},
|
|
"label": {
|
|
"String": "Exit"
|
|
},
|
|
"provider": {
|
|
"String": "anthropic"
|
|
},
|
|
"model": {
|
|
"String": "claude-opus-4-7"
|
|
}
|
|
}
|
|
},
|
|
"simplify_gpt": {
|
|
"id": "simplify_gpt",
|
|
"attrs": {
|
|
"prompt": {
|
|
"String": "# Simplify: Code Review and Cleanup\n\nReview changes vs. origin for reuse, quality, and efficiency. Fix any issues found.\n\n## Phase 1: Identify Changes\n\nRun git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.\n\n## Phase 2: Launch Three Review Agents in Parallel\n\nUse the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.\n\n### Agent 1: Code Reuse Review\n\nFor each change:\n\n1. Search for existing utilities and helpers that could replace newly written code. Use Grep to find similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.\n2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.\n3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.\n\nNote: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it.\n\n### Agent 2: Code Quality Review\n\nReview the same changes for hacky patterns:\n\n1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls\n2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones\n3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction\n4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries\n5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase\n\nNote: This is a greenfield app, so be aggressive in optimizing quality.\n\n### Agent 3: Efficiency Review\n\nReview the same changes for efficiency:\n\n1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns\n2. Missed concurrency: independent operations run sequentially when they could run in parallel\n3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths\n4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error\n5. Memory: unbounded data structures, missing cleanup, event listener leaks\n6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one\n\n## Phase 3: Fix Issues\n\nWait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.\n\nWhen done, briefly summarize what was fixed (or confirm the code was already clean)."
|
|
},
|
|
"label": {
|
|
"String": "Simplify (GPT-55)"
|
|
},
|
|
"provider": {
|
|
"String": "openai"
|
|
},
|
|
"model": {
|
|
"String": "gpt-5.5"
|
|
}
|
|
}
|
|
},
|
|
"implement": {
|
|
"id": "implement",
|
|
"attrs": {
|
|
"provider": {
|
|
"String": "openai"
|
|
},
|
|
"model": {
|
|
"String": "gpt-5.5"
|
|
},
|
|
"prompt": {
|
|
"String": "Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."
|
|
},
|
|
"label": {
|
|
"String": "Implement"
|
|
},
|
|
"reasoning_effort": {
|
|
"String": "xhigh"
|
|
}
|
|
}
|
|
},
|
|
"preflight_compile": {
|
|
"id": "preflight_compile",
|
|
"attrs": {
|
|
"shape": {
|
|
"String": "parallelogram"
|
|
},
|
|
"provider": {
|
|
"String": "anthropic"
|
|
},
|
|
"script": {
|
|
"String": "cargo check -q --workspace 2>&1"
|
|
},
|
|
"max_retries": {
|
|
"Integer": 0
|
|
},
|
|
"label": {
|
|
"String": "Preflight Compile"
|
|
},
|
|
"model": {
|
|
"String": "claude-opus-4-7"
|
|
}
|
|
}
|
|
},
|
|
"verify": {
|
|
"id": "verify",
|
|
"attrs": {
|
|
"script": {
|
|
"String": "git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1"
|
|
},
|
|
"retry_target": {
|
|
"String": "fixup"
|
|
},
|
|
"model": {
|
|
"String": "claude-opus-4-7"
|
|
},
|
|
"shape": {
|
|
"String": "parallelogram"
|
|
},
|
|
"goal_gate": {
|
|
"Boolean": true
|
|
},
|
|
"provider": {
|
|
"String": "anthropic"
|
|
},
|
|
"label": {
|
|
"String": "Verify"
|
|
}
|
|
}
|
|
},
|
|
"fix_lints": {
|
|
"id": "fix_lints",
|
|
"attrs": {
|
|
"prompt": {
|
|
"String": "The preflight lint step failed. Read the build output from context and fix all clippy lint warnings."
|
|
},
|
|
"label": {
|
|
"String": "Fix Lints"
|
|
},
|
|
"provider": {
|
|
"String": "anthropic"
|
|
},
|
|
"model": {
|
|
"String": "claude-opus-4-7"
|
|
},
|
|
"max_visits": {
|
|
"Integer": 3
|
|
}
|
|
}
|
|
},
|
|
"toolchain": {
|
|
"id": "toolchain",
|
|
"attrs": {
|
|
"label": {
|
|
"String": "Toolchain"
|
|
},
|
|
"max_retries": {
|
|
"Integer": 0
|
|
},
|
|
"shape": {
|
|
"String": "parallelogram"
|
|
},
|
|
"model": {
|
|
"String": "claude-opus-4-7"
|
|
},
|
|
"script": {
|
|
"String": "command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1"
|
|
},
|
|
"provider": {
|
|
"String": "anthropic"
|
|
}
|
|
}
|
|
},
|
|
"preflight_lint": {
|
|
"id": "preflight_lint",
|
|
"attrs": {
|
|
"model": {
|
|
"String": "claude-opus-4-7"
|
|
},
|
|
"shape": {
|
|
"String": "parallelogram"
|
|
},
|
|
"provider": {
|
|
"String": "anthropic"
|
|
},
|
|
"max_retries": {
|
|
"Integer": 0
|
|
},
|
|
"label": {
|
|
"String": "Preflight Lint"
|
|
},
|
|
"script": {
|
|
"String": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1"
|
|
}
|
|
}
|
|
},
|
|
"start": {
|
|
"id": "start",
|
|
"attrs": {
|
|
"provider": {
|
|
"String": "anthropic"
|
|
},
|
|
"model": {
|
|
"String": "claude-opus-4-7"
|
|
},
|
|
"shape": {
|
|
"String": "Mdiamond"
|
|
},
|
|
"label": {
|
|
"String": "Start"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"edges": [
|
|
{
|
|
"from": "start",
|
|
"to": "toolchain",
|
|
"attrs": {}
|
|
},
|
|
{
|
|
"from": "toolchain",
|
|
"to": "preflight_compile",
|
|
"attrs": {
|
|
"condition": {
|
|
"String": "outcome=succeeded"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"from": "toolchain",
|
|
"to": "exit",
|
|
"attrs": {}
|
|
},
|
|
{
|
|
"from": "preflight_compile",
|
|
"to": "preflight_lint",
|
|
"attrs": {
|
|
"condition": {
|
|
"String": "outcome=succeeded"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"from": "preflight_compile",
|
|
"to": "exit",
|
|
"attrs": {}
|
|
},
|
|
{
|
|
"from": "preflight_lint",
|
|
"to": "implement",
|
|
"attrs": {
|
|
"condition": {
|
|
"String": "outcome=succeeded"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"from": "preflight_lint",
|
|
"to": "fix_lints",
|
|
"attrs": {}
|
|
},
|
|
{
|
|
"from": "fix_lints",
|
|
"to": "preflight_lint",
|
|
"attrs": {}
|
|
},
|
|
{
|
|
"from": "implement",
|
|
"to": "simplify_opus",
|
|
"attrs": {}
|
|
},
|
|
{
|
|
"from": "simplify_opus",
|
|
"to": "simplify_gpt",
|
|
"attrs": {}
|
|
},
|
|
{
|
|
"from": "simplify_gpt",
|
|
"to": "verify",
|
|
"attrs": {}
|
|
},
|
|
{
|
|
"from": "verify",
|
|
"to": "exit",
|
|
"attrs": {
|
|
"condition": {
|
|
"String": "outcome=succeeded"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"from": "verify",
|
|
"to": "fixup",
|
|
"attrs": {}
|
|
},
|
|
{
|
|
"from": "fixup",
|
|
"to": "verify",
|
|
"attrs": {}
|
|
}
|
|
],
|
|
"attrs": {
|
|
"rankdir": {
|
|
"String": "LR"
|
|
},
|
|
"goal": {
|
|
"String": "---\ntitle: \"refactor: Remove IP allowlisting\"\ntype: refactor\nstatus: active\ndate: 2026-05-27\n---\n\n# refactor: Remove IP allowlisting\n\n## Overview\n\nRemove Fabro's inbound server IP allowlisting feature completely. This removes the\nserver-wide `[server.ip_allowlist]` setting, the GitHub webhook-specific\n`[server.integrations.github.webhooks.ip_allowlist]` overlay, request middleware,\nclient-IP extraction, GitHub `/meta` hook-range expansion, settings API exposure,\ngenerated API client types, and the Settings > Security UI display.\n\nExisting configs that still contain removed IP allowlist keys should fail as\nunknown fields. This is an intentional hard removal, not a compatibility\ndeprecation.\n\n## Problem Frame\n\nThe feature is being removed from Fabro rather than maintained as an in-process\nnetwork access-control layer. Network source restrictions should be handled\noutside Fabro by reverse proxies, firewalls, VPNs, Tailscale, platform ingress,\nor other deployment-layer controls.\n\n## Requirements Trace\n\n- R1. Remove all runtime enforcement of inbound source-IP allowlisting from web,\n API, static asset, and GitHub webhook routes.\n- R2. Remove the server settings schema for global and GitHub webhook IP\n allowlists.\n- R3. Remove `IpAllowEntry` and related API schema/client types from public\n settings payloads.\n- R4. Keep unrelated allowlists intact: GitHub username allowlists, sandbox\n egress CIDR allow lists, and internal policy/test allowlists.\n- R5. Preserve existing authentication, GitHub webhook HMAC verification,\n routing, health checks, request logging, and settings hot-reload behavior.\n- R6. Treat old IP allowlist config as invalid after removal.\n- R7. Update operator-facing docs/changelog so users know to move source-IP\n restrictions upstream.\n\n## Scope Boundaries\n\n- Do not remove `[server.auth.github].allowed_usernames`.\n- Do not remove Daytona/sandbox `cidr_allow_list` network policy support.\n- Do not remove generic uses of \"allowlist\" in policy tests, markdown rendering,\n model controls, or other unrelated domains.\n- Do not add a migration, warning-only parser, or fallback compatibility path for\n old IP allowlist settings.\n- Do not change GitHub webhook signature verification or webhook route strategy.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- Settings use sparse config layers in `lib/crates/fabro-config/src/layers/`\n and dense resolved types in `lib/crates/fabro-types/src/settings/`.\n- The OpenAPI spec at `docs/public/api-reference/fabro-api.yaml` is the source\n of truth for API wire shape; `cargo build -p fabro-api` regenerates Rust API\n code, and `cd lib/packages/fabro-api-client && bun run generate` regenerates\n the TypeScript client.\n- `lib/crates/fabro-server/src/server.rs` builds the router and currently wires\n IP allowlist middleware before other route dispatch behavior.\n- `lib/crates/fabro-server/src/serve.rs` currently resolves server and webhook\n allowlist configs at startup and creates a GitHub `/meta` resolver.\n- `apps/fabro-web/app/routes/settings-security.tsx` displays the settings API's\n `server.ip_allowlist` state.\n\n### Main Removal Surfaces\n\n- Config/types: `fabro-types`, `fabro-config`, config tests.\n- Server runtime: `fabro-server/src/ip_allowlist.rs`, router wiring, startup\n wiring, test helpers, routing/TCP tests.\n- API/contracts: OpenAPI schema, `fabro-api` type replacements/exports, generated\n TypeScript API client models.\n- Frontend/docs: Settings > Security copy, settings navigation copy, changelog.\n\n## Key Technical Decisions\n\n- **Hard removal:** Removed config keys should fail through existing\n `deny_unknown_fields` behavior. This keeps the change simple and makes stale\n deployment config visible immediately.\n- **Delete, do not stub:** Remove `IpAllowlistConfig` and middleware parameters\n instead of passing default empty configs through the router. Empty stubs would\n keep the feature shape alive and make future cleanup harder.\n- **Keep webhook auth unchanged:** GitHub webhook source-IP filtering goes away,\n but HMAC signature verification remains the security boundary for webhook\n payload authenticity.\n- **Generated clients follow OpenAPI:** Update the OpenAPI schemas first, then\n regenerate Rust and TypeScript API outputs rather than hand-editing generated\n client files except as a short-term cleanup if generation leaves stale exports.\n- **Docs mention upstream controls:** The changelog and security guidance should\n direct operators to network-layer controls, not a replacement Fabro setting.\n\n## Implementation Units\n\n- [ ] **Unit 1: Remove config schema and resolved types**\n\n**Goal:** Delete the IP allowlist settings contract from config parsing and dense\nserver settings.\n\n**Requirements:** R2, R3, R4, R6\n\n**Dependencies:** None\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/settings/server.rs`\n- Modify: `lib/crates/fabro-types/src/settings/mod.rs`\n- Modify: `lib/crates/fabro-types/Cargo.toml`\n- Modify: `lib/crates/fabro-config/src/layers/server.rs`\n- Modify: `lib/crates/fabro-config/src/layers/mod.rs`\n- Modify: `lib/crates/fabro-config/src/lib.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/server.rs`\n- Modify: `lib/crates/fabro-config/src/tests/resolve_server.rs`\n\n**Approach:**\n- Remove `ServerNamespace.ip_allowlist` and its `test_default()` population.\n- Delete `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, and\n `IpAllowEntry`.\n- Remove `ServerLayer.ip_allowlist`, `ServerIpAllowlistLayer`,\n `ServerIpAllowlistOverrideLayer`, and `IntegrationWebhooksLayer.ip_allowlist`.\n- Remove resolver functions and validation for global and webhook IP allowlists,\n including `github_meta_hooks` parsing and Unix socket trusted-proxy checks.\n- Remove `ipnet` from `fabro-types`. Keep `ipnet` in `fabro-config` because\n `resolve/environment.rs` still validates sandbox CIDR policy.\n- Replace old positive/negative IP allowlist config tests with unknown-field\n tests for `[server.ip_allowlist]` and\n `[server.integrations.github.webhooks.ip_allowlist]`.\n\n**Patterns to follow:**\n- Existing unknown-field tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`\n for retired server settings.\n- Existing `ServerLayer`/`ServerNamespace` layer-to-resolved pattern for removing\n a server subdomain cleanly.\n\n**Test scenarios:**\n- Error path: TOML with `[server.ip_allowlist]` fails parsing/resolution with an\n unknown-field diagnostic mentioning `ip_allowlist`.\n- Error path: TOML with `[server.integrations.github.webhooks.ip_allowlist]`\n fails with an unknown-field diagnostic mentioning `ip_allowlist`.\n- Happy path: minimal valid server settings still resolve without any\n `ip_allowlist` field.\n- Integration: serialized `ServerSettings` JSON no longer includes\n `server.ip_allowlist`.\n\n**Verification:**\n- `fabro-config` and `fabro-types` compile without removed IP allowlist symbols.\n- Config tests prove stale allowlist settings are rejected.\n\n- [ ] **Unit 2: Remove server middleware and startup resolution**\n\n**Goal:** Remove all runtime source-IP filtering and GitHub `/meta` range\nresolution from `fabro-server`.\n\n**Requirements:** R1, R4, R5\n\n**Dependencies:** Unit 1\n\n**Files:**\n- Delete: `lib/crates/fabro-server/src/ip_allowlist.rs`\n- Modify: `lib/crates/fabro-server/src/lib.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Modify: `lib/crates/fabro-server/src/serve.rs`\n- Modify: `lib/crates/fabro-server/src/test_support.rs`\n- Modify: `lib/crates/fabro-server/Cargo.toml`\n- Modify: `lib/crates/fabro-server/src/auth/translate.rs`\n- Modify: `lib/crates/fabro-server/src/web_auth.rs`\n- Modify: `lib/crates/fabro-cli/tests/it/support/auth_harness.rs`\n\n**Approach:**\n- Remove the public `ip_allowlist` module export.\n- Delete `IpAllowlistConfig`, `IpAllowlist`, `GitHubMetaResolver`, client-IP\n extraction, middleware, and GitHub meta cache helpers.\n- Simplify `build_router_with_options` by removing its `Arc<IpAllowlistConfig>`\n parameter and removing `RouterOptions.github_webhook_ip_allowlist`.\n- Remove the global allowlist middleware layer from the main app router.\n- Simplify `github_webhook_routes` so it only receives the webhook secret and no\n route-specific allowlist config.\n- Remove startup creation of `GitHubMetaResolver`, `default_ip_allowlist`,\n `resolve_github_webhook_ip_allowlist`, and\n `resolve_startup_github_webhook_ip_allowlist`.\n- Replace all test/helper call sites that pass `IpAllowlistConfig::default()`\n with the simplified router signature.\n- Remove `ipnet` and any now-unused HTTP mocking/test-only dependencies from\n `fabro-server` if they are only used by the deleted module.\n- Consider whether `serve.rs` still needs `SocketAddr` for TCP\n `ConnectInfo`; if it was only present for allowlisting tests, remove the\n connect-info service wrapper and imports.\n\n**Patterns to follow:**\n- Keep router layers ordered as they are after the allowlist layer is removed:\n auth translation, demo routing, auth extension, canonical host, security\n headers, HTTP logging, request ID.\n- Existing webhook route HMAC tests and auth tests should remain the source of\n truth for webhook behavior.\n\n**Test scenarios:**\n- Happy path: API requests from any TCP remote address route normally when auth\n requirements are satisfied.\n- Happy path: `/health` remains accessible.\n- Integration: GitHub webhook route still rejects missing/invalid signatures and\n accepts valid signatures exactly as before.\n- Cleanup: there are no references to `IpAllowlistConfig`, `ip_allowlist_middleware`,\n `GitHubMetaResolver`, `github_meta_hooks`, or `github-meta-hooks.json`.\n\n**Verification:**\n- `fabro-server` and CLI auth harness tests compile against the simplified router\n API.\n- Runtime startup no longer performs any GitHub `/meta` fetch for webhook IP\n ranges.\n\n- [ ] **Unit 3: Update OpenAPI and generated API clients**\n\n**Goal:** Remove IP allowlist fields and schemas from public settings API\ncontracts and regenerated clients.\n\n**Requirements:** R3, R4\n\n**Dependencies:** Units 1 and 2\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs`\n- Modify/generated: `lib/packages/fabro-api-client/src/models/*`\n\n**Approach:**\n- Remove `ip_allowlist` from `ServerNamespace.required` and\n `ServerNamespace.properties`.\n- Remove `ServerIpAllowlistSettings`,\n `ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,\n `LiteralIpAllowEntry`, and `GitHubMetaHooksEntry` schemas.\n- Remove `IntegrationWebhooksSettings.ip_allowlist` from required/properties.\n- Remove `with_replacement` entries and public re-exports for removed types in\n `fabro-api`.\n- Regenerate Rust API code by building `fabro-api`.\n- Regenerate TypeScript Axios client and ensure stale allowlist model files and\n index exports are gone.\n- Strengthen round-trip tests to assert settings JSON omits\n `server.ip_allowlist` and webhook `ip_allowlist`.\n\n**Patterns to follow:**\n- Existing OpenAPI-first workflow in `AGENTS.md`.\n- Existing `server_settings_family_reuses_domain_types` assertions for shared\n domain type identity.\n\n**Test scenarios:**\n- Contract: `ServerSettings` round-trips through API types without any IP\n allowlist field.\n- Contract: OpenAPI-generated TypeScript `ServerNamespace` has no\n `ip_allowlist` property.\n- Contract: `IntegrationWebhooksSettings` has only webhook strategy fields after\n removal.\n\n**Verification:**\n- Generated Rust and TypeScript clients match the updated OpenAPI spec.\n- `rg \"ServerIpAllowlist|IpAllowEntry|server-ip-allowlist|literal-ip-allow-entry\"`\n finds no remaining generated/public API references.\n\n- [ ] **Unit 4: Update web settings UI**\n\n**Goal:** Remove IP allowlist display from Settings > Security and align copy\nwith the new settings shape.\n\n**Requirements:** R3, R7\n\n**Dependencies:** Unit 3\n\n**Files:**\n- Modify: `apps/fabro-web/app/routes/settings-security.tsx`\n- Modify: `apps/fabro-web/app/routes/settings.tsx`\n\n**Approach:**\n- Change the Security page description from \"Authentication methods and network\n allowlist\" to authentication-only wording.\n- Remove destructuring and rendering of `settings.server.ip_allowlist`.\n- Remove the unused `Count` and `plural` imports if no longer used.\n- Update the Settings nav description for Security from \"Authentication and\n network allowlist\" to authentication-focused copy.\n\n**Patterns to follow:**\n- Existing `settings-panel` row layout for Auth methods and Allowed usernames.\n\n**Test scenarios:**\n- Typecheck: the route compiles against the regenerated API client with no\n `server.ip_allowlist` property.\n- UI behavior: Security page still renders auth methods and allowed usernames.\n- Cleanup: no frontend references to `ip_allowlist`, `IP allowlist`, or\n `trusted_proxy_count` remain.\n\n**Verification:**\n- `apps/fabro-web` typecheck passes against the new generated client.\n\n- [ ] **Unit 5: Update tests, docs, changelog, and dependency lockfile**\n\n**Goal:** Remove stale references and document the operator-facing behavior\nchange.\n\n**Requirements:** R4, R6, R7\n\n**Dependencies:** Units 1 through 4\n\n**Files:**\n- Modify: `lib/crates/fabro-server/tests/it/api/routing.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/tcp.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/settings.rs`\n- Modify: `docs/public/administration/security.mdx`\n- Create or modify: `docs/public/changelog/2026-05-27.mdx`\n- Modify: `Cargo.lock` if dependency graph changes\n\n**Approach:**\n- Delete route/TCP tests that only prove IP allowlist enforcement or\n `ConnectInfo` behavior.\n- Adjust any router setup helpers after the signature simplification.\n- Add a settings API assertion that `server.ip_allowlist` is absent from\n `/api/v1/settings`.\n- Update security docs to explicitly state that Fabro does not provide inbound\n source-IP allowlisting and operators should use upstream network controls.\n- Add a changelog entry dated 2026-05-27 describing the hard removal and the\n expected replacement at the deployment layer.\n- Run dependency resolution after removing direct `ipnet`/test dependencies;\n keep transitive or unrelated `ipnet` entries needed by sandbox CIDR validation.\n\n**Patterns to follow:**\n- Existing changelog style in `docs/public/changelog/2026-05-26.mdx`.\n- Existing settings API integration test style in\n `lib/crates/fabro-server/tests/it/api/settings.rs`.\n\n**Test scenarios:**\n- Settings API: response contains server auth, listen, storage, scheduler, and\n integrations fields, but not `server.ip_allowlist`.\n- Config compatibility: old IP allowlist TOML fails before startup rather than\n being silently ignored.\n- Docs validation: security docs no longer imply Fabro can restrict inbound\n source IPs internally.\n\n**Verification:**\n- `rg -n \"ip_allowlist|trusted_proxy_count|github_meta_hooks|IP allowlist|ip allowlist\"`\n returns only historical archived plan/brainstorm/spec references or unrelated\n non-server allowlist text.\n\n## System-Wide Impact\n\n- **Public API:** `GET /api/v1/settings` response shape changes by removing\n `server.ip_allowlist` and webhook `ip_allowlist`.\n- **Config compatibility:** Existing `settings.toml` files containing the removed\n keys become invalid. This is intentional.\n- **Runtime security posture:** Fabro no longer blocks requests based on source\n IP. Operators must enforce network source restrictions upstream.\n- **Webhook handling:** GitHub webhook HMAC verification remains unchanged; only\n optional source-IP filtering is removed.\n- **Generated clients:** Downstream TypeScript/Rust consumers that read\n `server.ip_allowlist` must update.\n\n## Risks & Mitigations\n\n| Risk | Mitigation |\n|------|------------|\n| Operators accidentally expose a server that previously relied on Fabro IP allowlisting | Changelog and security docs explicitly call out the removal and direct operators to upstream controls. |\n| Generated API clients retain stale types | Update OpenAPI first, regenerate both Rust and TypeScript clients, and run search checks for stale symbols. |\n| Unrelated allowlist functionality is removed by broad search/replace | Scope searches to `IpAllow`, `ip_allowlist`, `trusted_proxy_count`, and `github_meta_hooks`; preserve sandbox CIDR and GitHub username allowlists. |\n| Router signature cleanup breaks many tests | Update shared test helpers first, then compile-driven cleanup of remaining call sites. |\n| `ipnet` is removed where still needed | Keep `fabro-config` dependency if environment CIDR validation still imports `ipnet::IpNet`. |\n\n## Test Plan\n\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun test`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n\n## Assumptions\n\n- The accepted compatibility policy is hard removal: no warning-only parser, no\n migration, and no custom compatibility error path.\n- Public API consumers can tolerate a breaking settings shape change for this\n removed feature.\n- Historical documents in `docs/plans/`, `docs/brainstorms/`, and\n `docs/superpowers/` are archival and do not need rewriting.\n\n## Sources & References\n\n- Previous feature requirements:\n `docs/brainstorms/2026-04-15-ip-whitelist-requirements.md`\n- Previous implementation plan:\n `docs/plans/2026-04-15-002-feat-ip-allowlist-plan.md`\n- API workflow guidance: `AGENTS.md`\n- OpenAPI source of truth: `docs/public/api-reference/fabro-api.yaml`\n"
|
|
},
|
|
"model_stylesheet": {
|
|
"String": "\n * { model: claude-opus-4-7; }\n "
|
|
}
|
|
}
|
|
},
|
|
"graph_source": "digraph ImplementPlan {\n graph [\n goal=\"Implement and simplify\",\n model_stylesheet=\"\n * { model: claude-opus-4-7; }\n \"\n ]\n rankdir=LR\n\n start [shape=Mdiamond, label=\"Start\"]\n exit [shape=Msquare, label=\"Exit\"]\n\n toolchain [label=\"Toolchain\", shape=parallelogram, script=\"command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1\", max_retries=0]\n preflight_compile [label=\"Preflight Compile\", shape=parallelogram, script=\"cargo check -q --workspace 2>&1\", max_retries=0]\n preflight_lint [label=\"Preflight Lint\", shape=parallelogram, script=\"cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1\", max_retries=0]\n fix_lints [label=\"Fix Lints\", prompt=\"The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.\", max_visits=3]\n implement [label=\"Implement\", prompt=\"Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.\", model=\"gpt-55\", reasoning_effort=\"xhigh\"]\n simplify_opus [label=\"Simplify (Opus)\", prompt=\"@prompts/simplify.md\"]\n simplify_gpt [label=\"Simplify (GPT-55)\", prompt=\"@prompts/simplify.md\", model=\"gpt-55\"]\n verify [label=\"Verify\", shape=parallelogram, script=\"git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\\\"disabled\\\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1\", goal_gate=true, retry_target=\"fixup\"]\n fixup [label=\"Fixup\", prompt=\"The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.\", max_visits=3]\n\n start -> toolchain\n toolchain -> preflight_compile [condition=\"outcome=succeeded\"]\n toolchain -> exit\n preflight_compile -> preflight_lint [condition=\"outcome=succeeded\"]\n preflight_compile -> exit\n preflight_lint -> implement [condition=\"outcome=succeeded\"]\n preflight_lint -> fix_lints\n fix_lints -> preflight_lint\n implement -> simplify_opus -> simplify_gpt -> verify\n verify -> exit [condition=\"outcome=succeeded\"]\n verify -> fixup\n fixup -> verify\n}\n",
|
|
"workflow_slug": "implement-plan",
|
|
"source_directory": "/Users/bhelmkamp/p/fabro-sh/fabro",
|
|
"provenance": {
|
|
"server": {
|
|
"version": "0.246.0-nightly.0"
|
|
},
|
|
"client": {
|
|
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36"
|
|
},
|
|
"subject": {
|
|
"kind": "user",
|
|
"identity": {
|
|
"issuer": "https://github.com",
|
|
"subject": "19"
|
|
},
|
|
"login": "brynary",
|
|
"auth_method": "github",
|
|
"avatar_url": "https://avatars.githubusercontent.com/u/19?v=4"
|
|
}
|
|
},
|
|
"manifest_blob": "b9eabcdd38d084c352917a1599173540f3bb31075038f2064163fa8251aafa07",
|
|
"definition_blob": "303626c83b308220ef99468ae5d93ca4e254084c746b7c13fb24072fe5b0bfb2",
|
|
"git": {
|
|
"origin_url": "https://github.com/fabro-sh/fabro",
|
|
"branch": "main",
|
|
"sha": "c767db897f14b4e1bf4c7572f83b164c5650ce58",
|
|
"dirty": "dirty",
|
|
"push_outcome": {
|
|
"type": "not_attempted"
|
|
}
|
|
}
|
|
},
|
|
"web_url": "http://127.0.0.1:32276/runs/01KSNYVQCKXPCSCEGWMZ1Q574Z",
|
|
"start": {
|
|
"start_time": "2026-05-28T00:13:58.936788Z",
|
|
"run_branch": "fabro/run/01KSNYVQCKXPCSCEGWMZ1Q574Z",
|
|
"base_sha": "a992a7d76b22f6206d5ea107feadeba852677da1"
|
|
},
|
|
"status": {
|
|
"kind": "running"
|
|
},
|
|
"status_updated_at": "2026-05-28T00:13:58.936868Z",
|
|
"last_event_at": "2026-05-28T01:02:40.663242Z",
|
|
"pending_control": null,
|
|
"checkpoints": [
|
|
{
|
|
"seq": 20,
|
|
"checkpoint": {
|
|
"timestamp": "2026-05-28T00:14:00.982264Z",
|
|
"current_node": "start",
|
|
"completed_nodes": [
|
|
"start"
|
|
],
|
|
"node_retries": {},
|
|
"context_values": {
|
|
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
|
"internal.fidelity": "compact",
|
|
"internal.thread_id": null,
|
|
"failure_class": "",
|
|
"failure_signature": "",
|
|
"internal.work_dir": "/home/daytona/workspace/fabro",
|
|
"graph.goal": "---\ntitle: \"refactor: Remove IP allowlisting\"\ntype: refactor\nstatus: active\ndate: 2026-05-27\n---\n\n# refactor: Remove IP allowlisting\n\n## Overview\n\nRemove Fabro's inbound server IP allowlisting feature completely. This removes the\nserver-wide `[server.ip_allowlist]` setting, the GitHub webhook-specific\n`[server.integrations.github.webhooks.ip_allowlist]` overlay, request middleware,\nclient-IP extraction, GitHub `/meta` hook-range expansion, settings API exposure,\ngenerated API client types, and the Settings > Security UI display.\n\nExisting configs that still contain removed IP allowlist keys should fail as\nunknown fields. This is an intentional hard removal, not a compatibility\ndeprecation.\n\n## Problem Frame\n\nThe feature is being removed from Fabro rather than maintained as an in-process\nnetwork access-control layer. Network source restrictions should be handled\noutside Fabro by reverse proxies, firewalls, VPNs, Tailscale, platform ingress,\nor other deployment-layer controls.\n\n## Requirements Trace\n\n- R1. Remove all runtime enforcement of inbound source-IP allowlisting from web,\n API, static asset, and GitHub webhook routes.\n- R2. Remove the server settings schema for global and GitHub webhook IP\n allowlists.\n- R3. Remove `IpAllowEntry` and related API schema/client types from public\n settings payloads.\n- R4. Keep unrelated allowlists intact: GitHub username allowlists, sandbox\n egress CIDR allow lists, and internal policy/test allowlists.\n- R5. Preserve existing authentication, GitHub webhook HMAC verification,\n routing, health checks, request logging, and settings hot-reload behavior.\n- R6. Treat old IP allowlist config as invalid after removal.\n- R7. Update operator-facing docs/changelog so users know to move source-IP\n restrictions upstream.\n\n## Scope Boundaries\n\n- Do not remove `[server.auth.github].allowed_usernames`.\n- Do not remove Daytona/sandbox `cidr_allow_list` network policy support.\n- Do not remove generic uses of \"allowlist\" in policy tests, markdown rendering,\n model controls, or other unrelated domains.\n- Do not add a migration, warning-only parser, or fallback compatibility path for\n old IP allowlist settings.\n- Do not change GitHub webhook signature verification or webhook route strategy.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- Settings use sparse config layers in `lib/crates/fabro-config/src/layers/`\n and dense resolved types in `lib/crates/fabro-types/src/settings/`.\n- The OpenAPI spec at `docs/public/api-reference/fabro-api.yaml` is the source\n of truth for API wire shape; `cargo build -p fabro-api` regenerates Rust API\n code, and `cd lib/packages/fabro-api-client && bun run generate` regenerates\n the TypeScript client.\n- `lib/crates/fabro-server/src/server.rs` builds the router and currently wires\n IP allowlist middleware before other route dispatch behavior.\n- `lib/crates/fabro-server/src/serve.rs` currently resolves server and webhook\n allowlist configs at startup and creates a GitHub `/meta` resolver.\n- `apps/fabro-web/app/routes/settings-security.tsx` displays the settings API's\n `server.ip_allowlist` state.\n\n### Main Removal Surfaces\n\n- Config/types: `fabro-types`, `fabro-config`, config tests.\n- Server runtime: `fabro-server/src/ip_allowlist.rs`, router wiring, startup\n wiring, test helpers, routing/TCP tests.\n- API/contracts: OpenAPI schema, `fabro-api` type replacements/exports, generated\n TypeScript API client models.\n- Frontend/docs: Settings > Security copy, settings navigation copy, changelog.\n\n## Key Technical Decisions\n\n- **Hard removal:** Removed config keys should fail through existing\n `deny_unknown_fields` behavior. This keeps the change simple and makes stale\n deployment config visible immediately.\n- **Delete, do not stub:** Remove `IpAllowlistConfig` and middleware parameters\n instead of passing default empty configs through the router. Empty stubs would\n keep the feature shape alive and make future cleanup harder.\n- **Keep webhook auth unchanged:** GitHub webhook source-IP filtering goes away,\n but HMAC signature verification remains the security boundary for webhook\n payload authenticity.\n- **Generated clients follow OpenAPI:** Update the OpenAPI schemas first, then\n regenerate Rust and TypeScript API outputs rather than hand-editing generated\n client files except as a short-term cleanup if generation leaves stale exports.\n- **Docs mention upstream controls:** The changelog and security guidance should\n direct operators to network-layer controls, not a replacement Fabro setting.\n\n## Implementation Units\n\n- [ ] **Unit 1: Remove config schema and resolved types**\n\n**Goal:** Delete the IP allowlist settings contract from config parsing and dense\nserver settings.\n\n**Requirements:** R2, R3, R4, R6\n\n**Dependencies:** None\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/settings/server.rs`\n- Modify: `lib/crates/fabro-types/src/settings/mod.rs`\n- Modify: `lib/crates/fabro-types/Cargo.toml`\n- Modify: `lib/crates/fabro-config/src/layers/server.rs`\n- Modify: `lib/crates/fabro-config/src/layers/mod.rs`\n- Modify: `lib/crates/fabro-config/src/lib.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/server.rs`\n- Modify: `lib/crates/fabro-config/src/tests/resolve_server.rs`\n\n**Approach:**\n- Remove `ServerNamespace.ip_allowlist` and its `test_default()` population.\n- Delete `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, and\n `IpAllowEntry`.\n- Remove `ServerLayer.ip_allowlist`, `ServerIpAllowlistLayer`,\n `ServerIpAllowlistOverrideLayer`, and `IntegrationWebhooksLayer.ip_allowlist`.\n- Remove resolver functions and validation for global and webhook IP allowlists,\n including `github_meta_hooks` parsing and Unix socket trusted-proxy checks.\n- Remove `ipnet` from `fabro-types`. Keep `ipnet` in `fabro-config` because\n `resolve/environment.rs` still validates sandbox CIDR policy.\n- Replace old positive/negative IP allowlist config tests with unknown-field\n tests for `[server.ip_allowlist]` and\n `[server.integrations.github.webhooks.ip_allowlist]`.\n\n**Patterns to follow:**\n- Existing unknown-field tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`\n for retired server settings.\n- Existing `ServerLayer`/`ServerNamespace` layer-to-resolved pattern for removing\n a server subdomain cleanly.\n\n**Test scenarios:**\n- Error path: TOML with `[server.ip_allowlist]` fails parsing/resolution with an\n unknown-field diagnostic mentioning `ip_allowlist`.\n- Error path: TOML with `[server.integrations.github.webhooks.ip_allowlist]`\n fails with an unknown-field diagnostic mentioning `ip_allowlist`.\n- Happy path: minimal valid server settings still resolve without any\n `ip_allowlist` field.\n- Integration: serialized `ServerSettings` JSON no longer includes\n `server.ip_allowlist`.\n\n**Verification:**\n- `fabro-config` and `fabro-types` compile without removed IP allowlist symbols.\n- Config tests prove stale allowlist settings are rejected.\n\n- [ ] **Unit 2: Remove server middleware and startup resolution**\n\n**Goal:** Remove all runtime source-IP filtering and GitHub `/meta` range\nresolution from `fabro-server`.\n\n**Requirements:** R1, R4, R5\n\n**Dependencies:** Unit 1\n\n**Files:**\n- Delete: `lib/crates/fabro-server/src/ip_allowlist.rs`\n- Modify: `lib/crates/fabro-server/src/lib.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Modify: `lib/crates/fabro-server/src/serve.rs`\n- Modify: `lib/crates/fabro-server/src/test_support.rs`\n- Modify: `lib/crates/fabro-server/Cargo.toml`\n- Modify: `lib/crates/fabro-server/src/auth/translate.rs`\n- Modify: `lib/crates/fabro-server/src/web_auth.rs`\n- Modify: `lib/crates/fabro-cli/tests/it/support/auth_harness.rs`\n\n**Approach:**\n- Remove the public `ip_allowlist` module export.\n- Delete `IpAllowlistConfig`, `IpAllowlist`, `GitHubMetaResolver`, client-IP\n extraction, middleware, and GitHub meta cache helpers.\n- Simplify `build_router_with_options` by removing its `Arc<IpAllowlistConfig>`\n parameter and removing `RouterOptions.github_webhook_ip_allowlist`.\n- Remove the global allowlist middleware layer from the main app router.\n- Simplify `github_webhook_routes` so it only receives the webhook secret and no\n route-specific allowlist config.\n- Remove startup creation of `GitHubMetaResolver`, `default_ip_allowlist`,\n `resolve_github_webhook_ip_allowlist`, and\n `resolve_startup_github_webhook_ip_allowlist`.\n- Replace all test/helper call sites that pass `IpAllowlistConfig::default()`\n with the simplified router signature.\n- Remove `ipnet` and any now-unused HTTP mocking/test-only dependencies from\n `fabro-server` if they are only used by the deleted module.\n- Consider whether `serve.rs` still needs `SocketAddr` for TCP\n `ConnectInfo`; if it was only present for allowlisting tests, remove the\n connect-info service wrapper and imports.\n\n**Patterns to follow:**\n- Keep router layers ordered as they are after the allowlist layer is removed:\n auth translation, demo routing, auth extension, canonical host, security\n headers, HTTP logging, request ID.\n- Existing webhook route HMAC tests and auth tests should remain the source of\n truth for webhook behavior.\n\n**Test scenarios:**\n- Happy path: API requests from any TCP remote address route normally when auth\n requirements are satisfied.\n- Happy path: `/health` remains accessible.\n- Integration: GitHub webhook route still rejects missing/invalid signatures and\n accepts valid signatures exactly as before.\n- Cleanup: there are no references to `IpAllowlistConfig`, `ip_allowlist_middleware`,\n `GitHubMetaResolver`, `github_meta_hooks`, or `github-meta-hooks.json`.\n\n**Verification:**\n- `fabro-server` and CLI auth harness tests compile against the simplified router\n API.\n- Runtime startup no longer performs any GitHub `/meta` fetch for webhook IP\n ranges.\n\n- [ ] **Unit 3: Update OpenAPI and generated API clients**\n\n**Goal:** Remove IP allowlist fields and schemas from public settings API\ncontracts and regenerated clients.\n\n**Requirements:** R3, R4\n\n**Dependencies:** Units 1 and 2\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs`\n- Modify/generated: `lib/packages/fabro-api-client/src/models/*`\n\n**Approach:**\n- Remove `ip_allowlist` from `ServerNamespace.required` and\n `ServerNamespace.properties`.\n- Remove `ServerIpAllowlistSettings`,\n `ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,\n `LiteralIpAllowEntry`, and `GitHubMetaHooksEntry` schemas.\n- Remove `IntegrationWebhooksSettings.ip_allowlist` from required/properties.\n- Remove `with_replacement` entries and public re-exports for removed types in\n `fabro-api`.\n- Regenerate Rust API code by building `fabro-api`.\n- Regenerate TypeScript Axios client and ensure stale allowlist model files and\n index exports are gone.\n- Strengthen round-trip tests to assert settings JSON omits\n `server.ip_allowlist` and webhook `ip_allowlist`.\n\n**Patterns to follow:**\n- Existing OpenAPI-first workflow in `AGENTS.md`.\n- Existing `server_settings_family_reuses_domain_types` assertions for shared\n domain type identity.\n\n**Test scenarios:**\n- Contract: `ServerSettings` round-trips through API types without any IP\n allowlist field.\n- Contract: OpenAPI-generated TypeScript `ServerNamespace` has no\n `ip_allowlist` property.\n- Contract: `IntegrationWebhooksSettings` has only webhook strategy fields after\n removal.\n\n**Verification:**\n- Generated Rust and TypeScript clients match the updated OpenAPI spec.\n- `rg \"ServerIpAllowlist|IpAllowEntry|server-ip-allowlist|literal-ip-allow-entry\"`\n finds no remaining generated/public API references.\n\n- [ ] **Unit 4: Update web settings UI**\n\n**Goal:** Remove IP allowlist display from Settings > Security and align copy\nwith the new settings shape.\n\n**Requirements:** R3, R7\n\n**Dependencies:** Unit 3\n\n**Files:**\n- Modify: `apps/fabro-web/app/routes/settings-security.tsx`\n- Modify: `apps/fabro-web/app/routes/settings.tsx`\n\n**Approach:**\n- Change the Security page description from \"Authentication methods and network\n allowlist\" to authentication-only wording.\n- Remove destructuring and rendering of `settings.server.ip_allowlist`.\n- Remove the unused `Count` and `plural` imports if no longer used.\n- Update the Settings nav description for Security from \"Authentication and\n network allowlist\" to authentication-focused copy.\n\n**Patterns to follow:**\n- Existing `settings-panel` row layout for Auth methods and Allowed usernames.\n\n**Test scenarios:**\n- Typecheck: the route compiles against the regenerated API client with no\n `server.ip_allowlist` property.\n- UI behavior: Security page still renders auth methods and allowed usernames.\n- Cleanup: no frontend references to `ip_allowlist`, `IP allowlist`, or\n `trusted_proxy_count` remain.\n\n**Verification:**\n- `apps/fabro-web` typecheck passes against the new generated client.\n\n- [ ] **Unit 5: Update tests, docs, changelog, and dependency lockfile**\n\n**Goal:** Remove stale references and document the operator-facing behavior\nchange.\n\n**Requirements:** R4, R6, R7\n\n**Dependencies:** Units 1 through 4\n\n**Files:**\n- Modify: `lib/crates/fabro-server/tests/it/api/routing.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/tcp.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/settings.rs`\n- Modify: `docs/public/administration/security.mdx`\n- Create or modify: `docs/public/changelog/2026-05-27.mdx`\n- Modify: `Cargo.lock` if dependency graph changes\n\n**Approach:**\n- Delete route/TCP tests that only prove IP allowlist enforcement or\n `ConnectInfo` behavior.\n- Adjust any router setup helpers after the signature simplification.\n- Add a settings API assertion that `server.ip_allowlist` is absent from\n `/api/v1/settings`.\n- Update security docs to explicitly state that Fabro does not provide inbound\n source-IP allowlisting and operators should use upstream network controls.\n- Add a changelog entry dated 2026-05-27 describing the hard removal and the\n expected replacement at the deployment layer.\n- Run dependency resolution after removing direct `ipnet`/test dependencies;\n keep transitive or unrelated `ipnet` entries needed by sandbox CIDR validation.\n\n**Patterns to follow:**\n- Existing changelog style in `docs/public/changelog/2026-05-26.mdx`.\n- Existing settings API integration test style in\n `lib/crates/fabro-server/tests/it/api/settings.rs`.\n\n**Test scenarios:**\n- Settings API: response contains server auth, listen, storage, scheduler, and\n integrations fields, but not `server.ip_allowlist`.\n- Config compatibility: old IP allowlist TOML fails before startup rather than\n being silently ignored.\n- Docs validation: security docs no longer imply Fabro can restrict inbound\n source IPs internally.\n\n**Verification:**\n- `rg -n \"ip_allowlist|trusted_proxy_count|github_meta_hooks|IP allowlist|ip allowlist\"`\n returns only historical archived plan/brainstorm/spec references or unrelated\n non-server allowlist text.\n\n## System-Wide Impact\n\n- **Public API:** `GET /api/v1/settings` response shape changes by removing\n `server.ip_allowlist` and webhook `ip_allowlist`.\n- **Config compatibility:** Existing `settings.toml` files containing the removed\n keys become invalid. This is intentional.\n- **Runtime security posture:** Fabro no longer blocks requests based on source\n IP. Operators must enforce network source restrictions upstream.\n- **Webhook handling:** GitHub webhook HMAC verification remains unchanged; only\n optional source-IP filtering is removed.\n- **Generated clients:** Downstream TypeScript/Rust consumers that read\n `server.ip_allowlist` must update.\n\n## Risks & Mitigations\n\n| Risk | Mitigation |\n|------|------------|\n| Operators accidentally expose a server that previously relied on Fabro IP allowlisting | Changelog and security docs explicitly call out the removal and direct operators to upstream controls. |\n| Generated API clients retain stale types | Update OpenAPI first, regenerate both Rust and TypeScript clients, and run search checks for stale symbols. |\n| Unrelated allowlist functionality is removed by broad search/replace | Scope searches to `IpAllow`, `ip_allowlist`, `trusted_proxy_count`, and `github_meta_hooks`; preserve sandbox CIDR and GitHub username allowlists. |\n| Router signature cleanup breaks many tests | Update shared test helpers first, then compile-driven cleanup of remaining call sites. |\n| `ipnet` is removed where still needed | Keep `fabro-config` dependency if environment CIDR validation still imports `ipnet::IpNet`. |\n\n## Test Plan\n\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun test`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n\n## Assumptions\n\n- The accepted compatibility policy is hard removal: no warning-only parser, no\n migration, and no custom compatibility error path.\n- Public API consumers can tolerate a breaking settings shape change for this\n removed feature.\n- Historical documents in `docs/plans/`, `docs/brainstorms/`, and\n `docs/superpowers/` are archival and do not need rewriting.\n\n## Sources & References\n\n- Previous feature requirements:\n `docs/brainstorms/2026-04-15-ip-whitelist-requirements.md`\n- Previous implementation plan:\n `docs/plans/2026-04-15-002-feat-ip-allowlist-plan.md`\n- API workflow guidance: `AGENTS.md`\n- OpenAPI source of truth: `docs/public/api-reference/fabro-api.yaml`\n",
|
|
"outcome": "succeeded",
|
|
"internal.node_visit_count": 1,
|
|
"internal.run_id": "01KSNYVQCKXPCSCEGWMZ1Q574Z",
|
|
"graph.rankdir": "LR",
|
|
"internal.retry_count.start": 0,
|
|
"current_node": "start"
|
|
},
|
|
"node_outcomes": {
|
|
"start": {
|
|
"status": "succeeded",
|
|
"usage": null
|
|
}
|
|
},
|
|
"next_node_id": "toolchain",
|
|
"node_visits": {
|
|
"start": 1
|
|
}
|
|
},
|
|
"diff": {}
|
|
},
|
|
{
|
|
"seq": 28,
|
|
"checkpoint": {
|
|
"timestamp": "2026-05-28T00:14:15.429054Z",
|
|
"current_node": "toolchain",
|
|
"completed_nodes": [
|
|
"start",
|
|
"toolchain"
|
|
],
|
|
"node_retries": {},
|
|
"context_values": {
|
|
"failure_signature": "",
|
|
"internal.fidelity": "compact",
|
|
"graph.rankdir": "LR",
|
|
"internal.thread_id": "start",
|
|
"thread.start.current_node": "toolchain",
|
|
"current_node": "toolchain",
|
|
"internal.retry_count.toolchain": 0,
|
|
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
|
"internal.run_id": "01KSNYVQCKXPCSCEGWMZ1Q574Z",
|
|
"internal.work_dir": "/home/daytona/workspace/fabro",
|
|
"outcome": "succeeded",
|
|
"graph.goal": "---\ntitle: \"refactor: Remove IP allowlisting\"\ntype: refactor\nstatus: active\ndate: 2026-05-27\n---\n\n# refactor: Remove IP allowlisting\n\n## Overview\n\nRemove Fabro's inbound server IP allowlisting feature completely. This removes the\nserver-wide `[server.ip_allowlist]` setting, the GitHub webhook-specific\n`[server.integrations.github.webhooks.ip_allowlist]` overlay, request middleware,\nclient-IP extraction, GitHub `/meta` hook-range expansion, settings API exposure,\ngenerated API client types, and the Settings > Security UI display.\n\nExisting configs that still contain removed IP allowlist keys should fail as\nunknown fields. This is an intentional hard removal, not a compatibility\ndeprecation.\n\n## Problem Frame\n\nThe feature is being removed from Fabro rather than maintained as an in-process\nnetwork access-control layer. Network source restrictions should be handled\noutside Fabro by reverse proxies, firewalls, VPNs, Tailscale, platform ingress,\nor other deployment-layer controls.\n\n## Requirements Trace\n\n- R1. Remove all runtime enforcement of inbound source-IP allowlisting from web,\n API, static asset, and GitHub webhook routes.\n- R2. Remove the server settings schema for global and GitHub webhook IP\n allowlists.\n- R3. Remove `IpAllowEntry` and related API schema/client types from public\n settings payloads.\n- R4. Keep unrelated allowlists intact: GitHub username allowlists, sandbox\n egress CIDR allow lists, and internal policy/test allowlists.\n- R5. Preserve existing authentication, GitHub webhook HMAC verification,\n routing, health checks, request logging, and settings hot-reload behavior.\n- R6. Treat old IP allowlist config as invalid after removal.\n- R7. Update operator-facing docs/changelog so users know to move source-IP\n restrictions upstream.\n\n## Scope Boundaries\n\n- Do not remove `[server.auth.github].allowed_usernames`.\n- Do not remove Daytona/sandbox `cidr_allow_list` network policy support.\n- Do not remove generic uses of \"allowlist\" in policy tests, markdown rendering,\n model controls, or other unrelated domains.\n- Do not add a migration, warning-only parser, or fallback compatibility path for\n old IP allowlist settings.\n- Do not change GitHub webhook signature verification or webhook route strategy.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- Settings use sparse config layers in `lib/crates/fabro-config/src/layers/`\n and dense resolved types in `lib/crates/fabro-types/src/settings/`.\n- The OpenAPI spec at `docs/public/api-reference/fabro-api.yaml` is the source\n of truth for API wire shape; `cargo build -p fabro-api` regenerates Rust API\n code, and `cd lib/packages/fabro-api-client && bun run generate` regenerates\n the TypeScript client.\n- `lib/crates/fabro-server/src/server.rs` builds the router and currently wires\n IP allowlist middleware before other route dispatch behavior.\n- `lib/crates/fabro-server/src/serve.rs` currently resolves server and webhook\n allowlist configs at startup and creates a GitHub `/meta` resolver.\n- `apps/fabro-web/app/routes/settings-security.tsx` displays the settings API's\n `server.ip_allowlist` state.\n\n### Main Removal Surfaces\n\n- Config/types: `fabro-types`, `fabro-config`, config tests.\n- Server runtime: `fabro-server/src/ip_allowlist.rs`, router wiring, startup\n wiring, test helpers, routing/TCP tests.\n- API/contracts: OpenAPI schema, `fabro-api` type replacements/exports, generated\n TypeScript API client models.\n- Frontend/docs: Settings > Security copy, settings navigation copy, changelog.\n\n## Key Technical Decisions\n\n- **Hard removal:** Removed config keys should fail through existing\n `deny_unknown_fields` behavior. This keeps the change simple and makes stale\n deployment config visible immediately.\n- **Delete, do not stub:** Remove `IpAllowlistConfig` and middleware parameters\n instead of passing default empty configs through the router. Empty stubs would\n keep the feature shape alive and make future cleanup harder.\n- **Keep webhook auth unchanged:** GitHub webhook source-IP filtering goes away,\n but HMAC signature verification remains the security boundary for webhook\n payload authenticity.\n- **Generated clients follow OpenAPI:** Update the OpenAPI schemas first, then\n regenerate Rust and TypeScript API outputs rather than hand-editing generated\n client files except as a short-term cleanup if generation leaves stale exports.\n- **Docs mention upstream controls:** The changelog and security guidance should\n direct operators to network-layer controls, not a replacement Fabro setting.\n\n## Implementation Units\n\n- [ ] **Unit 1: Remove config schema and resolved types**\n\n**Goal:** Delete the IP allowlist settings contract from config parsing and dense\nserver settings.\n\n**Requirements:** R2, R3, R4, R6\n\n**Dependencies:** None\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/settings/server.rs`\n- Modify: `lib/crates/fabro-types/src/settings/mod.rs`\n- Modify: `lib/crates/fabro-types/Cargo.toml`\n- Modify: `lib/crates/fabro-config/src/layers/server.rs`\n- Modify: `lib/crates/fabro-config/src/layers/mod.rs`\n- Modify: `lib/crates/fabro-config/src/lib.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/server.rs`\n- Modify: `lib/crates/fabro-config/src/tests/resolve_server.rs`\n\n**Approach:**\n- Remove `ServerNamespace.ip_allowlist` and its `test_default()` population.\n- Delete `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, and\n `IpAllowEntry`.\n- Remove `ServerLayer.ip_allowlist`, `ServerIpAllowlistLayer`,\n `ServerIpAllowlistOverrideLayer`, and `IntegrationWebhooksLayer.ip_allowlist`.\n- Remove resolver functions and validation for global and webhook IP allowlists,\n including `github_meta_hooks` parsing and Unix socket trusted-proxy checks.\n- Remove `ipnet` from `fabro-types`. Keep `ipnet` in `fabro-config` because\n `resolve/environment.rs` still validates sandbox CIDR policy.\n- Replace old positive/negative IP allowlist config tests with unknown-field\n tests for `[server.ip_allowlist]` and\n `[server.integrations.github.webhooks.ip_allowlist]`.\n\n**Patterns to follow:**\n- Existing unknown-field tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`\n for retired server settings.\n- Existing `ServerLayer`/`ServerNamespace` layer-to-resolved pattern for removing\n a server subdomain cleanly.\n\n**Test scenarios:**\n- Error path: TOML with `[server.ip_allowlist]` fails parsing/resolution with an\n unknown-field diagnostic mentioning `ip_allowlist`.\n- Error path: TOML with `[server.integrations.github.webhooks.ip_allowlist]`\n fails with an unknown-field diagnostic mentioning `ip_allowlist`.\n- Happy path: minimal valid server settings still resolve without any\n `ip_allowlist` field.\n- Integration: serialized `ServerSettings` JSON no longer includes\n `server.ip_allowlist`.\n\n**Verification:**\n- `fabro-config` and `fabro-types` compile without removed IP allowlist symbols.\n- Config tests prove stale allowlist settings are rejected.\n\n- [ ] **Unit 2: Remove server middleware and startup resolution**\n\n**Goal:** Remove all runtime source-IP filtering and GitHub `/meta` range\nresolution from `fabro-server`.\n\n**Requirements:** R1, R4, R5\n\n**Dependencies:** Unit 1\n\n**Files:**\n- Delete: `lib/crates/fabro-server/src/ip_allowlist.rs`\n- Modify: `lib/crates/fabro-server/src/lib.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Modify: `lib/crates/fabro-server/src/serve.rs`\n- Modify: `lib/crates/fabro-server/src/test_support.rs`\n- Modify: `lib/crates/fabro-server/Cargo.toml`\n- Modify: `lib/crates/fabro-server/src/auth/translate.rs`\n- Modify: `lib/crates/fabro-server/src/web_auth.rs`\n- Modify: `lib/crates/fabro-cli/tests/it/support/auth_harness.rs`\n\n**Approach:**\n- Remove the public `ip_allowlist` module export.\n- Delete `IpAllowlistConfig`, `IpAllowlist`, `GitHubMetaResolver`, client-IP\n extraction, middleware, and GitHub meta cache helpers.\n- Simplify `build_router_with_options` by removing its `Arc<IpAllowlistConfig>`\n parameter and removing `RouterOptions.github_webhook_ip_allowlist`.\n- Remove the global allowlist middleware layer from the main app router.\n- Simplify `github_webhook_routes` so it only receives the webhook secret and no\n route-specific allowlist config.\n- Remove startup creation of `GitHubMetaResolver`, `default_ip_allowlist`,\n `resolve_github_webhook_ip_allowlist`, and\n `resolve_startup_github_webhook_ip_allowlist`.\n- Replace all test/helper call sites that pass `IpAllowlistConfig::default()`\n with the simplified router signature.\n- Remove `ipnet` and any now-unused HTTP mocking/test-only dependencies from\n `fabro-server` if they are only used by the deleted module.\n- Consider whether `serve.rs` still needs `SocketAddr` for TCP\n `ConnectInfo`; if it was only present for allowlisting tests, remove the\n connect-info service wrapper and imports.\n\n**Patterns to follow:**\n- Keep router layers ordered as they are after the allowlist layer is removed:\n auth translation, demo routing, auth extension, canonical host, security\n headers, HTTP logging, request ID.\n- Existing webhook route HMAC tests and auth tests should remain the source of\n truth for webhook behavior.\n\n**Test scenarios:**\n- Happy path: API requests from any TCP remote address route normally when auth\n requirements are satisfied.\n- Happy path: `/health` remains accessible.\n- Integration: GitHub webhook route still rejects missing/invalid signatures and\n accepts valid signatures exactly as before.\n- Cleanup: there are no references to `IpAllowlistConfig`, `ip_allowlist_middleware`,\n `GitHubMetaResolver`, `github_meta_hooks`, or `github-meta-hooks.json`.\n\n**Verification:**\n- `fabro-server` and CLI auth harness tests compile against the simplified router\n API.\n- Runtime startup no longer performs any GitHub `/meta` fetch for webhook IP\n ranges.\n\n- [ ] **Unit 3: Update OpenAPI and generated API clients**\n\n**Goal:** Remove IP allowlist fields and schemas from public settings API\ncontracts and regenerated clients.\n\n**Requirements:** R3, R4\n\n**Dependencies:** Units 1 and 2\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs`\n- Modify/generated: `lib/packages/fabro-api-client/src/models/*`\n\n**Approach:**\n- Remove `ip_allowlist` from `ServerNamespace.required` and\n `ServerNamespace.properties`.\n- Remove `ServerIpAllowlistSettings`,\n `ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,\n `LiteralIpAllowEntry`, and `GitHubMetaHooksEntry` schemas.\n- Remove `IntegrationWebhooksSettings.ip_allowlist` from required/properties.\n- Remove `with_replacement` entries and public re-exports for removed types in\n `fabro-api`.\n- Regenerate Rust API code by building `fabro-api`.\n- Regenerate TypeScript Axios client and ensure stale allowlist model files and\n index exports are gone.\n- Strengthen round-trip tests to assert settings JSON omits\n `server.ip_allowlist` and webhook `ip_allowlist`.\n\n**Patterns to follow:**\n- Existing OpenAPI-first workflow in `AGENTS.md`.\n- Existing `server_settings_family_reuses_domain_types` assertions for shared\n domain type identity.\n\n**Test scenarios:**\n- Contract: `ServerSettings` round-trips through API types without any IP\n allowlist field.\n- Contract: OpenAPI-generated TypeScript `ServerNamespace` has no\n `ip_allowlist` property.\n- Contract: `IntegrationWebhooksSettings` has only webhook strategy fields after\n removal.\n\n**Verification:**\n- Generated Rust and TypeScript clients match the updated OpenAPI spec.\n- `rg \"ServerIpAllowlist|IpAllowEntry|server-ip-allowlist|literal-ip-allow-entry\"`\n finds no remaining generated/public API references.\n\n- [ ] **Unit 4: Update web settings UI**\n\n**Goal:** Remove IP allowlist display from Settings > Security and align copy\nwith the new settings shape.\n\n**Requirements:** R3, R7\n\n**Dependencies:** Unit 3\n\n**Files:**\n- Modify: `apps/fabro-web/app/routes/settings-security.tsx`\n- Modify: `apps/fabro-web/app/routes/settings.tsx`\n\n**Approach:**\n- Change the Security page description from \"Authentication methods and network\n allowlist\" to authentication-only wording.\n- Remove destructuring and rendering of `settings.server.ip_allowlist`.\n- Remove the unused `Count` and `plural` imports if no longer used.\n- Update the Settings nav description for Security from \"Authentication and\n network allowlist\" to authentication-focused copy.\n\n**Patterns to follow:**\n- Existing `settings-panel` row layout for Auth methods and Allowed usernames.\n\n**Test scenarios:**\n- Typecheck: the route compiles against the regenerated API client with no\n `server.ip_allowlist` property.\n- UI behavior: Security page still renders auth methods and allowed usernames.\n- Cleanup: no frontend references to `ip_allowlist`, `IP allowlist`, or\n `trusted_proxy_count` remain.\n\n**Verification:**\n- `apps/fabro-web` typecheck passes against the new generated client.\n\n- [ ] **Unit 5: Update tests, docs, changelog, and dependency lockfile**\n\n**Goal:** Remove stale references and document the operator-facing behavior\nchange.\n\n**Requirements:** R4, R6, R7\n\n**Dependencies:** Units 1 through 4\n\n**Files:**\n- Modify: `lib/crates/fabro-server/tests/it/api/routing.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/tcp.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/settings.rs`\n- Modify: `docs/public/administration/security.mdx`\n- Create or modify: `docs/public/changelog/2026-05-27.mdx`\n- Modify: `Cargo.lock` if dependency graph changes\n\n**Approach:**\n- Delete route/TCP tests that only prove IP allowlist enforcement or\n `ConnectInfo` behavior.\n- Adjust any router setup helpers after the signature simplification.\n- Add a settings API assertion that `server.ip_allowlist` is absent from\n `/api/v1/settings`.\n- Update security docs to explicitly state that Fabro does not provide inbound\n source-IP allowlisting and operators should use upstream network controls.\n- Add a changelog entry dated 2026-05-27 describing the hard removal and the\n expected replacement at the deployment layer.\n- Run dependency resolution after removing direct `ipnet`/test dependencies;\n keep transitive or unrelated `ipnet` entries needed by sandbox CIDR validation.\n\n**Patterns to follow:**\n- Existing changelog style in `docs/public/changelog/2026-05-26.mdx`.\n- Existing settings API integration test style in\n `lib/crates/fabro-server/tests/it/api/settings.rs`.\n\n**Test scenarios:**\n- Settings API: response contains server auth, listen, storage, scheduler, and\n integrations fields, but not `server.ip_allowlist`.\n- Config compatibility: old IP allowlist TOML fails before startup rather than\n being silently ignored.\n- Docs validation: security docs no longer imply Fabro can restrict inbound\n source IPs internally.\n\n**Verification:**\n- `rg -n \"ip_allowlist|trusted_proxy_count|github_meta_hooks|IP allowlist|ip allowlist\"`\n returns only historical archived plan/brainstorm/spec references or unrelated\n non-server allowlist text.\n\n## System-Wide Impact\n\n- **Public API:** `GET /api/v1/settings` response shape changes by removing\n `server.ip_allowlist` and webhook `ip_allowlist`.\n- **Config compatibility:** Existing `settings.toml` files containing the removed\n keys become invalid. This is intentional.\n- **Runtime security posture:** Fabro no longer blocks requests based on source\n IP. Operators must enforce network source restrictions upstream.\n- **Webhook handling:** GitHub webhook HMAC verification remains unchanged; only\n optional source-IP filtering is removed.\n- **Generated clients:** Downstream TypeScript/Rust consumers that read\n `server.ip_allowlist` must update.\n\n## Risks & Mitigations\n\n| Risk | Mitigation |\n|------|------------|\n| Operators accidentally expose a server that previously relied on Fabro IP allowlisting | Changelog and security docs explicitly call out the removal and direct operators to upstream controls. |\n| Generated API clients retain stale types | Update OpenAPI first, regenerate both Rust and TypeScript clients, and run search checks for stale symbols. |\n| Unrelated allowlist functionality is removed by broad search/replace | Scope searches to `IpAllow`, `ip_allowlist`, `trusted_proxy_count`, and `github_meta_hooks`; preserve sandbox CIDR and GitHub username allowlists. |\n| Router signature cleanup breaks many tests | Update shared test helpers first, then compile-driven cleanup of remaining call sites. |\n| `ipnet` is removed where still needed | Keep `fabro-config` dependency if environment CIDR validation still imports `ipnet::IpNet`. |\n\n## Test Plan\n\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun test`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n\n## Assumptions\n\n- The accepted compatibility policy is hard removal: no warning-only parser, no\n migration, and no custom compatibility error path.\n- Public API consumers can tolerate a breaking settings shape change for this\n removed feature.\n- Historical documents in `docs/plans/`, `docs/brainstorms/`, and\n `docs/superpowers/` are archival and do not need rewriting.\n\n## Sources & References\n\n- Previous feature requirements:\n `docs/brainstorms/2026-04-15-ip-whitelist-requirements.md`\n- Previous implementation plan:\n `docs/plans/2026-04-15-002-feat-ip-allowlist-plan.md`\n- API workflow guidance: `AGENTS.md`\n- OpenAPI source of truth: `docs/public/api-reference/fabro-api.yaml`\n",
|
|
"internal.node_visit_count": 1,
|
|
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c",
|
|
"internal.retry_count.start": 0,
|
|
"failure_class": ""
|
|
},
|
|
"node_outcomes": {
|
|
"start": {
|
|
"status": "succeeded",
|
|
"usage": null
|
|
},
|
|
"toolchain": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
|
},
|
|
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 1482,
|
|
"active_time_ms": 1482
|
|
}
|
|
}
|
|
},
|
|
"next_node_id": "preflight_compile",
|
|
"git_commit_sha": "389657acaf21d621be6fd33602f581df72acab8a",
|
|
"node_visits": {
|
|
"toolchain": 1,
|
|
"start": 1
|
|
}
|
|
},
|
|
"diff": {
|
|
"summary": {
|
|
"files_changed": 0,
|
|
"additions": 0,
|
|
"deletions": 0
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"seq": 38,
|
|
"checkpoint": {
|
|
"timestamp": "2026-05-28T00:16:35.031738Z",
|
|
"current_node": "preflight_compile",
|
|
"completed_nodes": [
|
|
"start",
|
|
"toolchain",
|
|
"preflight_compile"
|
|
],
|
|
"node_retries": {},
|
|
"context_values": {
|
|
"internal.work_dir": "/home/daytona/workspace/fabro",
|
|
"current_node": "preflight_compile",
|
|
"failure_class": "",
|
|
"failure_signature": "",
|
|
"graph.rankdir": "LR",
|
|
"internal.fidelity": "compact",
|
|
"internal.node_visit_count": 1,
|
|
"internal.retry_count.toolchain": 0,
|
|
"internal.run_id": "01KSNYVQCKXPCSCEGWMZ1Q574Z",
|
|
"thread.start.current_node": "toolchain",
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
|
|
"graph.goal": "---\ntitle: \"refactor: Remove IP allowlisting\"\ntype: refactor\nstatus: active\ndate: 2026-05-27\n---\n\n# refactor: Remove IP allowlisting\n\n## Overview\n\nRemove Fabro's inbound server IP allowlisting feature completely. This removes the\nserver-wide `[server.ip_allowlist]` setting, the GitHub webhook-specific\n`[server.integrations.github.webhooks.ip_allowlist]` overlay, request middleware,\nclient-IP extraction, GitHub `/meta` hook-range expansion, settings API exposure,\ngenerated API client types, and the Settings > Security UI display.\n\nExisting configs that still contain removed IP allowlist keys should fail as\nunknown fields. This is an intentional hard removal, not a compatibility\ndeprecation.\n\n## Problem Frame\n\nThe feature is being removed from Fabro rather than maintained as an in-process\nnetwork access-control layer. Network source restrictions should be handled\noutside Fabro by reverse proxies, firewalls, VPNs, Tailscale, platform ingress,\nor other deployment-layer controls.\n\n## Requirements Trace\n\n- R1. Remove all runtime enforcement of inbound source-IP allowlisting from web,\n API, static asset, and GitHub webhook routes.\n- R2. Remove the server settings schema for global and GitHub webhook IP\n allowlists.\n- R3. Remove `IpAllowEntry` and related API schema/client types from public\n settings payloads.\n- R4. Keep unrelated allowlists intact: GitHub username allowlists, sandbox\n egress CIDR allow lists, and internal policy/test allowlists.\n- R5. Preserve existing authentication, GitHub webhook HMAC verification,\n routing, health checks, request logging, and settings hot-reload behavior.\n- R6. Treat old IP allowlist config as invalid after removal.\n- R7. Update operator-facing docs/changelog so users know to move source-IP\n restrictions upstream.\n\n## Scope Boundaries\n\n- Do not remove `[server.auth.github].allowed_usernames`.\n- Do not remove Daytona/sandbox `cidr_allow_list` network policy support.\n- Do not remove generic uses of \"allowlist\" in policy tests, markdown rendering,\n model controls, or other unrelated domains.\n- Do not add a migration, warning-only parser, or fallback compatibility path for\n old IP allowlist settings.\n- Do not change GitHub webhook signature verification or webhook route strategy.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- Settings use sparse config layers in `lib/crates/fabro-config/src/layers/`\n and dense resolved types in `lib/crates/fabro-types/src/settings/`.\n- The OpenAPI spec at `docs/public/api-reference/fabro-api.yaml` is the source\n of truth for API wire shape; `cargo build -p fabro-api` regenerates Rust API\n code, and `cd lib/packages/fabro-api-client && bun run generate` regenerates\n the TypeScript client.\n- `lib/crates/fabro-server/src/server.rs` builds the router and currently wires\n IP allowlist middleware before other route dispatch behavior.\n- `lib/crates/fabro-server/src/serve.rs` currently resolves server and webhook\n allowlist configs at startup and creates a GitHub `/meta` resolver.\n- `apps/fabro-web/app/routes/settings-security.tsx` displays the settings API's\n `server.ip_allowlist` state.\n\n### Main Removal Surfaces\n\n- Config/types: `fabro-types`, `fabro-config`, config tests.\n- Server runtime: `fabro-server/src/ip_allowlist.rs`, router wiring, startup\n wiring, test helpers, routing/TCP tests.\n- API/contracts: OpenAPI schema, `fabro-api` type replacements/exports, generated\n TypeScript API client models.\n- Frontend/docs: Settings > Security copy, settings navigation copy, changelog.\n\n## Key Technical Decisions\n\n- **Hard removal:** Removed config keys should fail through existing\n `deny_unknown_fields` behavior. This keeps the change simple and makes stale\n deployment config visible immediately.\n- **Delete, do not stub:** Remove `IpAllowlistConfig` and middleware parameters\n instead of passing default empty configs through the router. Empty stubs would\n keep the feature shape alive and make future cleanup harder.\n- **Keep webhook auth unchanged:** GitHub webhook source-IP filtering goes away,\n but HMAC signature verification remains the security boundary for webhook\n payload authenticity.\n- **Generated clients follow OpenAPI:** Update the OpenAPI schemas first, then\n regenerate Rust and TypeScript API outputs rather than hand-editing generated\n client files except as a short-term cleanup if generation leaves stale exports.\n- **Docs mention upstream controls:** The changelog and security guidance should\n direct operators to network-layer controls, not a replacement Fabro setting.\n\n## Implementation Units\n\n- [ ] **Unit 1: Remove config schema and resolved types**\n\n**Goal:** Delete the IP allowlist settings contract from config parsing and dense\nserver settings.\n\n**Requirements:** R2, R3, R4, R6\n\n**Dependencies:** None\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/settings/server.rs`\n- Modify: `lib/crates/fabro-types/src/settings/mod.rs`\n- Modify: `lib/crates/fabro-types/Cargo.toml`\n- Modify: `lib/crates/fabro-config/src/layers/server.rs`\n- Modify: `lib/crates/fabro-config/src/layers/mod.rs`\n- Modify: `lib/crates/fabro-config/src/lib.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/server.rs`\n- Modify: `lib/crates/fabro-config/src/tests/resolve_server.rs`\n\n**Approach:**\n- Remove `ServerNamespace.ip_allowlist` and its `test_default()` population.\n- Delete `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, and\n `IpAllowEntry`.\n- Remove `ServerLayer.ip_allowlist`, `ServerIpAllowlistLayer`,\n `ServerIpAllowlistOverrideLayer`, and `IntegrationWebhooksLayer.ip_allowlist`.\n- Remove resolver functions and validation for global and webhook IP allowlists,\n including `github_meta_hooks` parsing and Unix socket trusted-proxy checks.\n- Remove `ipnet` from `fabro-types`. Keep `ipnet` in `fabro-config` because\n `resolve/environment.rs` still validates sandbox CIDR policy.\n- Replace old positive/negative IP allowlist config tests with unknown-field\n tests for `[server.ip_allowlist]` and\n `[server.integrations.github.webhooks.ip_allowlist]`.\n\n**Patterns to follow:**\n- Existing unknown-field tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`\n for retired server settings.\n- Existing `ServerLayer`/`ServerNamespace` layer-to-resolved pattern for removing\n a server subdomain cleanly.\n\n**Test scenarios:**\n- Error path: TOML with `[server.ip_allowlist]` fails parsing/resolution with an\n unknown-field diagnostic mentioning `ip_allowlist`.\n- Error path: TOML with `[server.integrations.github.webhooks.ip_allowlist]`\n fails with an unknown-field diagnostic mentioning `ip_allowlist`.\n- Happy path: minimal valid server settings still resolve without any\n `ip_allowlist` field.\n- Integration: serialized `ServerSettings` JSON no longer includes\n `server.ip_allowlist`.\n\n**Verification:**\n- `fabro-config` and `fabro-types` compile without removed IP allowlist symbols.\n- Config tests prove stale allowlist settings are rejected.\n\n- [ ] **Unit 2: Remove server middleware and startup resolution**\n\n**Goal:** Remove all runtime source-IP filtering and GitHub `/meta` range\nresolution from `fabro-server`.\n\n**Requirements:** R1, R4, R5\n\n**Dependencies:** Unit 1\n\n**Files:**\n- Delete: `lib/crates/fabro-server/src/ip_allowlist.rs`\n- Modify: `lib/crates/fabro-server/src/lib.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Modify: `lib/crates/fabro-server/src/serve.rs`\n- Modify: `lib/crates/fabro-server/src/test_support.rs`\n- Modify: `lib/crates/fabro-server/Cargo.toml`\n- Modify: `lib/crates/fabro-server/src/auth/translate.rs`\n- Modify: `lib/crates/fabro-server/src/web_auth.rs`\n- Modify: `lib/crates/fabro-cli/tests/it/support/auth_harness.rs`\n\n**Approach:**\n- Remove the public `ip_allowlist` module export.\n- Delete `IpAllowlistConfig`, `IpAllowlist`, `GitHubMetaResolver`, client-IP\n extraction, middleware, and GitHub meta cache helpers.\n- Simplify `build_router_with_options` by removing its `Arc<IpAllowlistConfig>`\n parameter and removing `RouterOptions.github_webhook_ip_allowlist`.\n- Remove the global allowlist middleware layer from the main app router.\n- Simplify `github_webhook_routes` so it only receives the webhook secret and no\n route-specific allowlist config.\n- Remove startup creation of `GitHubMetaResolver`, `default_ip_allowlist`,\n `resolve_github_webhook_ip_allowlist`, and\n `resolve_startup_github_webhook_ip_allowlist`.\n- Replace all test/helper call sites that pass `IpAllowlistConfig::default()`\n with the simplified router signature.\n- Remove `ipnet` and any now-unused HTTP mocking/test-only dependencies from\n `fabro-server` if they are only used by the deleted module.\n- Consider whether `serve.rs` still needs `SocketAddr` for TCP\n `ConnectInfo`; if it was only present for allowlisting tests, remove the\n connect-info service wrapper and imports.\n\n**Patterns to follow:**\n- Keep router layers ordered as they are after the allowlist layer is removed:\n auth translation, demo routing, auth extension, canonical host, security\n headers, HTTP logging, request ID.\n- Existing webhook route HMAC tests and auth tests should remain the source of\n truth for webhook behavior.\n\n**Test scenarios:**\n- Happy path: API requests from any TCP remote address route normally when auth\n requirements are satisfied.\n- Happy path: `/health` remains accessible.\n- Integration: GitHub webhook route still rejects missing/invalid signatures and\n accepts valid signatures exactly as before.\n- Cleanup: there are no references to `IpAllowlistConfig`, `ip_allowlist_middleware`,\n `GitHubMetaResolver`, `github_meta_hooks`, or `github-meta-hooks.json`.\n\n**Verification:**\n- `fabro-server` and CLI auth harness tests compile against the simplified router\n API.\n- Runtime startup no longer performs any GitHub `/meta` fetch for webhook IP\n ranges.\n\n- [ ] **Unit 3: Update OpenAPI and generated API clients**\n\n**Goal:** Remove IP allowlist fields and schemas from public settings API\ncontracts and regenerated clients.\n\n**Requirements:** R3, R4\n\n**Dependencies:** Units 1 and 2\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs`\n- Modify/generated: `lib/packages/fabro-api-client/src/models/*`\n\n**Approach:**\n- Remove `ip_allowlist` from `ServerNamespace.required` and\n `ServerNamespace.properties`.\n- Remove `ServerIpAllowlistSettings`,\n `ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,\n `LiteralIpAllowEntry`, and `GitHubMetaHooksEntry` schemas.\n- Remove `IntegrationWebhooksSettings.ip_allowlist` from required/properties.\n- Remove `with_replacement` entries and public re-exports for removed types in\n `fabro-api`.\n- Regenerate Rust API code by building `fabro-api`.\n- Regenerate TypeScript Axios client and ensure stale allowlist model files and\n index exports are gone.\n- Strengthen round-trip tests to assert settings JSON omits\n `server.ip_allowlist` and webhook `ip_allowlist`.\n\n**Patterns to follow:**\n- Existing OpenAPI-first workflow in `AGENTS.md`.\n- Existing `server_settings_family_reuses_domain_types` assertions for shared\n domain type identity.\n\n**Test scenarios:**\n- Contract: `ServerSettings` round-trips through API types without any IP\n allowlist field.\n- Contract: OpenAPI-generated TypeScript `ServerNamespace` has no\n `ip_allowlist` property.\n- Contract: `IntegrationWebhooksSettings` has only webhook strategy fields after\n removal.\n\n**Verification:**\n- Generated Rust and TypeScript clients match the updated OpenAPI spec.\n- `rg \"ServerIpAllowlist|IpAllowEntry|server-ip-allowlist|literal-ip-allow-entry\"`\n finds no remaining generated/public API references.\n\n- [ ] **Unit 4: Update web settings UI**\n\n**Goal:** Remove IP allowlist display from Settings > Security and align copy\nwith the new settings shape.\n\n**Requirements:** R3, R7\n\n**Dependencies:** Unit 3\n\n**Files:**\n- Modify: `apps/fabro-web/app/routes/settings-security.tsx`\n- Modify: `apps/fabro-web/app/routes/settings.tsx`\n\n**Approach:**\n- Change the Security page description from \"Authentication methods and network\n allowlist\" to authentication-only wording.\n- Remove destructuring and rendering of `settings.server.ip_allowlist`.\n- Remove the unused `Count` and `plural` imports if no longer used.\n- Update the Settings nav description for Security from \"Authentication and\n network allowlist\" to authentication-focused copy.\n\n**Patterns to follow:**\n- Existing `settings-panel` row layout for Auth methods and Allowed usernames.\n\n**Test scenarios:**\n- Typecheck: the route compiles against the regenerated API client with no\n `server.ip_allowlist` property.\n- UI behavior: Security page still renders auth methods and allowed usernames.\n- Cleanup: no frontend references to `ip_allowlist`, `IP allowlist`, or\n `trusted_proxy_count` remain.\n\n**Verification:**\n- `apps/fabro-web` typecheck passes against the new generated client.\n\n- [ ] **Unit 5: Update tests, docs, changelog, and dependency lockfile**\n\n**Goal:** Remove stale references and document the operator-facing behavior\nchange.\n\n**Requirements:** R4, R6, R7\n\n**Dependencies:** Units 1 through 4\n\n**Files:**\n- Modify: `lib/crates/fabro-server/tests/it/api/routing.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/tcp.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/settings.rs`\n- Modify: `docs/public/administration/security.mdx`\n- Create or modify: `docs/public/changelog/2026-05-27.mdx`\n- Modify: `Cargo.lock` if dependency graph changes\n\n**Approach:**\n- Delete route/TCP tests that only prove IP allowlist enforcement or\n `ConnectInfo` behavior.\n- Adjust any router setup helpers after the signature simplification.\n- Add a settings API assertion that `server.ip_allowlist` is absent from\n `/api/v1/settings`.\n- Update security docs to explicitly state that Fabro does not provide inbound\n source-IP allowlisting and operators should use upstream network controls.\n- Add a changelog entry dated 2026-05-27 describing the hard removal and the\n expected replacement at the deployment layer.\n- Run dependency resolution after removing direct `ipnet`/test dependencies;\n keep transitive or unrelated `ipnet` entries needed by sandbox CIDR validation.\n\n**Patterns to follow:**\n- Existing changelog style in `docs/public/changelog/2026-05-26.mdx`.\n- Existing settings API integration test style in\n `lib/crates/fabro-server/tests/it/api/settings.rs`.\n\n**Test scenarios:**\n- Settings API: response contains server auth, listen, storage, scheduler, and\n integrations fields, but not `server.ip_allowlist`.\n- Config compatibility: old IP allowlist TOML fails before startup rather than\n being silently ignored.\n- Docs validation: security docs no longer imply Fabro can restrict inbound\n source IPs internally.\n\n**Verification:**\n- `rg -n \"ip_allowlist|trusted_proxy_count|github_meta_hooks|IP allowlist|ip allowlist\"`\n returns only historical archived plan/brainstorm/spec references or unrelated\n non-server allowlist text.\n\n## System-Wide Impact\n\n- **Public API:** `GET /api/v1/settings` response shape changes by removing\n `server.ip_allowlist` and webhook `ip_allowlist`.\n- **Config compatibility:** Existing `settings.toml` files containing the removed\n keys become invalid. This is intentional.\n- **Runtime security posture:** Fabro no longer blocks requests based on source\n IP. Operators must enforce network source restrictions upstream.\n- **Webhook handling:** GitHub webhook HMAC verification remains unchanged; only\n optional source-IP filtering is removed.\n- **Generated clients:** Downstream TypeScript/Rust consumers that read\n `server.ip_allowlist` must update.\n\n## Risks & Mitigations\n\n| Risk | Mitigation |\n|------|------------|\n| Operators accidentally expose a server that previously relied on Fabro IP allowlisting | Changelog and security docs explicitly call out the removal and direct operators to upstream controls. |\n| Generated API clients retain stale types | Update OpenAPI first, regenerate both Rust and TypeScript clients, and run search checks for stale symbols. |\n| Unrelated allowlist functionality is removed by broad search/replace | Scope searches to `IpAllow`, `ip_allowlist`, `trusted_proxy_count`, and `github_meta_hooks`; preserve sandbox CIDR and GitHub username allowlists. |\n| Router signature cleanup breaks many tests | Update shared test helpers first, then compile-driven cleanup of remaining call sites. |\n| `ipnet` is removed where still needed | Keep `fabro-config` dependency if environment CIDR validation still imports `ipnet::IpNet`. |\n\n## Test Plan\n\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun test`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n\n## Assumptions\n\n- The accepted compatibility policy is hard removal: no warning-only parser, no\n migration, and no custom compatibility error path.\n- Public API consumers can tolerate a breaking settings shape change for this\n removed feature.\n- Historical documents in `docs/plans/`, `docs/brainstorms/`, and\n `docs/superpowers/` are archival and do not need rewriting.\n\n## Sources & References\n\n- Previous feature requirements:\n `docs/brainstorms/2026-04-15-ip-whitelist-requirements.md`\n- Previous implementation plan:\n `docs/plans/2026-04-15-002-feat-ip-allowlist-plan.md`\n- API workflow guidance: `AGENTS.md`\n- OpenAPI source of truth: `docs/public/api-reference/fabro-api.yaml`\n",
|
|
"internal.retry_count.start": 0,
|
|
"outcome": "succeeded",
|
|
"internal.retry_count.preflight_compile": 0,
|
|
"internal.thread_id": "toolchain",
|
|
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
|
"thread.toolchain.current_node": "preflight_compile"
|
|
},
|
|
"node_outcomes": {
|
|
"toolchain": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
|
},
|
|
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 1482,
|
|
"active_time_ms": 1482
|
|
}
|
|
},
|
|
"preflight_compile": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
|
},
|
|
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 135614,
|
|
"active_time_ms": 135614
|
|
}
|
|
},
|
|
"start": {
|
|
"status": "succeeded",
|
|
"usage": null
|
|
}
|
|
},
|
|
"next_node_id": "preflight_lint",
|
|
"git_commit_sha": "6c1e8640c7c44c6215d136832d5dbc20212709a8",
|
|
"node_visits": {
|
|
"start": 1,
|
|
"preflight_compile": 1,
|
|
"toolchain": 1
|
|
}
|
|
},
|
|
"diff": {
|
|
"summary": {
|
|
"files_changed": 0,
|
|
"additions": 0,
|
|
"deletions": 0
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"seq": 48,
|
|
"checkpoint": {
|
|
"timestamp": "2026-05-28T00:19:01.907865Z",
|
|
"current_node": "preflight_lint",
|
|
"completed_nodes": [
|
|
"start",
|
|
"toolchain",
|
|
"preflight_compile",
|
|
"preflight_lint"
|
|
],
|
|
"node_retries": {},
|
|
"context_values": {
|
|
"internal.retry_count.preflight_lint": 0,
|
|
"internal.retry_count.start": 0,
|
|
"thread.start.current_node": "toolchain",
|
|
"thread.toolchain.current_node": "preflight_compile",
|
|
"graph.rankdir": "LR",
|
|
"graph.goal": "---\ntitle: \"refactor: Remove IP allowlisting\"\ntype: refactor\nstatus: active\ndate: 2026-05-27\n---\n\n# refactor: Remove IP allowlisting\n\n## Overview\n\nRemove Fabro's inbound server IP allowlisting feature completely. This removes the\nserver-wide `[server.ip_allowlist]` setting, the GitHub webhook-specific\n`[server.integrations.github.webhooks.ip_allowlist]` overlay, request middleware,\nclient-IP extraction, GitHub `/meta` hook-range expansion, settings API exposure,\ngenerated API client types, and the Settings > Security UI display.\n\nExisting configs that still contain removed IP allowlist keys should fail as\nunknown fields. This is an intentional hard removal, not a compatibility\ndeprecation.\n\n## Problem Frame\n\nThe feature is being removed from Fabro rather than maintained as an in-process\nnetwork access-control layer. Network source restrictions should be handled\noutside Fabro by reverse proxies, firewalls, VPNs, Tailscale, platform ingress,\nor other deployment-layer controls.\n\n## Requirements Trace\n\n- R1. Remove all runtime enforcement of inbound source-IP allowlisting from web,\n API, static asset, and GitHub webhook routes.\n- R2. Remove the server settings schema for global and GitHub webhook IP\n allowlists.\n- R3. Remove `IpAllowEntry` and related API schema/client types from public\n settings payloads.\n- R4. Keep unrelated allowlists intact: GitHub username allowlists, sandbox\n egress CIDR allow lists, and internal policy/test allowlists.\n- R5. Preserve existing authentication, GitHub webhook HMAC verification,\n routing, health checks, request logging, and settings hot-reload behavior.\n- R6. Treat old IP allowlist config as invalid after removal.\n- R7. Update operator-facing docs/changelog so users know to move source-IP\n restrictions upstream.\n\n## Scope Boundaries\n\n- Do not remove `[server.auth.github].allowed_usernames`.\n- Do not remove Daytona/sandbox `cidr_allow_list` network policy support.\n- Do not remove generic uses of \"allowlist\" in policy tests, markdown rendering,\n model controls, or other unrelated domains.\n- Do not add a migration, warning-only parser, or fallback compatibility path for\n old IP allowlist settings.\n- Do not change GitHub webhook signature verification or webhook route strategy.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- Settings use sparse config layers in `lib/crates/fabro-config/src/layers/`\n and dense resolved types in `lib/crates/fabro-types/src/settings/`.\n- The OpenAPI spec at `docs/public/api-reference/fabro-api.yaml` is the source\n of truth for API wire shape; `cargo build -p fabro-api` regenerates Rust API\n code, and `cd lib/packages/fabro-api-client && bun run generate` regenerates\n the TypeScript client.\n- `lib/crates/fabro-server/src/server.rs` builds the router and currently wires\n IP allowlist middleware before other route dispatch behavior.\n- `lib/crates/fabro-server/src/serve.rs` currently resolves server and webhook\n allowlist configs at startup and creates a GitHub `/meta` resolver.\n- `apps/fabro-web/app/routes/settings-security.tsx` displays the settings API's\n `server.ip_allowlist` state.\n\n### Main Removal Surfaces\n\n- Config/types: `fabro-types`, `fabro-config`, config tests.\n- Server runtime: `fabro-server/src/ip_allowlist.rs`, router wiring, startup\n wiring, test helpers, routing/TCP tests.\n- API/contracts: OpenAPI schema, `fabro-api` type replacements/exports, generated\n TypeScript API client models.\n- Frontend/docs: Settings > Security copy, settings navigation copy, changelog.\n\n## Key Technical Decisions\n\n- **Hard removal:** Removed config keys should fail through existing\n `deny_unknown_fields` behavior. This keeps the change simple and makes stale\n deployment config visible immediately.\n- **Delete, do not stub:** Remove `IpAllowlistConfig` and middleware parameters\n instead of passing default empty configs through the router. Empty stubs would\n keep the feature shape alive and make future cleanup harder.\n- **Keep webhook auth unchanged:** GitHub webhook source-IP filtering goes away,\n but HMAC signature verification remains the security boundary for webhook\n payload authenticity.\n- **Generated clients follow OpenAPI:** Update the OpenAPI schemas first, then\n regenerate Rust and TypeScript API outputs rather than hand-editing generated\n client files except as a short-term cleanup if generation leaves stale exports.\n- **Docs mention upstream controls:** The changelog and security guidance should\n direct operators to network-layer controls, not a replacement Fabro setting.\n\n## Implementation Units\n\n- [ ] **Unit 1: Remove config schema and resolved types**\n\n**Goal:** Delete the IP allowlist settings contract from config parsing and dense\nserver settings.\n\n**Requirements:** R2, R3, R4, R6\n\n**Dependencies:** None\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/settings/server.rs`\n- Modify: `lib/crates/fabro-types/src/settings/mod.rs`\n- Modify: `lib/crates/fabro-types/Cargo.toml`\n- Modify: `lib/crates/fabro-config/src/layers/server.rs`\n- Modify: `lib/crates/fabro-config/src/layers/mod.rs`\n- Modify: `lib/crates/fabro-config/src/lib.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/server.rs`\n- Modify: `lib/crates/fabro-config/src/tests/resolve_server.rs`\n\n**Approach:**\n- Remove `ServerNamespace.ip_allowlist` and its `test_default()` population.\n- Delete `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, and\n `IpAllowEntry`.\n- Remove `ServerLayer.ip_allowlist`, `ServerIpAllowlistLayer`,\n `ServerIpAllowlistOverrideLayer`, and `IntegrationWebhooksLayer.ip_allowlist`.\n- Remove resolver functions and validation for global and webhook IP allowlists,\n including `github_meta_hooks` parsing and Unix socket trusted-proxy checks.\n- Remove `ipnet` from `fabro-types`. Keep `ipnet` in `fabro-config` because\n `resolve/environment.rs` still validates sandbox CIDR policy.\n- Replace old positive/negative IP allowlist config tests with unknown-field\n tests for `[server.ip_allowlist]` and\n `[server.integrations.github.webhooks.ip_allowlist]`.\n\n**Patterns to follow:**\n- Existing unknown-field tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`\n for retired server settings.\n- Existing `ServerLayer`/`ServerNamespace` layer-to-resolved pattern for removing\n a server subdomain cleanly.\n\n**Test scenarios:**\n- Error path: TOML with `[server.ip_allowlist]` fails parsing/resolution with an\n unknown-field diagnostic mentioning `ip_allowlist`.\n- Error path: TOML with `[server.integrations.github.webhooks.ip_allowlist]`\n fails with an unknown-field diagnostic mentioning `ip_allowlist`.\n- Happy path: minimal valid server settings still resolve without any\n `ip_allowlist` field.\n- Integration: serialized `ServerSettings` JSON no longer includes\n `server.ip_allowlist`.\n\n**Verification:**\n- `fabro-config` and `fabro-types` compile without removed IP allowlist symbols.\n- Config tests prove stale allowlist settings are rejected.\n\n- [ ] **Unit 2: Remove server middleware and startup resolution**\n\n**Goal:** Remove all runtime source-IP filtering and GitHub `/meta` range\nresolution from `fabro-server`.\n\n**Requirements:** R1, R4, R5\n\n**Dependencies:** Unit 1\n\n**Files:**\n- Delete: `lib/crates/fabro-server/src/ip_allowlist.rs`\n- Modify: `lib/crates/fabro-server/src/lib.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Modify: `lib/crates/fabro-server/src/serve.rs`\n- Modify: `lib/crates/fabro-server/src/test_support.rs`\n- Modify: `lib/crates/fabro-server/Cargo.toml`\n- Modify: `lib/crates/fabro-server/src/auth/translate.rs`\n- Modify: `lib/crates/fabro-server/src/web_auth.rs`\n- Modify: `lib/crates/fabro-cli/tests/it/support/auth_harness.rs`\n\n**Approach:**\n- Remove the public `ip_allowlist` module export.\n- Delete `IpAllowlistConfig`, `IpAllowlist`, `GitHubMetaResolver`, client-IP\n extraction, middleware, and GitHub meta cache helpers.\n- Simplify `build_router_with_options` by removing its `Arc<IpAllowlistConfig>`\n parameter and removing `RouterOptions.github_webhook_ip_allowlist`.\n- Remove the global allowlist middleware layer from the main app router.\n- Simplify `github_webhook_routes` so it only receives the webhook secret and no\n route-specific allowlist config.\n- Remove startup creation of `GitHubMetaResolver`, `default_ip_allowlist`,\n `resolve_github_webhook_ip_allowlist`, and\n `resolve_startup_github_webhook_ip_allowlist`.\n- Replace all test/helper call sites that pass `IpAllowlistConfig::default()`\n with the simplified router signature.\n- Remove `ipnet` and any now-unused HTTP mocking/test-only dependencies from\n `fabro-server` if they are only used by the deleted module.\n- Consider whether `serve.rs` still needs `SocketAddr` for TCP\n `ConnectInfo`; if it was only present for allowlisting tests, remove the\n connect-info service wrapper and imports.\n\n**Patterns to follow:**\n- Keep router layers ordered as they are after the allowlist layer is removed:\n auth translation, demo routing, auth extension, canonical host, security\n headers, HTTP logging, request ID.\n- Existing webhook route HMAC tests and auth tests should remain the source of\n truth for webhook behavior.\n\n**Test scenarios:**\n- Happy path: API requests from any TCP remote address route normally when auth\n requirements are satisfied.\n- Happy path: `/health` remains accessible.\n- Integration: GitHub webhook route still rejects missing/invalid signatures and\n accepts valid signatures exactly as before.\n- Cleanup: there are no references to `IpAllowlistConfig`, `ip_allowlist_middleware`,\n `GitHubMetaResolver`, `github_meta_hooks`, or `github-meta-hooks.json`.\n\n**Verification:**\n- `fabro-server` and CLI auth harness tests compile against the simplified router\n API.\n- Runtime startup no longer performs any GitHub `/meta` fetch for webhook IP\n ranges.\n\n- [ ] **Unit 3: Update OpenAPI and generated API clients**\n\n**Goal:** Remove IP allowlist fields and schemas from public settings API\ncontracts and regenerated clients.\n\n**Requirements:** R3, R4\n\n**Dependencies:** Units 1 and 2\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs`\n- Modify/generated: `lib/packages/fabro-api-client/src/models/*`\n\n**Approach:**\n- Remove `ip_allowlist` from `ServerNamespace.required` and\n `ServerNamespace.properties`.\n- Remove `ServerIpAllowlistSettings`,\n `ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,\n `LiteralIpAllowEntry`, and `GitHubMetaHooksEntry` schemas.\n- Remove `IntegrationWebhooksSettings.ip_allowlist` from required/properties.\n- Remove `with_replacement` entries and public re-exports for removed types in\n `fabro-api`.\n- Regenerate Rust API code by building `fabro-api`.\n- Regenerate TypeScript Axios client and ensure stale allowlist model files and\n index exports are gone.\n- Strengthen round-trip tests to assert settings JSON omits\n `server.ip_allowlist` and webhook `ip_allowlist`.\n\n**Patterns to follow:**\n- Existing OpenAPI-first workflow in `AGENTS.md`.\n- Existing `server_settings_family_reuses_domain_types` assertions for shared\n domain type identity.\n\n**Test scenarios:**\n- Contract: `ServerSettings` round-trips through API types without any IP\n allowlist field.\n- Contract: OpenAPI-generated TypeScript `ServerNamespace` has no\n `ip_allowlist` property.\n- Contract: `IntegrationWebhooksSettings` has only webhook strategy fields after\n removal.\n\n**Verification:**\n- Generated Rust and TypeScript clients match the updated OpenAPI spec.\n- `rg \"ServerIpAllowlist|IpAllowEntry|server-ip-allowlist|literal-ip-allow-entry\"`\n finds no remaining generated/public API references.\n\n- [ ] **Unit 4: Update web settings UI**\n\n**Goal:** Remove IP allowlist display from Settings > Security and align copy\nwith the new settings shape.\n\n**Requirements:** R3, R7\n\n**Dependencies:** Unit 3\n\n**Files:**\n- Modify: `apps/fabro-web/app/routes/settings-security.tsx`\n- Modify: `apps/fabro-web/app/routes/settings.tsx`\n\n**Approach:**\n- Change the Security page description from \"Authentication methods and network\n allowlist\" to authentication-only wording.\n- Remove destructuring and rendering of `settings.server.ip_allowlist`.\n- Remove the unused `Count` and `plural` imports if no longer used.\n- Update the Settings nav description for Security from \"Authentication and\n network allowlist\" to authentication-focused copy.\n\n**Patterns to follow:**\n- Existing `settings-panel` row layout for Auth methods and Allowed usernames.\n\n**Test scenarios:**\n- Typecheck: the route compiles against the regenerated API client with no\n `server.ip_allowlist` property.\n- UI behavior: Security page still renders auth methods and allowed usernames.\n- Cleanup: no frontend references to `ip_allowlist`, `IP allowlist`, or\n `trusted_proxy_count` remain.\n\n**Verification:**\n- `apps/fabro-web` typecheck passes against the new generated client.\n\n- [ ] **Unit 5: Update tests, docs, changelog, and dependency lockfile**\n\n**Goal:** Remove stale references and document the operator-facing behavior\nchange.\n\n**Requirements:** R4, R6, R7\n\n**Dependencies:** Units 1 through 4\n\n**Files:**\n- Modify: `lib/crates/fabro-server/tests/it/api/routing.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/tcp.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/settings.rs`\n- Modify: `docs/public/administration/security.mdx`\n- Create or modify: `docs/public/changelog/2026-05-27.mdx`\n- Modify: `Cargo.lock` if dependency graph changes\n\n**Approach:**\n- Delete route/TCP tests that only prove IP allowlist enforcement or\n `ConnectInfo` behavior.\n- Adjust any router setup helpers after the signature simplification.\n- Add a settings API assertion that `server.ip_allowlist` is absent from\n `/api/v1/settings`.\n- Update security docs to explicitly state that Fabro does not provide inbound\n source-IP allowlisting and operators should use upstream network controls.\n- Add a changelog entry dated 2026-05-27 describing the hard removal and the\n expected replacement at the deployment layer.\n- Run dependency resolution after removing direct `ipnet`/test dependencies;\n keep transitive or unrelated `ipnet` entries needed by sandbox CIDR validation.\n\n**Patterns to follow:**\n- Existing changelog style in `docs/public/changelog/2026-05-26.mdx`.\n- Existing settings API integration test style in\n `lib/crates/fabro-server/tests/it/api/settings.rs`.\n\n**Test scenarios:**\n- Settings API: response contains server auth, listen, storage, scheduler, and\n integrations fields, but not `server.ip_allowlist`.\n- Config compatibility: old IP allowlist TOML fails before startup rather than\n being silently ignored.\n- Docs validation: security docs no longer imply Fabro can restrict inbound\n source IPs internally.\n\n**Verification:**\n- `rg -n \"ip_allowlist|trusted_proxy_count|github_meta_hooks|IP allowlist|ip allowlist\"`\n returns only historical archived plan/brainstorm/spec references or unrelated\n non-server allowlist text.\n\n## System-Wide Impact\n\n- **Public API:** `GET /api/v1/settings` response shape changes by removing\n `server.ip_allowlist` and webhook `ip_allowlist`.\n- **Config compatibility:** Existing `settings.toml` files containing the removed\n keys become invalid. This is intentional.\n- **Runtime security posture:** Fabro no longer blocks requests based on source\n IP. Operators must enforce network source restrictions upstream.\n- **Webhook handling:** GitHub webhook HMAC verification remains unchanged; only\n optional source-IP filtering is removed.\n- **Generated clients:** Downstream TypeScript/Rust consumers that read\n `server.ip_allowlist` must update.\n\n## Risks & Mitigations\n\n| Risk | Mitigation |\n|------|------------|\n| Operators accidentally expose a server that previously relied on Fabro IP allowlisting | Changelog and security docs explicitly call out the removal and direct operators to upstream controls. |\n| Generated API clients retain stale types | Update OpenAPI first, regenerate both Rust and TypeScript clients, and run search checks for stale symbols. |\n| Unrelated allowlist functionality is removed by broad search/replace | Scope searches to `IpAllow`, `ip_allowlist`, `trusted_proxy_count`, and `github_meta_hooks`; preserve sandbox CIDR and GitHub username allowlists. |\n| Router signature cleanup breaks many tests | Update shared test helpers first, then compile-driven cleanup of remaining call sites. |\n| `ipnet` is removed where still needed | Keep `fabro-config` dependency if environment CIDR validation still imports `ipnet::IpNet`. |\n\n## Test Plan\n\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun test`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n\n## Assumptions\n\n- The accepted compatibility policy is hard removal: no warning-only parser, no\n migration, and no custom compatibility error path.\n- Public API consumers can tolerate a breaking settings shape change for this\n removed feature.\n- Historical documents in `docs/plans/`, `docs/brainstorms/`, and\n `docs/superpowers/` are archival and do not need rewriting.\n\n## Sources & References\n\n- Previous feature requirements:\n `docs/brainstorms/2026-04-15-ip-whitelist-requirements.md`\n- Previous implementation plan:\n `docs/plans/2026-04-15-002-feat-ip-allowlist-plan.md`\n- API workflow guidance: `AGENTS.md`\n- OpenAPI source of truth: `docs/public/api-reference/fabro-api.yaml`\n",
|
|
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
|
"current_node": "preflight_lint",
|
|
"failure_signature": "",
|
|
"outcome": "succeeded",
|
|
"thread.preflight_compile.current_node": "preflight_lint",
|
|
"internal.fidelity": "compact",
|
|
"internal.retry_count.toolchain": 0,
|
|
"internal.run_id": "01KSNYVQCKXPCSCEGWMZ1Q574Z",
|
|
"internal.thread_id": "preflight_compile",
|
|
"internal.work_dir": "/home/daytona/workspace/fabro",
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
|
|
"internal.node_visit_count": 1,
|
|
"internal.retry_count.preflight_compile": 0,
|
|
"failure_class": ""
|
|
},
|
|
"node_outcomes": {
|
|
"start": {
|
|
"status": "succeeded",
|
|
"usage": null
|
|
},
|
|
"toolchain": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
|
},
|
|
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 1482,
|
|
"active_time_ms": 1482
|
|
}
|
|
},
|
|
"preflight_compile": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
|
},
|
|
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 135614,
|
|
"active_time_ms": 135614
|
|
}
|
|
},
|
|
"preflight_lint": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
|
},
|
|
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 142983,
|
|
"active_time_ms": 142983
|
|
}
|
|
}
|
|
},
|
|
"next_node_id": "implement",
|
|
"git_commit_sha": "00a7336c611cbf8da8f670404e8d053802cbac6d",
|
|
"node_visits": {
|
|
"preflight_compile": 1,
|
|
"preflight_lint": 1,
|
|
"toolchain": 1,
|
|
"start": 1
|
|
}
|
|
},
|
|
"diff": {
|
|
"summary": {
|
|
"files_changed": 0,
|
|
"additions": 0,
|
|
"deletions": 0
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"seq": 707,
|
|
"checkpoint": {
|
|
"timestamp": "2026-05-28T00:53:01.019764Z",
|
|
"current_node": "implement",
|
|
"completed_nodes": [
|
|
"start",
|
|
"toolchain",
|
|
"preflight_compile",
|
|
"preflight_lint",
|
|
"implement"
|
|
],
|
|
"node_retries": {},
|
|
"context_values": {
|
|
"internal.thread_id": "preflight_lint",
|
|
"last_response": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.int",
|
|
"internal.retry_count.preflight_lint": 0,
|
|
"thread.toolchain.current_node": "preflight_compile",
|
|
"internal.retry_count.toolchain": 0,
|
|
"thread.preflight_lint.current_node": "implement",
|
|
"graph.rankdir": "LR",
|
|
"current_node": "implement",
|
|
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
|
"internal.retry_count.start": 0,
|
|
"response.implement": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.integrations.github.webhooks.ip_allowlist]`\n - `IpAllowEntry` and related settings/types.\n- Deleted server runtime enforcement:\n - Removed `ip_allowlist` middleware/module.\n - Removed `IpAllowlistConfig`, client-IP extraction, trusted proxy handling, and GitHub `/meta` hook range resolution.\n - Simplified router/startup wiring and removed `ConnectInfo` serving requirements that only existed for allowlisting.\n- Preserved unrelated allowlists:\n - GitHub username allowlists remain.\n - Sandbox/Daytona CIDR allow lists remain.\n- Updated OpenAPI and generated clients:\n - Removed allowlist schemas/fields from `fabro-api.yaml`.\n - Regenerated Rust API and TypeScript API client.\n - Deleted stale generated TS allowlist model files.\n- Updated web UI:\n - Removed Settings > Security network allowlist display.\n - Adjusted Security page/nav copy.\n- Updated tests/docs:\n - Added unknown-field tests proving stale IP allowlist config is now rejected.\n - Added settings API assertions that removed fields are absent.\n - Updated security docs and changelog to direct operators to upstream network controls.\n- Updated dependency graph:\n - Removed direct `ipnet` deps from `fabro-types` and `fabro-server`; retained transitive/use in `fabro-config` for sandbox CIDR validation.\n\nRed/green TDD:\n- Added stale-config rejection tests and verified they failed before schema removal, then passed after removal.\n\nValidation run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo build -p fabro-cli --bin fabro`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server --no-fail-fast`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `git diff --check`\n- Stale-symbol search confirmed remaining `ip_allowlist`/IP allowlist references are only intentional docs/tests/assertions.\n\nNo commits were created.",
|
|
"thread.preflight_compile.current_node": "preflight_lint",
|
|
"internal.node_visit_count": 1,
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
|
|
"graph.goal": "---\ntitle: \"refactor: Remove IP allowlisting\"\ntype: refactor\nstatus: active\ndate: 2026-05-27\n---\n\n# refactor: Remove IP allowlisting\n\n## Overview\n\nRemove Fabro's inbound server IP allowlisting feature completely. This removes the\nserver-wide `[server.ip_allowlist]` setting, the GitHub webhook-specific\n`[server.integrations.github.webhooks.ip_allowlist]` overlay, request middleware,\nclient-IP extraction, GitHub `/meta` hook-range expansion, settings API exposure,\ngenerated API client types, and the Settings > Security UI display.\n\nExisting configs that still contain removed IP allowlist keys should fail as\nunknown fields. This is an intentional hard removal, not a compatibility\ndeprecation.\n\n## Problem Frame\n\nThe feature is being removed from Fabro rather than maintained as an in-process\nnetwork access-control layer. Network source restrictions should be handled\noutside Fabro by reverse proxies, firewalls, VPNs, Tailscale, platform ingress,\nor other deployment-layer controls.\n\n## Requirements Trace\n\n- R1. Remove all runtime enforcement of inbound source-IP allowlisting from web,\n API, static asset, and GitHub webhook routes.\n- R2. Remove the server settings schema for global and GitHub webhook IP\n allowlists.\n- R3. Remove `IpAllowEntry` and related API schema/client types from public\n settings payloads.\n- R4. Keep unrelated allowlists intact: GitHub username allowlists, sandbox\n egress CIDR allow lists, and internal policy/test allowlists.\n- R5. Preserve existing authentication, GitHub webhook HMAC verification,\n routing, health checks, request logging, and settings hot-reload behavior.\n- R6. Treat old IP allowlist config as invalid after removal.\n- R7. Update operator-facing docs/changelog so users know to move source-IP\n restrictions upstream.\n\n## Scope Boundaries\n\n- Do not remove `[server.auth.github].allowed_usernames`.\n- Do not remove Daytona/sandbox `cidr_allow_list` network policy support.\n- Do not remove generic uses of \"allowlist\" in policy tests, markdown rendering,\n model controls, or other unrelated domains.\n- Do not add a migration, warning-only parser, or fallback compatibility path for\n old IP allowlist settings.\n- Do not change GitHub webhook signature verification or webhook route strategy.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- Settings use sparse config layers in `lib/crates/fabro-config/src/layers/`\n and dense resolved types in `lib/crates/fabro-types/src/settings/`.\n- The OpenAPI spec at `docs/public/api-reference/fabro-api.yaml` is the source\n of truth for API wire shape; `cargo build -p fabro-api` regenerates Rust API\n code, and `cd lib/packages/fabro-api-client && bun run generate` regenerates\n the TypeScript client.\n- `lib/crates/fabro-server/src/server.rs` builds the router and currently wires\n IP allowlist middleware before other route dispatch behavior.\n- `lib/crates/fabro-server/src/serve.rs` currently resolves server and webhook\n allowlist configs at startup and creates a GitHub `/meta` resolver.\n- `apps/fabro-web/app/routes/settings-security.tsx` displays the settings API's\n `server.ip_allowlist` state.\n\n### Main Removal Surfaces\n\n- Config/types: `fabro-types`, `fabro-config`, config tests.\n- Server runtime: `fabro-server/src/ip_allowlist.rs`, router wiring, startup\n wiring, test helpers, routing/TCP tests.\n- API/contracts: OpenAPI schema, `fabro-api` type replacements/exports, generated\n TypeScript API client models.\n- Frontend/docs: Settings > Security copy, settings navigation copy, changelog.\n\n## Key Technical Decisions\n\n- **Hard removal:** Removed config keys should fail through existing\n `deny_unknown_fields` behavior. This keeps the change simple and makes stale\n deployment config visible immediately.\n- **Delete, do not stub:** Remove `IpAllowlistConfig` and middleware parameters\n instead of passing default empty configs through the router. Empty stubs would\n keep the feature shape alive and make future cleanup harder.\n- **Keep webhook auth unchanged:** GitHub webhook source-IP filtering goes away,\n but HMAC signature verification remains the security boundary for webhook\n payload authenticity.\n- **Generated clients follow OpenAPI:** Update the OpenAPI schemas first, then\n regenerate Rust and TypeScript API outputs rather than hand-editing generated\n client files except as a short-term cleanup if generation leaves stale exports.\n- **Docs mention upstream controls:** The changelog and security guidance should\n direct operators to network-layer controls, not a replacement Fabro setting.\n\n## Implementation Units\n\n- [ ] **Unit 1: Remove config schema and resolved types**\n\n**Goal:** Delete the IP allowlist settings contract from config parsing and dense\nserver settings.\n\n**Requirements:** R2, R3, R4, R6\n\n**Dependencies:** None\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/settings/server.rs`\n- Modify: `lib/crates/fabro-types/src/settings/mod.rs`\n- Modify: `lib/crates/fabro-types/Cargo.toml`\n- Modify: `lib/crates/fabro-config/src/layers/server.rs`\n- Modify: `lib/crates/fabro-config/src/layers/mod.rs`\n- Modify: `lib/crates/fabro-config/src/lib.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/server.rs`\n- Modify: `lib/crates/fabro-config/src/tests/resolve_server.rs`\n\n**Approach:**\n- Remove `ServerNamespace.ip_allowlist` and its `test_default()` population.\n- Delete `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, and\n `IpAllowEntry`.\n- Remove `ServerLayer.ip_allowlist`, `ServerIpAllowlistLayer`,\n `ServerIpAllowlistOverrideLayer`, and `IntegrationWebhooksLayer.ip_allowlist`.\n- Remove resolver functions and validation for global and webhook IP allowlists,\n including `github_meta_hooks` parsing and Unix socket trusted-proxy checks.\n- Remove `ipnet` from `fabro-types`. Keep `ipnet` in `fabro-config` because\n `resolve/environment.rs` still validates sandbox CIDR policy.\n- Replace old positive/negative IP allowlist config tests with unknown-field\n tests for `[server.ip_allowlist]` and\n `[server.integrations.github.webhooks.ip_allowlist]`.\n\n**Patterns to follow:**\n- Existing unknown-field tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`\n for retired server settings.\n- Existing `ServerLayer`/`ServerNamespace` layer-to-resolved pattern for removing\n a server subdomain cleanly.\n\n**Test scenarios:**\n- Error path: TOML with `[server.ip_allowlist]` fails parsing/resolution with an\n unknown-field diagnostic mentioning `ip_allowlist`.\n- Error path: TOML with `[server.integrations.github.webhooks.ip_allowlist]`\n fails with an unknown-field diagnostic mentioning `ip_allowlist`.\n- Happy path: minimal valid server settings still resolve without any\n `ip_allowlist` field.\n- Integration: serialized `ServerSettings` JSON no longer includes\n `server.ip_allowlist`.\n\n**Verification:**\n- `fabro-config` and `fabro-types` compile without removed IP allowlist symbols.\n- Config tests prove stale allowlist settings are rejected.\n\n- [ ] **Unit 2: Remove server middleware and startup resolution**\n\n**Goal:** Remove all runtime source-IP filtering and GitHub `/meta` range\nresolution from `fabro-server`.\n\n**Requirements:** R1, R4, R5\n\n**Dependencies:** Unit 1\n\n**Files:**\n- Delete: `lib/crates/fabro-server/src/ip_allowlist.rs`\n- Modify: `lib/crates/fabro-server/src/lib.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Modify: `lib/crates/fabro-server/src/serve.rs`\n- Modify: `lib/crates/fabro-server/src/test_support.rs`\n- Modify: `lib/crates/fabro-server/Cargo.toml`\n- Modify: `lib/crates/fabro-server/src/auth/translate.rs`\n- Modify: `lib/crates/fabro-server/src/web_auth.rs`\n- Modify: `lib/crates/fabro-cli/tests/it/support/auth_harness.rs`\n\n**Approach:**\n- Remove the public `ip_allowlist` module export.\n- Delete `IpAllowlistConfig`, `IpAllowlist`, `GitHubMetaResolver`, client-IP\n extraction, middleware, and GitHub meta cache helpers.\n- Simplify `build_router_with_options` by removing its `Arc<IpAllowlistConfig>`\n parameter and removing `RouterOptions.github_webhook_ip_allowlist`.\n- Remove the global allowlist middleware layer from the main app router.\n- Simplify `github_webhook_routes` so it only receives the webhook secret and no\n route-specific allowlist config.\n- Remove startup creation of `GitHubMetaResolver`, `default_ip_allowlist`,\n `resolve_github_webhook_ip_allowlist`, and\n `resolve_startup_github_webhook_ip_allowlist`.\n- Replace all test/helper call sites that pass `IpAllowlistConfig::default()`\n with the simplified router signature.\n- Remove `ipnet` and any now-unused HTTP mocking/test-only dependencies from\n `fabro-server` if they are only used by the deleted module.\n- Consider whether `serve.rs` still needs `SocketAddr` for TCP\n `ConnectInfo`; if it was only present for allowlisting tests, remove the\n connect-info service wrapper and imports.\n\n**Patterns to follow:**\n- Keep router layers ordered as they are after the allowlist layer is removed:\n auth translation, demo routing, auth extension, canonical host, security\n headers, HTTP logging, request ID.\n- Existing webhook route HMAC tests and auth tests should remain the source of\n truth for webhook behavior.\n\n**Test scenarios:**\n- Happy path: API requests from any TCP remote address route normally when auth\n requirements are satisfied.\n- Happy path: `/health` remains accessible.\n- Integration: GitHub webhook route still rejects missing/invalid signatures and\n accepts valid signatures exactly as before.\n- Cleanup: there are no references to `IpAllowlistConfig`, `ip_allowlist_middleware`,\n `GitHubMetaResolver`, `github_meta_hooks`, or `github-meta-hooks.json`.\n\n**Verification:**\n- `fabro-server` and CLI auth harness tests compile against the simplified router\n API.\n- Runtime startup no longer performs any GitHub `/meta` fetch for webhook IP\n ranges.\n\n- [ ] **Unit 3: Update OpenAPI and generated API clients**\n\n**Goal:** Remove IP allowlist fields and schemas from public settings API\ncontracts and regenerated clients.\n\n**Requirements:** R3, R4\n\n**Dependencies:** Units 1 and 2\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs`\n- Modify/generated: `lib/packages/fabro-api-client/src/models/*`\n\n**Approach:**\n- Remove `ip_allowlist` from `ServerNamespace.required` and\n `ServerNamespace.properties`.\n- Remove `ServerIpAllowlistSettings`,\n `ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,\n `LiteralIpAllowEntry`, and `GitHubMetaHooksEntry` schemas.\n- Remove `IntegrationWebhooksSettings.ip_allowlist` from required/properties.\n- Remove `with_replacement` entries and public re-exports for removed types in\n `fabro-api`.\n- Regenerate Rust API code by building `fabro-api`.\n- Regenerate TypeScript Axios client and ensure stale allowlist model files and\n index exports are gone.\n- Strengthen round-trip tests to assert settings JSON omits\n `server.ip_allowlist` and webhook `ip_allowlist`.\n\n**Patterns to follow:**\n- Existing OpenAPI-first workflow in `AGENTS.md`.\n- Existing `server_settings_family_reuses_domain_types` assertions for shared\n domain type identity.\n\n**Test scenarios:**\n- Contract: `ServerSettings` round-trips through API types without any IP\n allowlist field.\n- Contract: OpenAPI-generated TypeScript `ServerNamespace` has no\n `ip_allowlist` property.\n- Contract: `IntegrationWebhooksSettings` has only webhook strategy fields after\n removal.\n\n**Verification:**\n- Generated Rust and TypeScript clients match the updated OpenAPI spec.\n- `rg \"ServerIpAllowlist|IpAllowEntry|server-ip-allowlist|literal-ip-allow-entry\"`\n finds no remaining generated/public API references.\n\n- [ ] **Unit 4: Update web settings UI**\n\n**Goal:** Remove IP allowlist display from Settings > Security and align copy\nwith the new settings shape.\n\n**Requirements:** R3, R7\n\n**Dependencies:** Unit 3\n\n**Files:**\n- Modify: `apps/fabro-web/app/routes/settings-security.tsx`\n- Modify: `apps/fabro-web/app/routes/settings.tsx`\n\n**Approach:**\n- Change the Security page description from \"Authentication methods and network\n allowlist\" to authentication-only wording.\n- Remove destructuring and rendering of `settings.server.ip_allowlist`.\n- Remove the unused `Count` and `plural` imports if no longer used.\n- Update the Settings nav description for Security from \"Authentication and\n network allowlist\" to authentication-focused copy.\n\n**Patterns to follow:**\n- Existing `settings-panel` row layout for Auth methods and Allowed usernames.\n\n**Test scenarios:**\n- Typecheck: the route compiles against the regenerated API client with no\n `server.ip_allowlist` property.\n- UI behavior: Security page still renders auth methods and allowed usernames.\n- Cleanup: no frontend references to `ip_allowlist`, `IP allowlist`, or\n `trusted_proxy_count` remain.\n\n**Verification:**\n- `apps/fabro-web` typecheck passes against the new generated client.\n\n- [ ] **Unit 5: Update tests, docs, changelog, and dependency lockfile**\n\n**Goal:** Remove stale references and document the operator-facing behavior\nchange.\n\n**Requirements:** R4, R6, R7\n\n**Dependencies:** Units 1 through 4\n\n**Files:**\n- Modify: `lib/crates/fabro-server/tests/it/api/routing.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/tcp.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/settings.rs`\n- Modify: `docs/public/administration/security.mdx`\n- Create or modify: `docs/public/changelog/2026-05-27.mdx`\n- Modify: `Cargo.lock` if dependency graph changes\n\n**Approach:**\n- Delete route/TCP tests that only prove IP allowlist enforcement or\n `ConnectInfo` behavior.\n- Adjust any router setup helpers after the signature simplification.\n- Add a settings API assertion that `server.ip_allowlist` is absent from\n `/api/v1/settings`.\n- Update security docs to explicitly state that Fabro does not provide inbound\n source-IP allowlisting and operators should use upstream network controls.\n- Add a changelog entry dated 2026-05-27 describing the hard removal and the\n expected replacement at the deployment layer.\n- Run dependency resolution after removing direct `ipnet`/test dependencies;\n keep transitive or unrelated `ipnet` entries needed by sandbox CIDR validation.\n\n**Patterns to follow:**\n- Existing changelog style in `docs/public/changelog/2026-05-26.mdx`.\n- Existing settings API integration test style in\n `lib/crates/fabro-server/tests/it/api/settings.rs`.\n\n**Test scenarios:**\n- Settings API: response contains server auth, listen, storage, scheduler, and\n integrations fields, but not `server.ip_allowlist`.\n- Config compatibility: old IP allowlist TOML fails before startup rather than\n being silently ignored.\n- Docs validation: security docs no longer imply Fabro can restrict inbound\n source IPs internally.\n\n**Verification:**\n- `rg -n \"ip_allowlist|trusted_proxy_count|github_meta_hooks|IP allowlist|ip allowlist\"`\n returns only historical archived plan/brainstorm/spec references or unrelated\n non-server allowlist text.\n\n## System-Wide Impact\n\n- **Public API:** `GET /api/v1/settings` response shape changes by removing\n `server.ip_allowlist` and webhook `ip_allowlist`.\n- **Config compatibility:** Existing `settings.toml` files containing the removed\n keys become invalid. This is intentional.\n- **Runtime security posture:** Fabro no longer blocks requests based on source\n IP. Operators must enforce network source restrictions upstream.\n- **Webhook handling:** GitHub webhook HMAC verification remains unchanged; only\n optional source-IP filtering is removed.\n- **Generated clients:** Downstream TypeScript/Rust consumers that read\n `server.ip_allowlist` must update.\n\n## Risks & Mitigations\n\n| Risk | Mitigation |\n|------|------------|\n| Operators accidentally expose a server that previously relied on Fabro IP allowlisting | Changelog and security docs explicitly call out the removal and direct operators to upstream controls. |\n| Generated API clients retain stale types | Update OpenAPI first, regenerate both Rust and TypeScript clients, and run search checks for stale symbols. |\n| Unrelated allowlist functionality is removed by broad search/replace | Scope searches to `IpAllow`, `ip_allowlist`, `trusted_proxy_count`, and `github_meta_hooks`; preserve sandbox CIDR and GitHub username allowlists. |\n| Router signature cleanup breaks many tests | Update shared test helpers first, then compile-driven cleanup of remaining call sites. |\n| `ipnet` is removed where still needed | Keep `fabro-config` dependency if environment CIDR validation still imports `ipnet::IpNet`. |\n\n## Test Plan\n\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun test`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n\n## Assumptions\n\n- The accepted compatibility policy is hard removal: no warning-only parser, no\n migration, and no custom compatibility error path.\n- Public API consumers can tolerate a breaking settings shape change for this\n removed feature.\n- Historical documents in `docs/plans/`, `docs/brainstorms/`, and\n `docs/superpowers/` are archival and do not need rewriting.\n\n## Sources & References\n\n- Previous feature requirements:\n `docs/brainstorms/2026-04-15-ip-whitelist-requirements.md`\n- Previous implementation plan:\n `docs/plans/2026-04-15-002-feat-ip-allowlist-plan.md`\n- API workflow guidance: `AGENTS.md`\n- OpenAPI source of truth: `docs/public/api-reference/fabro-api.yaml`\n",
|
|
"internal.fidelity": "compact",
|
|
"internal.retry_count.preflight_compile": 0,
|
|
"internal.work_dir": "/home/daytona/workspace/fabro",
|
|
"last_stage": "implement",
|
|
"thread.start.current_node": "toolchain",
|
|
"failure_class": "",
|
|
"internal.retry_count.implement": 0,
|
|
"outcome": "succeeded",
|
|
"failure_signature": "",
|
|
"internal.run_id": "01KSNYVQCKXPCSCEGWMZ1Q574Z"
|
|
},
|
|
"node_outcomes": {
|
|
"start": {
|
|
"status": "succeeded",
|
|
"usage": null
|
|
},
|
|
"preflight_compile": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
|
},
|
|
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 135614,
|
|
"active_time_ms": 135614
|
|
}
|
|
},
|
|
"preflight_lint": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
|
},
|
|
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 142983,
|
|
"active_time_ms": 142983
|
|
}
|
|
},
|
|
"implement": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"last_stage": "implement",
|
|
"last_response": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.int",
|
|
"response.implement": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.integrations.github.webhooks.ip_allowlist]`\n - `IpAllowEntry` and related settings/types.\n- Deleted server runtime enforcement:\n - Removed `ip_allowlist` middleware/module.\n - Removed `IpAllowlistConfig`, client-IP extraction, trusted proxy handling, and GitHub `/meta` hook range resolution.\n - Simplified router/startup wiring and removed `ConnectInfo` serving requirements that only existed for allowlisting.\n- Preserved unrelated allowlists:\n - GitHub username allowlists remain.\n - Sandbox/Daytona CIDR allow lists remain.\n- Updated OpenAPI and generated clients:\n - Removed allowlist schemas/fields from `fabro-api.yaml`.\n - Regenerated Rust API and TypeScript API client.\n - Deleted stale generated TS allowlist model files.\n- Updated web UI:\n - Removed Settings > Security network allowlist display.\n - Adjusted Security page/nav copy.\n- Updated tests/docs:\n - Added unknown-field tests proving stale IP allowlist config is now rejected.\n - Added settings API assertions that removed fields are absent.\n - Updated security docs and changelog to direct operators to upstream network controls.\n- Updated dependency graph:\n - Removed direct `ipnet` deps from `fabro-types` and `fabro-server`; retained transitive/use in `fabro-config` for sandbox CIDR validation.\n\nRed/green TDD:\n- Added stale-config rejection tests and verified they failed before schema removal, then passed after removal.\n\nValidation run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo build -p fabro-cli --bin fabro`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server --no-fail-fast`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `git diff --check`\n- Stale-symbol search confirmed remaining `ip_allowlist`/IP allowlist references are only intentional docs/tests/assertions.\n\nNo commits were created."
|
|
},
|
|
"notes": "Stage completed: implement",
|
|
"usage": {
|
|
"input": {
|
|
"usage": {
|
|
"model": {
|
|
"provider": "openai",
|
|
"model_id": "gpt-5.5"
|
|
},
|
|
"tokens": {
|
|
"input_tokens": 2556897,
|
|
"output_tokens": 26084,
|
|
"reasoning_tokens": 9280,
|
|
"cache_read_tokens": 17923072,
|
|
"cache_write_tokens": 0
|
|
}
|
|
},
|
|
"facts": {
|
|
"algorithm": "openai"
|
|
}
|
|
},
|
|
"total_usd_micros": 22806941
|
|
},
|
|
"files_touched": [
|
|
"/home/daytona/workspace/fabro/docs/public/changelog/2026-05-27.mdx"
|
|
],
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 1238818,
|
|
"tool_time_ms": 594726,
|
|
"active_time_ms": 1833544
|
|
}
|
|
},
|
|
"toolchain": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
|
},
|
|
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 1482,
|
|
"active_time_ms": 1482
|
|
}
|
|
}
|
|
},
|
|
"next_node_id": "simplify_opus",
|
|
"git_commit_sha": "43c460e20583a373165b452b88267b4bd2c93225",
|
|
"node_visits": {
|
|
"start": 1,
|
|
"implement": 1,
|
|
"toolchain": 1,
|
|
"preflight_compile": 1,
|
|
"preflight_lint": 1
|
|
}
|
|
},
|
|
"diff": {
|
|
"patch": "diff --git a/Cargo.lock b/Cargo.lock\nindex 0986b55a6..7365db53d 100644\n--- a/Cargo.lock\n+++ b/Cargo.lock\n@@ -2388,7 +2388,6 @@ dependencies = [\n \"hmac\",\n \"http-body-util\",\n \"httpmock\",\n- \"ipnet\",\n \"jsonwebtoken\",\n \"mime_guess\",\n \"multer\",\n@@ -2590,7 +2589,6 @@ dependencies = [\n \"fabro-model\",\n \"fabro-util\",\n \"hex\",\n- \"ipnet\",\n \"serde\",\n \"serde_json\",\n \"sha2\",\n@@ -3854,9 +3852,6 @@ name = \"ipnet\"\n version = \"2.11.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130\"\n-dependencies = [\n- \"serde\",\n-]\n \n [[package]]\n name = \"iri-string\"\ndiff --git a/apps/fabro-web/app/routes/settings-security.tsx b/apps/fabro-web/app/routes/settings-security.tsx\nindex 779f3f7a9..2075131c9 100644\n--- a/apps/fabro-web/app/routes/settings-security.tsx\n+++ b/apps/fabro-web/app/routes/settings-security.tsx\n@@ -2,7 +2,6 @@ import type { ServerSettings } from \"@qltysh/fabro-api-client\";\n import { useServerSettings } from \"../lib/queries\";\n import {\n Badge,\n- Count,\n Muted,\n Panel,\n PanelSkeleton,\n@@ -10,7 +9,6 @@ import {\n SettingsPageIntro,\n UsernameList,\n } from \"../components/settings-panel\";\n-import { plural } from \"../lib/plural\";\n \n export function meta() {\n return [{ title: \"Security — Fabro\" }];\n@@ -18,7 +16,7 @@ export function meta() {\n \n const DESCRIPTION = (\n <>\n- Authentication methods and network allowlist. Edit via{\" \"}\n+ Authentication methods and permitted GitHub usernames. Edit via{\" \"}\n <code className=\"font-mono text-fg-2\">settings.toml</code>; changes take\n effect on the next server restart.\n </>\n@@ -37,7 +35,7 @@ export default function SettingsSecurity() {\n }\n \n function SecurityPanel({ settings }: { settings: ServerSettings }) {\n- const { auth, ip_allowlist } = settings.server;\n+ const { auth } = settings.server;\n const githubUsers = auth.github.allowed_usernames;\n return (\n <Panel title=\"Security\">\n@@ -62,18 +60,6 @@ function SecurityPanel({ settings }: { settings: ServerSettings }) {\n <UsernameList names={githubUsers} />\n )}\n </Row>\n- <Row title=\"IP allowlist\" help=\"Network sources permitted to reach the API.\">\n- <Count\n- n={ip_allowlist.entries.length}\n- singular=\"entry\"\n- plural=\"entries\"\n- suffix={\n- ip_allowlist.trusted_proxy_count > 0\n- ? `· ${ip_allowlist.trusted_proxy_count} trusted ${plural(ip_allowlist.trusted_proxy_count, \"proxy\", \"proxies\")}`\n- : undefined\n- }\n- />\n- </Row>\n </Panel>\n );\n }\ndiff --git a/apps/fabro-web/app/routes/settings.tsx b/apps/fabro-web/app/routes/settings.tsx\nindex 67e750c28..648edbd24 100644\n--- a/apps/fabro-web/app/routes/settings.tsx\n+++ b/apps/fabro-web/app/routes/settings.tsx\n@@ -63,7 +63,7 @@ export const navSections: NavSection[] = [\n name: \"Security\",\n href: \"/settings/security\",\n icon: ShieldCheckIcon,\n- description: \"Authentication and network allowlist.\",\n+ description: \"Authentication methods and access.\",\n match: (p) => p.startsWith(\"/settings/security\"),\n },\n {\ndiff --git a/docs/public/administration/security.mdx b/docs/public/administration/security.mdx\nindex ec1a30bdd..27e1aeea9 100644\n--- a/docs/public/administration/security.mdx\n+++ b/docs/public/administration/security.mdx\n@@ -15,6 +15,7 @@ Fabro is single-tenant software designed for small, trusted teams. The following\n |---|---|\n | **Multi-tenancy** | Not supported. Fabro is single-tenant only — there is no organization or tenant isolation. |\n | **Roles or ACLs** | Not supported. All authenticated users have identical, full access to every run, workflow, and API endpoint. |\n+| **Inbound source-IP allowlisting** | Not implemented in Fabro. Restrict source networks with a reverse proxy, firewall, VPN, Tailscale, platform ingress policy, or another deployment-layer control. |\n | **Rate limiting** | Not implemented. The API server does not throttle requests. |\n | **Custom security header policy** | Not configurable. Fabro emits default security headers, including enforced `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, and `Strict-Transport-Security` when HTTPS is detected. |\n \n@@ -24,6 +25,7 @@ Fabro is single-tenant software designed for small, trusted teams. The following\n \n - **Deploy the web app on a private network.** Use a Tailscale tailnet, VPN, or internal network to keep the web UI off the public internet.\n - **Deploy the API on a private network.** Only allow access from expected hosts (the web app, CI runners, developer machines). The API binds to `127.0.0.1` by default — do not expose it with `--host 0.0.0.0` unless it is behind a firewall or private network.\n+- **Enforce source-IP restrictions upstream.** Fabro does not provide inbound source-IP allowlisting. Use your reverse proxy, firewall, VPN, Tailscale ACLs, cloud load balancer, Kubernetes ingress, or platform policy to limit who can connect.\n - **Use Daytona sandboxes with network controls.** When running untrusted or generated code, use the Daytona sandbox provider with `network = \"block\"` or a CIDR-based allow list to restrict agent egress.\n \n ### Authentication\ndiff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml\nindex aff64c205..103ab4cad 100644\n--- a/docs/public/api-reference/fabro-api.yaml\n+++ b/docs/public/api-reference/fabro-api.yaml\n@@ -12280,7 +12280,6 @@ components:\n - api\n - web\n - auth\n- - ip_allowlist\n - sandbox\n - storage\n - artifacts\n@@ -12297,8 +12296,6 @@ components:\n $ref: \"#/components/schemas/ServerWebSettings\"\n auth:\n $ref: \"#/components/schemas/ServerAuthSettings\"\n- ip_allowlist:\n- $ref: \"#/components/schemas/ServerIpAllowlistSettings\"\n sandbox:\n $ref: \"#/components/schemas/ServerSandboxSettings\"\n storage:\n@@ -12379,44 +12376,6 @@ components:\n items:\n type: string\n \n- ServerIpAllowlistSettings:\n- type: object\n- required: [entries, trusted_proxy_count]\n- properties:\n- entries:\n- type: array\n- items:\n- $ref: \"#/components/schemas/IpAllowEntry\"\n- trusted_proxy_count:\n- type: integer\n-\n- ServerIpAllowlistOverrideSettings:\n- type: object\n- required: [entries, trusted_proxy_count]\n- properties:\n- entries:\n- type: [\"array\", \"null\"]\n- items:\n- $ref: \"#/components/schemas/IpAllowEntry\"\n- trusted_proxy_count:\n- type: [\"integer\", \"null\"]\n-\n- IpAllowEntry:\n- oneOf:\n- - $ref: \"#/components/schemas/LiteralIpAllowEntry\"\n- - $ref: \"#/components/schemas/GitHubMetaHooksEntry\"\n-\n- LiteralIpAllowEntry:\n- type: object\n- required: [Literal]\n- properties:\n- Literal:\n- type: string\n-\n- GitHubMetaHooksEntry:\n- type: string\n- enum: [GitHubMetaHooks]\n-\n ServerSandboxSettings:\n type: object\n required: [providers]\n@@ -12571,16 +12530,12 @@ components:\n \n IntegrationWebhooksSettings:\n type: object\n- required: [strategy, ip_allowlist]\n+ required: [strategy]\n properties:\n strategy:\n oneOf:\n - $ref: \"#/components/schemas/WebhookStrategy\"\n - type: \"null\"\n- ip_allowlist:\n- oneOf:\n- - $ref: \"#/components/schemas/ServerIpAllowlistOverrideSettings\"\n- - type: \"null\"\n \n WebhookStrategy:\n type: string\ndiff --git a/docs/public/changelog/2026-04-19.mdx b/docs/public/changelog/2026-04-19.mdx\nindex 93b138b11..750482f49 100644\n--- a/docs/public/changelog/2026-04-19.mdx\n+++ b/docs/public/changelog/2026-04-19.mdx\n@@ -51,7 +51,7 @@ The install flow is also available in the embedded web app, so Docker and hosted\n - New `GET /api/v1/runs/{id}/files` endpoint returns paginated run file data and diff metadata\n - New `POST /api/v1/runs/{id}/archive` and `POST /api/v1/runs/{id}/unarchive` endpoints manage archived terminal runs\n - `GET /api/v1/runs` accepts `include_archived=true` for listing archived runs\n-- GitHub webhook handling now supports explicit source IP allowlisting with GitHub webhook range refresh\n+- GitHub webhook handling added explicit source IP allowlisting with GitHub webhook range refresh (removed on 2026-05-27)\n </Accordion>\n \n <Accordion title=\"CLI\">\ndiff --git a/docs/public/changelog/2026-05-27.mdx b/docs/public/changelog/2026-05-27.mdx\nnew file mode 100644\nindex 000000000..fb81d346a\n--- /dev/null\n+++ b/docs/public/changelog/2026-05-27.mdx\n@@ -0,0 +1,23 @@\n+---\n+title: \"Inbound IP allowlisting removed\"\n+date: \"2026-05-27\"\n+---\n+\n+## Inbound IP allowlisting removed\n+\n+Fabro no longer includes an in-process inbound source-IP allowlist for the web UI, API, static assets, or GitHub webhook routes. The former `[server.ip_allowlist]` setting and GitHub webhook overlay at `[server.integrations.github.webhooks.ip_allowlist]` have been removed from server configuration and the settings API.\n+\n+This is a hard removal: existing `settings.toml` files that still contain those keys now fail as unknown fields. Move any source-IP restrictions to deployment-layer controls such as a reverse proxy, firewall, VPN, Tailscale ACLs, Kubernetes ingress policy, cloud load balancer, or platform ingress.\n+\n+GitHub webhook HMAC signature verification is unchanged. GitHub username allowlists and sandbox egress CIDR allow lists are also unchanged.\n+\n+## More\n+\n+<Accordion title=\"Breaking changes\">\n+- Removed `server.ip_allowlist` and GitHub webhook `ip_allowlist` from `GET /api/v1/settings` responses and generated API clients\n+- Removed server config support for `[server.ip_allowlist]` and `[server.integrations.github.webhooks.ip_allowlist]`\n+</Accordion>\n+\n+<Accordion title=\"Security\">\n+- Fabro no longer performs runtime source-IP filtering; enforce inbound network restrictions upstream\n+</Accordion>\ndiff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs\nindex 185435b66..88d465128 100644\n--- a/lib/crates/fabro-api/build.rs\n+++ b/lib/crates/fabro-api/build.rs\n@@ -244,21 +244,6 @@ fn main() {\n \"fabro_types::settings::server::ServerAuthGithubSettings\",\n &[],\n ),\n- (\n- \"ServerIpAllowlistSettings\",\n- \"fabro_types::settings::server::ServerIpAllowlistSettings\",\n- &[],\n- ),\n- (\n- \"ServerIpAllowlistOverrideSettings\",\n- \"fabro_types::settings::server::ServerIpAllowlistOverrideSettings\",\n- &[],\n- ),\n- (\n- \"IpAllowEntry\",\n- \"fabro_types::settings::server::IpAllowEntry\",\n- &[],\n- ),\n (\n \"ServerSandboxSettings\",\n \"fabro_types::settings::server::ServerSandboxSettings\",\ndiff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs\nindex a3dcd6867..3837275f0 100644\n--- a/lib/crates/fabro-api/src/lib.rs\n+++ b/lib/crates/fabro-api/src/lib.rs\n@@ -26,9 +26,8 @@ pub mod types {\n pub use fabro_types::settings::ServerNamespace;\n pub use fabro_types::settings::server::{\n GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings,\n- IpAllowEntry, LogDestination, ObjectStoreSettings, ServerApiSettings,\n- ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings,\n- ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings,\n+ LogDestination, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings,\n+ ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings,\n ServerListenSettings, ServerLoggingSettings, ServerSandboxProviderSettings,\n ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings,\n ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,\ndiff --git a/lib/crates/fabro-api/tests/server_settings_round_trip.rs b/lib/crates/fabro-api/tests/server_settings_round_trip.rs\nindex a7421641b..d96002d8f 100644\n--- a/lib/crates/fabro-api/tests/server_settings_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/server_settings_round_trip.rs\n@@ -64,6 +64,9 @@ strategy = \"app\"\n app_id = \"12345\"\n client_id = \"Iv1.abcdef\"\n slug = \"fabro-dev\"\n+\n+[server.integrations.github.webhooks]\n+strategy = \"tailscale_funnel\"\n \"#,\n )\n .expect(\"settings should resolve\");\n@@ -85,6 +88,16 @@ slug = \"fabro-dev\"\n json[\"server\"][\"sandbox\"][\"providers\"][\"daytona\"][\"enabled\"],\n false\n );\n+ assert!(\n+ json[\"server\"].get(\"ip_allowlist\").is_none(),\n+ \"server settings API should not expose removed IP allowlist settings\"\n+ );\n+ assert!(\n+ json[\"server\"][\"integrations\"][\"github\"][\"webhooks\"]\n+ .get(\"ip_allowlist\")\n+ .is_none(),\n+ \"github webhook settings API should not expose removed IP allowlist settings\"\n+ );\n assert!(json.get(\"features\").is_none());\n \n let round_trip: ApiServerSettings =\ndiff --git a/lib/crates/fabro-cli/tests/it/support/auth_harness.rs b/lib/crates/fabro-cli/tests/it/support/auth_harness.rs\nindex e018ff988..cfaf06fd0 100644\n--- a/lib/crates/fabro-cli/tests/it/support/auth_harness.rs\n+++ b/lib/crates/fabro-cli/tests/it/support/auth_harness.rs\n@@ -21,7 +21,6 @@ use chrono::{Duration as ChronoDuration, Utc};\n use fabro_client::{AuthEntry, AuthStore, DevTokenEntry, ServerTarget};\n use fabro_config::{RunLayer, ServerSettingsBuilder};\n use fabro_server::auth::GithubEndpoints;\n-use fabro_server::ip_allowlist::IpAllowlistConfig;\n use fabro_server::jwt_auth::resolve_auth_mode_with_lookup;\n use fabro_server::server::{RouterOptions, build_router_with_options};\n use fabro_server::test_support::TestAppStateBuilder;\n@@ -92,21 +91,15 @@ impl RealAuthHarness {\n .vault_entries([(\"GITHUB_APP_CLIENT_SECRET\", github_client_secret.as_str())])\n .build();\n let github_base = github_base_url(&twin.base_url);\n- let router = build_router_with_options(\n- state,\n- &auth_mode,\n- Arc::new(IpAllowlistConfig::default()),\n- RouterOptions {\n- web_enabled: true,\n- github_endpoints: Some(Arc::new(GithubEndpoints::with_bases(\n- github_base.clone(),\n- github_base,\n- ))),\n- github_webhook_ip_allowlist: None,\n- static_asset_root: None,\n- watch_web: false,\n- },\n- );\n+ let router = build_router_with_options(state, &auth_mode, RouterOptions {\n+ web_enabled: true,\n+ github_endpoints: Some(Arc::new(GithubEndpoints::with_bases(\n+ github_base.clone(),\n+ github_base,\n+ ))),\n+ static_asset_root: None,\n+ watch_web: false,\n+ });\n \n let api_requests = ListenerRequestLog::default();\n let api_server = RunningHttpServer::start(api_listener, router, &api_requests);\ndiff --git a/lib/crates/fabro-config/src/layers/mod.rs b/lib/crates/fabro-config/src/layers/mod.rs\nindex 6c425c33d..b3aaac0ac 100644\n--- a/lib/crates/fabro-config/src/layers/mod.rs\n+++ b/lib/crates/fabro-config/src/layers/mod.rs\n@@ -41,10 +41,10 @@ pub use run::{\n pub use server::{\n GithubIntegrationLayer, IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer,\n ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer,\n- ServerIntegrationsLayer, ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerLayer,\n- ServerListenLayer, ServerLoggingLayer, ServerSandboxLayer, ServerSandboxProviderLayer,\n- ServerSandboxProvidersLayer, ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer,\n- ServerWebLayer, SlackIntegrationLayer,\n+ ServerIntegrationsLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,\n+ ServerSandboxLayer, ServerSandboxProviderLayer, ServerSandboxProvidersLayer,\n+ ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer,\n+ SlackIntegrationLayer,\n };\n pub use settings::SettingsLayer;\n pub use workflow::WorkflowLayer;\ndiff --git a/lib/crates/fabro-config/src/layers/server.rs b/lib/crates/fabro-config/src/layers/server.rs\nindex 62939a0e3..b68277d9e 100644\n--- a/lib/crates/fabro-config/src/layers/server.rs\n+++ b/lib/crates/fabro-config/src/layers/server.rs\n@@ -21,8 +21,6 @@ pub struct ServerLayer {\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub auth: Option<ServerAuthLayer>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub ip_allowlist: Option<ServerIpAllowlistLayer>,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub sandbox: Option<ServerSandboxLayer>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub storage: Option<ServerStorageLayer>,\n@@ -93,24 +91,6 @@ pub struct ServerAuthGithubLayer {\n pub allowed_usernames: Vec<String>,\n }\n \n-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]\n-#[serde(deny_unknown_fields)]\n-pub struct ServerIpAllowlistLayer {\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub entries: Option<Vec<String>>,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub trusted_proxy_count: Option<u32>,\n-}\n-\n-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]\n-#[serde(deny_unknown_fields)]\n-pub struct ServerIpAllowlistOverrideLayer {\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub entries: Option<Vec<String>>,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub trusted_proxy_count: Option<u32>,\n-}\n-\n /// `[server.sandbox]` — server-owned sandbox provider policy.\n #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]\n #[serde(deny_unknown_fields)]\n@@ -261,7 +241,5 @@ pub struct SlackIntegrationLayer {\n #[serde(deny_unknown_fields)]\n pub struct IntegrationWebhooksLayer {\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub strategy: Option<WebhookStrategy>,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub ip_allowlist: Option<ServerIpAllowlistOverrideLayer>,\n+ pub strategy: Option<WebhookStrategy>,\n }\ndiff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs\nindex 9d53800b7..ccabacd0f 100644\n--- a/lib/crates/fabro-config/src/lib.rs\n+++ b/lib/crates/fabro-config/src/lib.rs\n@@ -55,11 +55,10 @@ pub use layers::{\n RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer,\n RunMetaBranchLayer, RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer,\n RunRunBranchLayer, RunScmLayer, ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer,\n- ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerIpAllowlistLayer,\n- ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,\n- ServerSandboxLayer, ServerSandboxProviderLayer, ServerSandboxProvidersLayer,\n- ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer, SettingsLayer,\n- SlackIntegrationLayer, StickyMap, StringOrSplice, WorkflowLayer,\n+ ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerLayer,\n+ ServerListenLayer, ServerLoggingLayer, ServerSandboxLayer, ServerSandboxProviderLayer,\n+ ServerSandboxProvidersLayer, ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer,\n+ ServerWebLayer, SettingsLayer, SlackIntegrationLayer, StickyMap, StringOrSplice, WorkflowLayer,\n };\n pub use logging::{resolve_log_destination, resolve_log_destination_with_env};\n pub use parse::ParseError;\ndiff --git a/lib/crates/fabro-config/src/resolve/server.rs b/lib/crates/fabro-config/src/resolve/server.rs\nindex cfdf08c7a..9a2538295 100644\n--- a/lib/crates/fabro-config/src/resolve/server.rs\n+++ b/lib/crates/fabro-config/src/resolve/server.rs\n@@ -1,9 +1,8 @@\n use fabro_types::settings::InterpString;\n use fabro_types::settings::server::{\n GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings,\n- IpAllowEntry, ObjectStoreProvider, ObjectStoreSettings, ServerApiSettings,\n- ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings,\n- ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings,\n+ ObjectStoreProvider, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings,\n+ ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings,\n ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSandboxProviderSettings,\n ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings,\n ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,\n@@ -15,9 +14,9 @@ use super::{ResolveError, default_interp, parse_socket_addr, require_interp};\n use crate::user::default_storage_dir;\n use crate::{\n IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, ServerApiLayer,\n- ServerArtifactsLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerIpAllowlistLayer,\n- ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerSandboxLayer,\n- ServerSandboxProviderLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer,\n+ ServerArtifactsLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerLayer, ServerListenLayer,\n+ ServerSandboxLayer, ServerSandboxProviderLayer, ServerSlateDbLayer, ServerStorageLayer,\n+ ServerWebLayer,\n };\n \n pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> ServerNamespace {\n@@ -25,10 +24,7 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> Se\n let listen = resolve_listen(layer.listen.as_ref(), errors);\n let web = resolve_web(layer.web.as_ref());\n let auth = resolve_auth(layer.auth.as_ref(), errors);\n- let ip_allowlist = resolve_ip_allowlist(layer.ip_allowlist.as_ref(), errors);\n- let integrations = resolve_integrations(layer.integrations.as_ref(), errors);\n- validate_ip_allowlist_for_listen(&listen, &ip_allowlist, errors);\n- validate_github_webhook_ip_allowlist_for_listen(&listen, &ip_allowlist, &integrations, errors);\n+ let integrations = resolve_integrations(layer.integrations.as_ref());\n validate_github_webhook_strategy(&integrations, layer.api.as_ref(), errors);\n \n ServerNamespace {\n@@ -38,7 +34,6 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> Se\n },\n web,\n auth,\n- ip_allowlist,\n sandbox: resolve_sandbox(layer.sandbox.as_ref()),\n storage: storage.clone(),\n artifacts: resolve_artifacts(layer.artifacts.as_ref(), &storage.root, errors),\n@@ -176,166 +171,6 @@ fn resolve_auth(\n }\n }\n \n-fn resolve_ip_allowlist(\n- layer: Option<&ServerIpAllowlistLayer>,\n- errors: &mut Vec<ResolveError>,\n-) -> ServerIpAllowlistSettings {\n- let entries = layer\n- .and_then(|allowlist| allowlist.entries.as_ref())\n- .map(|entries| {\n- resolve_ip_allow_entries(entries, \"server.ip_allowlist.entries\", false, errors)\n- })\n- .unwrap_or_default();\n-\n- ServerIpAllowlistSettings {\n- entries,\n- trusted_proxy_count: layer\n- .and_then(|allowlist| allowlist.trusted_proxy_count)\n- .unwrap_or(0),\n- }\n-}\n-\n-fn effective_ip_allowlist_settings(\n- global: &ServerIpAllowlistSettings,\n- overlay: Option<&ServerIpAllowlistOverrideSettings>,\n-) -> ServerIpAllowlistSettings {\n- let Some(overlay) = overlay else {\n- return global.clone();\n- };\n-\n- ServerIpAllowlistSettings {\n- entries: overlay\n- .entries\n- .clone()\n- .unwrap_or_else(|| global.entries.clone()),\n- trusted_proxy_count: overlay\n- .trusted_proxy_count\n- .unwrap_or(global.trusted_proxy_count),\n- }\n-}\n-\n-fn resolve_ip_allowlist_override(\n- layer: Option<&ServerIpAllowlistOverrideLayer>,\n- path: &str,\n- allow_github_meta_hooks: bool,\n- errors: &mut Vec<ResolveError>,\n-) -> Option<ServerIpAllowlistOverrideSettings> {\n- layer.map(|allowlist| ServerIpAllowlistOverrideSettings {\n- entries: allowlist.entries.as_ref().map(|entries| {\n- resolve_ip_allow_entries(\n- entries,\n- &format!(\"{path}.entries\"),\n- allow_github_meta_hooks,\n- errors,\n- )\n- }),\n- trusted_proxy_count: allowlist.trusted_proxy_count,\n- })\n-}\n-\n-fn resolve_ip_allow_entries(\n- entries: &[String],\n- path: &str,\n- allow_github_meta_hooks: bool,\n- errors: &mut Vec<ResolveError>,\n-) -> Vec<IpAllowEntry> {\n- entries\n- .iter()\n- .enumerate()\n- .filter_map(|(index, entry)| {\n- resolve_ip_allow_entry(\n- entry,\n- &format!(\"{path}[{index}]\"),\n- allow_github_meta_hooks,\n- errors,\n- )\n- })\n- .collect()\n-}\n-\n-fn resolve_ip_allow_entry(\n- entry: &str,\n- path: &str,\n- allow_github_meta_hooks: bool,\n- errors: &mut Vec<ResolveError>,\n-) -> Option<IpAllowEntry> {\n- if entry == IpAllowEntry::GITHUB_META_HOOKS_KEYWORD {\n- if allow_github_meta_hooks {\n- return Some(IpAllowEntry::GitHubMetaHooks);\n- }\n-\n- errors.push(ResolveError::Invalid {\n- path: path.to_string(),\n- reason: format!(\n- \"`{}` is only valid in server.integrations.github.webhooks.ip_allowlist.entries\",\n- IpAllowEntry::GITHUB_META_HOOKS_KEYWORD\n- ),\n- });\n- return None;\n- }\n-\n- match IpAllowEntry::parse_literal(entry) {\n- Ok(parsed) => Some(parsed),\n- Err(reason) => {\n- errors.push(ResolveError::ParseFailure {\n- path: path.to_string(),\n- reason,\n- });\n- None\n- }\n- }\n-}\n-\n-fn validate_ip_allowlist_for_listen(\n- listen: &ServerListenSettings,\n- ip_allowlist: &ServerIpAllowlistSettings,\n- errors: &mut Vec<ResolveError>,\n-) {\n- if matches!(listen, ServerListenSettings::Unix { .. })\n- && !ip_allowlist.entries.is_empty()\n- && ip_allowlist.trusted_proxy_count == 0\n- {\n- errors.push(ResolveError::Invalid {\n- path: \"server.ip_allowlist.trusted_proxy_count\".to_string(),\n- reason: \"must be greater than 0 when using a Unix socket listener with a non-empty IP allowlist\".to_string(),\n- });\n- }\n-}\n-\n-fn validate_github_webhook_ip_allowlist_for_listen(\n- listen: &ServerListenSettings,\n- global_ip_allowlist: &ServerIpAllowlistSettings,\n- integrations: &ServerIntegrationsSettings,\n- errors: &mut Vec<ResolveError>,\n-) {\n- let Some(overlay) = integrations\n- .github\n- .webhooks\n- .as_ref()\n- .and_then(|webhooks| webhooks.ip_allowlist.as_ref())\n- else {\n- return;\n- };\n-\n- let effective = effective_ip_allowlist_settings(global_ip_allowlist, Some(overlay));\n- if effective == *global_ip_allowlist {\n- return;\n- }\n-\n- if matches!(listen, ServerListenSettings::Unix { .. })\n- && !effective.entries.is_empty()\n- && effective.trusted_proxy_count == 0\n- {\n- errors.push(ResolveError::Invalid {\n- path: \"server.integrations.github.webhooks.ip_allowlist.trusted_proxy_count\"\n- .to_string(),\n- reason:\n- \"must be greater than 0 when using a Unix socket listener with a non-empty GitHub webhook IP allowlist\"\n- .to_string(),\n- });\n- }\n-}\n-\n fn validate_github_webhook_strategy(\n integrations: &ServerIntegrationsSettings,\n api_layer: Option<&ServerApiLayer>,\n@@ -476,10 +311,7 @@ fn object_store_default_root(storage_root: &InterpString, domain: &str) -> Inter\n InterpString::parse(&format!(\"{root}/objects/{domain}\"))\n }\n \n-fn resolve_integrations(\n- layer: Option<&ServerIntegrationsLayer>,\n- errors: &mut Vec<ResolveError>,\n-) -> ServerIntegrationsSettings {\n+fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegrationsSettings {\n ServerIntegrationsSettings {\n github: layer\n .and_then(|integrations| integrations.github.as_ref())\n@@ -489,9 +321,7 @@ fn resolve_integrations(\n app_id: github.app_id.clone(),\n client_id: github.client_id.clone(),\n slug: github.slug.clone(),\n- webhooks: github.webhooks.as_ref().map(|webhooks| {\n- resolve_github_webhooks(webhooks, \"server.integrations.github.webhooks\", errors)\n- }),\n+ webhooks: github.webhooks.as_ref().map(resolve_github_webhooks),\n })\n .unwrap_or_default(),\n slack: layer\n@@ -504,18 +334,8 @@ fn resolve_integrations(\n }\n }\n \n-fn resolve_github_webhooks(\n- layer: &IntegrationWebhooksLayer,\n- path: &str,\n- errors: &mut Vec<ResolveError>,\n-) -> IntegrationWebhooksSettings {\n+fn resolve_github_webhooks(layer: &IntegrationWebhooksLayer) -> IntegrationWebhooksSettings {\n IntegrationWebhooksSettings {\n- strategy: layer.strategy,\n- ip_allowlist: resolve_ip_allowlist_override(\n- layer.ip_allowlist.as_ref(),\n- &format!(\"{path}.ip_allowlist\"),\n- true,\n- errors,\n- ),\n+ strategy: layer.strategy,\n }\n }\ndiff --git a/lib/crates/fabro-config/src/tests/resolve_server.rs b/lib/crates/fabro-config/src/tests/resolve_server.rs\nindex 5a8d38c35..be646e73c 100644\n--- a/lib/crates/fabro-config/src/tests/resolve_server.rs\n+++ b/lib/crates/fabro-config/src/tests/resolve_server.rs\n@@ -5,7 +5,7 @@\n \n use fabro_types::settings::InterpString;\n use fabro_types::settings::server::{\n- GithubIntegrationStrategy, IpAllowEntry, LogDestination, ObjectStoreSettings, ServerAuthMethod,\n+ GithubIntegrationStrategy, LogDestination, ObjectStoreSettings, ServerAuthMethod,\n ServerListenSettings, ServerNamespace,\n };\n use fabro_util::Home;\n@@ -427,92 +427,37 @@ disk_cache = true\n }\n \n #[test]\n-fn resolves_empty_ip_allowlist_by_default() {\n- let settings = resolve_server(&empty_settings_with_auth_methods());\n-\n- assert!(settings.ip_allowlist.entries.is_empty());\n- assert_eq!(settings.ip_allowlist.trusted_proxy_count, 0);\n-}\n-\n-#[test]\n-fn resolves_global_ip_allowlist_entries_and_proxy_count() {\n- let file = parse(\n- r#\"\n-_version = 1\n-\n-[server.ip_allowlist]\n-entries = [\"10.0.0.0/8\", \"2001:db8::/32\", \"192.0.2.42\"]\n-trusted_proxy_count = 2\n-\"#,\n- );\n-\n- let settings = resolve_server(&file);\n-\n- assert_eq!(settings.ip_allowlist.entries, vec![\n- IpAllowEntry::parse_literal(\"10.0.0.0/8\").unwrap(),\n- IpAllowEntry::parse_literal(\"2001:db8::/32\").unwrap(),\n- IpAllowEntry::parse_literal(\"192.0.2.42\").unwrap(),\n- ]);\n- assert_eq!(settings.ip_allowlist.trusted_proxy_count, 2);\n-}\n-\n-#[test]\n-fn resolves_github_webhook_ip_allowlist_overlay_with_inheritance() {\n- let file = parse(\n- r#\"\n+fn parsing_rejects_removed_server_ip_allowlist() {\n+ let err = r#\"\n _version = 1\n \n [server.ip_allowlist]\n entries = [\"10.0.0.0/8\"]\n-trusted_proxy_count = 2\n-\n-[server.integrations.github.webhooks.ip_allowlist]\n-entries = [\"github_meta_hooks\"]\n-\"#,\n- );\n-\n- let settings = resolve_server(&file);\n- let webhook_allowlist = settings\n- .integrations\n- .github\n- .webhooks\n- .expect(\"github webhooks settings should resolve\")\n- .ip_allowlist\n- .expect(\"github webhook ip allowlist overlay should resolve\");\n+\"#\n+ .parse::<SettingsLayer>()\n+ .expect_err(\"removed server IP allowlist setting should be rejected\");\n \n- assert_eq!(\n- webhook_allowlist.entries,\n- Some(vec![IpAllowEntry::GitHubMetaHooks])\n+ assert!(\n+ err.to_string().contains(\"ip_allowlist\"),\n+ \"unexpected error: {err}\"\n );\n- assert_eq!(webhook_allowlist.trusted_proxy_count, None);\n }\n \n #[test]\n-fn resolves_github_webhook_ip_allowlist_override_proxy_count() {\n- let file = parse(\n- r#\"\n+fn parsing_rejects_removed_github_webhook_ip_allowlist() {\n+ let err = r#\"\n _version = 1\n \n-[server.ip_allowlist]\n-entries = [\"10.0.0.0/8\"]\n-trusted_proxy_count = 2\n-\n [server.integrations.github.webhooks.ip_allowlist]\n-trusted_proxy_count = 3\n-\"#,\n- );\n+entries = [\"github_meta_hooks\"]\n+\"#\n+ .parse::<SettingsLayer>()\n+ .expect_err(\"removed github webhook IP allowlist setting should be rejected\");\n \n- let settings = resolve_server(&file);\n- let webhook_allowlist = settings\n- .integrations\n- .github\n- .webhooks\n- .expect(\"github webhooks settings should resolve\")\n- .ip_allowlist\n- .expect(\"github webhook ip allowlist overlay should resolve\");\n-\n- assert_eq!(webhook_allowlist.entries, None);\n- assert_eq!(webhook_allowlist.trusted_proxy_count, Some(3));\n+ assert!(\n+ err.to_string().contains(\"ip_allowlist\"),\n+ \"unexpected error: {err}\"\n+ );\n }\n \n #[test]\n@@ -558,91 +503,6 @@ strategy = \"tailscale_funnel\"\n assert!(rendered.contains(\"server.integrations.github.app_id\"));\n }\n \n-#[test]\n-fn rejects_invalid_ip_allowlist_entry() {\n- let file = parse(\n- r#\"\n-_version = 1\n-\n-[server.ip_allowlist]\n-entries = [\"10.0.0.0/33\"]\n-\"#,\n- );\n-\n- let rendered = render_resolve_error_lines(\n- ServerSettingsBuilder::from_layer(&file).expect_err(\"invalid CIDR should fail\"),\n- );\n-\n- assert!(rendered.contains(\"server.ip_allowlist.entries[0]\"));\n-}\n-\n-#[test]\n-fn rejects_github_meta_hooks_in_global_scope() {\n- let file = parse(\n- r#\"\n-_version = 1\n-\n-[server.ip_allowlist]\n-entries = [\"github_meta_hooks\"]\n-\"#,\n- );\n-\n- let rendered = render_resolve_error_lines(\n- ServerSettingsBuilder::from_layer(&file)\n- .expect_err(\"github_meta_hooks should be rejected outside github webhooks\"),\n- );\n-\n- assert!(rendered.contains(\"server.ip_allowlist.entries[0]\"));\n-}\n-\n-#[test]\n-fn rejects_unix_socket_allowlist_without_trusted_proxy() {\n- let file = parse(\n- r#\"\n-_version = 1\n-\n-[server.listen]\n-type = \"unix\"\n-path = \"/tmp/fabro.sock\"\n-\n-[server.ip_allowlist]\n-entries = [\"10.0.0.0/8\"]\n-\"#,\n- );\n-\n- let rendered = render_resolve_error_lines(\n- ServerSettingsBuilder::from_layer(&file)\n- .expect_err(\"unix allowlist without trusted proxies should fail\"),\n- );\n-\n- assert!(rendered.contains(\"server.ip_allowlist.trusted_proxy_count\"));\n-}\n-\n-#[test]\n-fn rejects_unix_socket_github_webhook_allowlist_without_trusted_proxy() {\n- let file = parse(\n- r#\"\n-_version = 1\n-\n-[server.listen]\n-type = \"unix\"\n-path = \"/tmp/fabro.sock\"\n-\n-[server.integrations.github.webhooks.ip_allowlist]\n-entries = [\"github_meta_hooks\"]\n-\"#,\n- );\n-\n- let rendered = render_resolve_error_lines(\n- ServerSettingsBuilder::from_layer(&file)\n- .expect_err(\"unix github webhook allowlist without trusted proxies should fail\"),\n- );\n-\n- assert!(\n- rendered.contains(\"server.integrations.github.webhooks.ip_allowlist.trusted_proxy_count\")\n- );\n-}\n-\n #[test]\n fn resolve_storage_root_defaults_with_minimal_server_auth_methods() {\n let settings = ServerSettingsBuilder::from_layer(&empty_settings_with_auth_methods())\ndiff --git a/lib/crates/fabro-server/Cargo.toml b/lib/crates/fabro-server/Cargo.toml\nindex 89611b971..c36eee94a 100644\n--- a/lib/crates/fabro-server/Cargo.toml\n+++ b/lib/crates/fabro-server/Cargo.toml\n@@ -92,7 +92,6 @@ semver.workspace = true\n walkdir.workspace = true\n multer = \"3\"\n thiserror.workspace = true\n-ipnet = \"2.11.0\"\n percent-encoding.workspace = true\n url = \"2\"\n zeroize.workspace = true\ndiff --git a/lib/crates/fabro-server/src/auth/translate.rs b/lib/crates/fabro-server/src/auth/translate.rs\nindex b1b8d8292..8d0f57cf5 100644\n--- a/lib/crates/fabro-server/src/auth/translate.rs\n+++ b/lib/crates/fabro-server/src/auth/translate.rs\n@@ -546,12 +546,7 @@ methods = [\"dev-token\"]\n #[tokio::test]\n async fn full_router_rejects_demo_cookie_without_credentials() {\n let state = test_state();\n- let app = server::build_router_with_options(\n- state,\n- &auth_mode(),\n- Arc::new(crate::ip_allowlist::IpAllowlistConfig::default()),\n- RouterOptions::default(),\n- );\n+ let app = server::build_router_with_options(state, &auth_mode(), RouterOptions::default());\n \n let response = app\n .oneshot(\n@@ -575,7 +570,6 @@ methods = [\"dev-token\"]\n let app = server::build_router_with_options(\n Arc::clone(&state),\n &auth_mode(),\n- Arc::new(crate::ip_allowlist::IpAllowlistConfig::default()),\n RouterOptions::default(),\n );\n \n@@ -601,12 +595,7 @@ methods = [\"dev-token\"]\n #[tokio::test]\n async fn full_router_does_not_demo_dispatch_auth_routes() {\n let state = test_state();\n- let app = server::build_router_with_options(\n- state,\n- &auth_mode(),\n- Arc::new(crate::ip_allowlist::IpAllowlistConfig::default()),\n- RouterOptions::default(),\n- );\n+ let app = server::build_router_with_options(state, &auth_mode(), RouterOptions::default());\n \n let response = app\n .oneshot(\n@@ -630,18 +619,12 @@ methods = [\"dev-token\"]\n #[tokio::test]\n async fn full_router_accepts_dev_token_bearer_when_web_is_disabled() {\n let state = test_state();\n- let app = server::build_router_with_options(\n- state,\n- &auth_mode(),\n- Arc::new(crate::ip_allowlist::IpAllowlistConfig::default()),\n- RouterOptions {\n- web_enabled: false,\n- github_endpoints: None,\n- github_webhook_ip_allowlist: None,\n- static_asset_root: None,\n- watch_web: false,\n- },\n- );\n+ let app = server::build_router_with_options(state, &auth_mode(), RouterOptions {\n+ web_enabled: false,\n+ github_endpoints: None,\n+ static_asset_root: None,\n+ watch_web: false,\n+ });\n \n let response = app\n .oneshot(\ndiff --git a/lib/crates/fabro-server/src/ip_allowlist.rs b/lib/crates/fabro-server/src/ip_allowlist.rs\ndeleted file mode 100644\nindex 2fe3fc6ec..000000000\n--- a/lib/crates/fabro-server/src/ip_allowlist.rs\n+++ /dev/null\n@@ -1,609 +0,0 @@\n-use std::net::{IpAddr, SocketAddr};\n-use std::path::{Path, PathBuf};\n-use std::sync::Arc;\n-\n-use anyhow::{Context, Result, anyhow};\n-use axum::extract::{ConnectInfo, Request, State};\n-use axum::http::Request as HttpRequest;\n-use axum::middleware::Next;\n-use axum::response::{IntoResponse, Response};\n-use fabro_types::settings::server::{\n- IpAllowEntry, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings,\n-};\n-use ipnet::IpNet;\n-use serde::{Deserialize, Serialize};\n-use tokio::fs;\n-use tracing::warn;\n-\n-use crate::ApiError;\n-\n-const GITHUB_META_URL: &str = \"https://api.github.com/meta\";\n-\n-#[derive(Clone, Debug, Default, PartialEq, Eq)]\n-pub struct IpAllowlist {\n- entries: Vec<IpNet>,\n-}\n-\n-impl IpAllowlist {\n- pub fn new(entries: Vec<IpNet>) -> Self {\n- Self { entries }\n- }\n-\n- pub fn is_empty(&self) -> bool {\n- self.entries.is_empty()\n- }\n-\n- pub fn contains(&self, ip: &IpAddr) -> bool {\n- let ip = normalize_ip(*ip);\n- self.entries.iter().any(|entry| entry.contains(&ip))\n- }\n-}\n-\n-#[derive(Clone, Debug, Default, PartialEq, Eq)]\n-pub struct IpAllowlistConfig {\n- pub allowlist: IpAllowlist,\n- pub trusted_proxy_count: u32,\n-}\n-\n-#[derive(Clone)]\n-pub struct GitHubMetaResolver {\n- client: HttpClient,\n- meta_url: String,\n- cache_path: PathBuf,\n-}\n-\n-impl GitHubMetaResolver {\n- pub fn new(client: HttpClient, meta_url: String, cache_path: PathBuf) -> Self {\n- Self {\n- client,\n- meta_url,\n- cache_path,\n- }\n- }\n-\n- pub fn from_cache_dir(cache_dir: &Path) -> Result<Self> {\n- Ok(Self::new(\n- fabro_http::http_client().context(\"building GitHub meta HTTP client\")?,\n- GITHUB_META_URL.to_string(),\n- github_meta_cache_path(cache_dir),\n- ))\n- }\n-\n- async fn resolve_hooks(&self) -> Result<Vec<IpNet>> {\n- let cached = self.load_cache().await?;\n- let mut request = self\n- .client\n- .get(&self.meta_url)\n- .header(\"Accept\", \"application/vnd.github+json\")\n- .header(\"User-Agent\", \"fabro\");\n-\n- if let Some(etag) = cached.as_ref().and_then(|cache| cache.etag.as_deref()) {\n- request = request.header(\"If-None-Match\", etag);\n- }\n-\n- let response = match request.send().await {\n- Ok(response) => response,\n- Err(error) => {\n- return self.cached_hooks_or_error(\n- cached.as_ref(),\n- anyhow::Error::new(error).context(\"fetching GitHub /meta\"),\n- );\n- }\n- };\n- if response.status() == fabro_http::StatusCode::NOT_MODIFIED {\n- let cached = cached.ok_or_else(|| {\n- anyhow!(\"GitHub /meta returned 304 Not Modified but no usable cache was present\")\n- })?;\n- return parse_ip_nets(&cached.hooks);\n- }\n-\n- if !response.status().is_success() {\n- return self.cached_hooks_or_error(\n- cached.as_ref(),\n- anyhow!(\"GitHub /meta returned {}\", response.status()),\n- );\n- }\n-\n- let etag = response\n- .headers()\n- .get(\"etag\")\n- .and_then(|value| value.to_str().ok())\n- .map(ToOwned::to_owned);\n- let payload: GitHubMetaResponse = match response.json().await {\n- Ok(payload) => payload,\n- Err(error) => {\n- return self.cached_hooks_or_error(\n- cached.as_ref(),\n- anyhow::Error::new(error).context(\"parsing GitHub /meta response\"),\n- );\n- }\n- };\n- let hooks = match parse_ip_nets(&payload.hooks) {\n- Ok(hooks) => hooks,\n- Err(error) => return self.cached_hooks_or_error(cached.as_ref(), error),\n- };\n- self.store_cache(&GitHubMetaCache {\n- etag,\n- hooks: payload.hooks,\n- })\n- .await?;\n- Ok(hooks)\n- }\n-\n- async fn load_cache(&self) -> Result<Option<GitHubMetaCache>> {\n- match fs::read(&self.cache_path).await {\n- Ok(contents) => match serde_json::from_slice(&contents) {\n- Ok(cache) => Ok(Some(cache)),\n- Err(error) => {\n- warn!(\n- path = %self.cache_path.display(),\n- error = %error,\n- \"Ignoring invalid GitHub meta cache\"\n- );\n- Ok(None)\n- }\n- },\n- Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),\n- Err(error) => {\n- Err(error).with_context(|| format!(\"reading {}\", self.cache_path.display()))\n- }\n- }\n- }\n-\n- async fn store_cache(&self, cache: &GitHubMetaCache) -> Result<()> {\n- if let Some(parent) = self.cache_path.parent() {\n- fs::create_dir_all(parent)\n- .await\n- .with_context(|| format!(\"creating {}\", parent.display()))?;\n- }\n-\n- let contents = serde_json::to_vec(cache).context(\"serializing GitHub meta cache\")?;\n- fs::write(&self.cache_path, contents)\n- .await\n- .with_context(|| format!(\"writing {}\", self.cache_path.display()))?;\n- Ok(())\n- }\n-\n- fn cached_hooks_or_error(\n- &self,\n- cached: Option<&GitHubMetaCache>,\n- error: anyhow::Error,\n- ) -> Result<Vec<IpNet>> {\n- let Some(cached) = cached else {\n- return Err(error);\n- };\n-\n- warn!(\n- path = %self.cache_path.display(),\n- error = %error,\n- \"Using cached GitHub meta hooks after refresh failed\"\n- );\n- parse_ip_nets(&cached.hooks)\n- }\n-}\n-\n-type HttpClient = fabro_http::HttpClient;\n-\n-#[derive(Debug, Deserialize)]\n-struct GitHubMetaResponse {\n- hooks: Vec<String>,\n-}\n-\n-#[derive(Debug, Serialize, Deserialize)]\n-struct GitHubMetaCache {\n- etag: Option<String>,\n- hooks: Vec<String>,\n-}\n-\n-pub fn effective_ip_allowlist_settings(\n- global: &ServerIpAllowlistSettings,\n- overlay: Option<&ServerIpAllowlistOverrideSettings>,\n-) -> ServerIpAllowlistSettings {\n- let Some(overlay) = overlay else {\n- return global.clone();\n- };\n-\n- ServerIpAllowlistSettings {\n- entries: overlay\n- .entries\n- .clone()\n- .unwrap_or_else(|| global.entries.clone()),\n- trusted_proxy_count: overlay\n- .trusted_proxy_count\n- .unwrap_or(global.trusted_proxy_count),\n- }\n-}\n-\n-pub async fn resolve_ip_allowlist_config(\n- global: &ServerIpAllowlistSettings,\n- overlay: Option<&ServerIpAllowlistOverrideSettings>,\n- github_meta_resolver: &GitHubMetaResolver,\n-) -> Result<IpAllowlistConfig> {\n- let effective = effective_ip_allowlist_settings(global, overlay);\n- let allowlist = expand_ip_allow_entries(&effective.entries, github_meta_resolver).await?;\n-\n- Ok(IpAllowlistConfig {\n- allowlist: IpAllowlist::new(allowlist),\n- trusted_proxy_count: effective.trusted_proxy_count,\n- })\n-}\n-\n-pub fn extract_client_ip<B>(request: &HttpRequest<B>, trusted_proxy_count: u32) -> Option<IpAddr> {\n- if trusted_proxy_count == 0 {\n- return request\n- .extensions()\n- .get::<ConnectInfo<SocketAddr>>()\n- .map(|connect_info| normalize_ip(connect_info.0.ip()));\n- }\n-\n- let header = request.headers().get(\"x-forwarded-for\")?.to_str().ok()?;\n- let entries = header\n- .split(',')\n- .map(str::trim)\n- .filter(|entry| !entry.is_empty())\n- .collect::<Vec<_>>();\n- let trusted_proxy_count = trusted_proxy_count as usize;\n- let client_index = entries.len().checked_sub(trusted_proxy_count + 1)?;\n-\n- entries[client_index]\n- .parse::<IpAddr>()\n- .ok()\n- .map(normalize_ip)\n-}\n-\n-pub async fn ip_allowlist_middleware(\n- State(config): State<Arc<IpAllowlistConfig>>,\n- request: Request,\n- next: Next,\n-) -> Response {\n- if config.allowlist.is_empty() || request.uri().path() == \"/health\" {\n- return next.run(request).await;\n- }\n-\n- let path = request.uri().path().to_string();\n- match extract_client_ip(&request, config.trusted_proxy_count) {\n- Some(client_ip) if config.allowlist.contains(&client_ip) => next.run(request).await,\n- Some(client_ip) => {\n- warn!(client_ip = %client_ip, path = %path, \"request rejected: IP not in allowlist\");\n- ApiError::forbidden().into_response()\n- }\n- None => {\n- warn!(path = %path, \"request rejected: IP not in allowlist\");\n- ApiError::forbidden().into_response()\n- }\n- }\n-}\n-\n-async fn expand_ip_allow_entries(\n- entries: &[IpAllowEntry],\n- github_meta_resolver: &GitHubMetaResolver,\n-) -> Result<Vec<IpNet>> {\n- let github_hooks = if entries\n- .iter()\n- .any(|entry| matches!(entry, IpAllowEntry::GitHubMetaHooks))\n- {\n- github_meta_resolver.resolve_hooks().await?\n- } else {\n- Vec::new()\n- };\n-\n- let mut expanded = Vec::new();\n- for entry in entries {\n- match entry {\n- IpAllowEntry::Literal(net) => expanded.push(*net),\n- IpAllowEntry::GitHubMetaHooks => expanded.extend(github_hooks.iter().copied()),\n- }\n- }\n-\n- Ok(expanded)\n-}\n-\n-fn parse_ip_nets(values: &[String]) -> Result<Vec<IpNet>> {\n- values\n- .iter()\n- .map(|value| {\n- value\n- .parse::<IpNet>()\n- .with_context(|| format!(\"invalid IP range `{value}` in GitHub /meta hooks\"))\n- })\n- .collect()\n-}\n-\n-fn normalize_ip(ip: IpAddr) -> IpAddr {\n- match ip {\n- IpAddr::V4(_) => ip,\n- IpAddr::V6(address) => address\n- .to_ipv4_mapped()\n- .map_or(IpAddr::V6(address), IpAddr::V4),\n- }\n-}\n-\n-pub fn github_meta_cache_path(cache_dir: &Path) -> PathBuf {\n- cache_dir.join(\"github-meta-hooks.json\")\n-}\n-\n-#[cfg(test)]\n-#[expect(\n- clippy::disallowed_methods,\n- reason = \"tests stage IP allowlist fixtures with sync std::fs::write\"\n-)]\n-mod tests {\n- use std::net::Ipv4Addr;\n-\n- use axum::body::Body;\n- use axum::http::{Request, StatusCode};\n- use axum::routing::get;\n- use axum::{Router, middleware};\n- use httpmock::MockServer;\n- use tower::ServiceExt;\n-\n- use super::*;\n-\n- fn literal(value: &str) -> IpAllowEntry {\n- IpAllowEntry::parse_literal(value).unwrap()\n- }\n-\n- macro_rules! assert_status {\n- ($response:expr, $expected:expr) => {\n- fabro_test::assert_axum_status($response, $expected, concat!(file!(), \":\", line!()))\n- };\n- }\n-\n- #[test]\n- fn effective_scope_inherits_global_fields_and_prefers_override_values() {\n- let global = ServerIpAllowlistSettings {\n- entries: vec![literal(\"10.0.0.0/8\")],\n- trusted_proxy_count: 1,\n- };\n- let overlay = ServerIpAllowlistOverrideSettings {\n- entries: Some(vec![IpAllowEntry::GitHubMetaHooks]),\n- trusted_proxy_count: None,\n- };\n-\n- let effective = effective_ip_allowlist_settings(&global, Some(&overlay));\n-\n- assert_eq!(effective.entries, vec![IpAllowEntry::GitHubMetaHooks]);\n- assert_eq!(effective.trusted_proxy_count, 1);\n- }\n-\n- #[test]\n- fn extract_client_ip_uses_connect_info_without_trusted_proxies() {\n- let request = Request::builder()\n- .uri(\"/api/v1/runs\")\n- .body(Body::empty())\n- .unwrap();\n- let mut request = request;\n- request\n- .extensions_mut()\n- .insert(ConnectInfo(SocketAddr::from((\n- Ipv4Addr::new(192, 0, 2, 42),\n- 8080,\n- ))));\n-\n- assert_eq!(\n- extract_client_ip(&request, 0),\n- Some(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 42)))\n- );\n- }\n-\n- #[test]\n- fn extract_client_ip_uses_rightmost_minus_trusted_proxy_count_from_x_forwarded_for() {\n- let request = Request::builder()\n- .uri(\"/api/v1/runs\")\n- .header(\n- \"x-forwarded-for\",\n- \"198.51.100.10, 203.0.113.20, 203.0.113.30\",\n- )\n- .body(Body::empty())\n- .unwrap();\n-\n- assert_eq!(\n- extract_client_ip(&request, 2),\n- Some(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 10)))\n- );\n- }\n-\n- #[test]\n- fn extract_client_ip_fails_closed_when_x_forwarded_for_chain_is_too_short() {\n- let request = Request::builder()\n- .uri(\"/api/v1/runs\")\n- .header(\"x-forwarded-for\", \"198.51.100.10\")\n- .body(Body::empty())\n- .unwrap();\n-\n- assert_eq!(extract_client_ip(&request, 1), None);\n- }\n-\n- #[test]\n- fn ip_allowlist_matches_ipv4_mapped_ipv6_addresses_against_ipv4_ranges() {\n- let allowlist = IpAllowlist::new(vec![\"10.0.0.0/8\".parse().unwrap()]);\n-\n- assert!(allowlist.contains(&\"::ffff:10.1.2.3\".parse().unwrap()));\n- }\n-\n- #[test]\n- fn github_meta_resolver_uses_storage_cache_dir() {\n- let cache_dir = tempfile::tempdir().unwrap();\n- let resolver = GitHubMetaResolver::from_cache_dir(cache_dir.path()).unwrap();\n-\n- assert_eq!(\n- resolver.cache_path,\n- cache_dir.path().join(\"github-meta-hooks.json\")\n- );\n- }\n-\n- #[tokio::test]\n- async fn resolve_ip_allowlist_config_expands_github_meta_hooks() {\n- let mock_server = MockServer::start_async().await;\n- mock_server\n- .mock_async(|when, then| {\n- when.method(\"GET\").path(\"/meta\");\n- then.status(200)\n- .header(\"content-type\", \"application/json\")\n- .header(\"etag\", \"\\\"meta-v1\\\"\")\n- .body(r#\"{\"hooks\":[\"192.30.252.0/22\",\"185.199.108.0/22\"]}\"#);\n- })\n- .await;\n-\n- let resolver = GitHubMetaResolver::new(\n- fabro_http::test_http_client().unwrap(),\n- format!(\"{}/meta\", mock_server.url(\"\")),\n- tempfile::tempdir().unwrap().path().join(\"github-meta.json\"),\n- );\n- let global = ServerIpAllowlistSettings {\n- entries: vec![IpAllowEntry::GitHubMetaHooks],\n- trusted_proxy_count: 1,\n- };\n-\n- let config = resolve_ip_allowlist_config(&global, None, &resolver)\n- .await\n- .unwrap();\n-\n- assert!(config.allowlist.contains(&\"192.30.252.45\".parse().unwrap()));\n- assert!(config.allowlist.contains(&\"185.199.109.1\".parse().unwrap()));\n- assert_eq!(config.trusted_proxy_count, 1);\n- }\n-\n- #[tokio::test]\n- async fn resolve_ip_allowlist_config_reuses_cached_github_meta_on_not_modified() {\n- let mock_server = MockServer::start_async().await;\n- mock_server\n- .mock_async(|when, then| {\n- when.method(\"GET\")\n- .path(\"/meta\")\n- .header(\"if-none-match\", \"\\\"meta-v1\\\"\");\n- then.status(304);\n- })\n- .await;\n-\n- let cache_dir = tempfile::tempdir().unwrap();\n- std::fs::write(\n- cache_dir.path().join(\"github-meta.json\"),\n- r#\"{\"etag\":\"\\\"meta-v1\\\"\",\"hooks\":[\"192.30.252.0/22\"]}\"#,\n- )\n- .unwrap();\n-\n- let resolver = GitHubMetaResolver::new(\n- fabro_http::test_http_client().unwrap(),\n- format!(\"{}/meta\", mock_server.url(\"\")),\n- cache_dir.path().join(\"github-meta.json\"),\n- );\n- let global = ServerIpAllowlistSettings {\n- entries: vec![IpAllowEntry::GitHubMetaHooks],\n- trusted_proxy_count: 0,\n- };\n-\n- let config = resolve_ip_allowlist_config(&global, None, &resolver)\n- .await\n- .unwrap();\n-\n- assert!(config.allowlist.contains(&\"192.30.252.42\".parse().unwrap()));\n- }\n-\n- #[tokio::test]\n- async fn resolve_ip_allowlist_config_uses_cached_github_meta_when_github_is_unavailable() {\n- let mock_server = MockServer::start_async().await;\n- mock_server\n- .mock_async(|when, then| {\n- when.method(\"GET\").path(\"/meta\");\n- then.status(503);\n- })\n- .await;\n-\n- let cache_dir = tempfile::tempdir().unwrap();\n- std::fs::write(\n- cache_dir.path().join(\"github-meta.json\"),\n- r#\"{\"etag\":\"\\\"meta-v1\\\"\",\"hooks\":[\"192.30.252.0/22\"]}\"#,\n- )\n- .unwrap();\n-\n- let resolver = GitHubMetaResolver::new(\n- fabro_http::test_http_client().unwrap(),\n- format!(\"{}/meta\", mock_server.url(\"\")),\n- cache_dir.path().join(\"github-meta.json\"),\n- );\n- let global = ServerIpAllowlistSettings {\n- entries: vec![IpAllowEntry::GitHubMetaHooks],\n- trusted_proxy_count: 0,\n- };\n-\n- let config = resolve_ip_allowlist_config(&global, None, &resolver)\n- .await\n- .expect(\"cached GitHub meta hooks should be reused\");\n-\n- assert!(config.allowlist.contains(&\"192.30.252.42\".parse().unwrap()));\n- }\n-\n- #[tokio::test]\n- async fn middleware_reads_client_ip_from_x_forwarded_for_when_trusted_proxy_count_is_set() {\n- let config = Arc::new(IpAllowlistConfig {\n- allowlist: IpAllowlist::new(vec![\"10.0.0.0/8\".parse().unwrap()]),\n- trusted_proxy_count: 1,\n- });\n- let app = Router::new()\n- .route(\"/api/v1/runs\", get(|| async { StatusCode::OK }))\n- .layer(middleware::from_fn_with_state(\n- Arc::clone(&config),\n- ip_allowlist_middleware,\n- ));\n-\n- let allowed_request = Request::builder()\n- .uri(\"/api/v1/runs\")\n- .header(\"x-forwarded-for\", \"10.0.0.1, 198.51.100.1\")\n- .body(Body::empty())\n- .unwrap();\n- let allowed_response = app.clone().oneshot(allowed_request).await.unwrap();\n- assert_status!(allowed_response, StatusCode::OK).await;\n-\n- let blocked_request = Request::builder()\n- .uri(\"/api/v1/runs\")\n- .header(\"x-forwarded-for\", \"203.0.113.1, 198.51.100.1\")\n- .body(Body::empty())\n- .unwrap();\n- let blocked_response = app.oneshot(blocked_request).await.unwrap();\n- assert_status!(blocked_response, StatusCode::FORBIDDEN).await;\n- }\n-\n- #[tokio::test]\n- async fn middleware_allows_health_and_blocks_non_allowlisted_requests() {\n- let config = Arc::new(IpAllowlistConfig {\n- allowlist: IpAllowlist::new(vec![\"10.0.0.0/8\".parse().unwrap()]),\n- trusted_proxy_count: 0,\n- });\n- let app = Router::new()\n- .route(\"/health\", get(|| async { StatusCode::OK }))\n- .route(\"/api/v1/runs\", get(|| async { StatusCode::OK }))\n- .layer(middleware::from_fn_with_state(\n- Arc::clone(&config),\n- ip_allowlist_middleware,\n- ));\n-\n- let health_response = app\n- .clone()\n- .oneshot(request_with_connect_info(\n- \"/health\",\n- IpAddr::V4(Ipv4Addr::new(203, 0, 113, 10)),\n- ))\n- .await\n- .unwrap();\n- assert_status!(health_response, StatusCode::OK).await;\n-\n- let blocked_response = app\n- .oneshot(request_with_connect_info(\n- \"/api/v1/runs\",\n- IpAddr::V4(Ipv4Addr::new(203, 0, 113, 10)),\n- ))\n- .await\n- .unwrap();\n- assert_status!(blocked_response, StatusCode::FORBIDDEN).await;\n- }\n-\n- fn request_with_connect_info(path: &str, ip: IpAddr) -> Request<Body> {\n- let request = Request::builder().uri(path).body(Body::empty()).unwrap();\n- let mut request = request;\n- request\n- .extensions_mut()\n- .insert(ConnectInfo(SocketAddr::new(ip, 8080)));\n- request\n- }\n-}\ndiff --git a/lib/crates/fabro-server/src/lib.rs b/lib/crates/fabro-server/src/lib.rs\nindex d6b5ffeb4..443300c09 100644\n--- a/lib/crates/fabro-server/src/lib.rs\n+++ b/lib/crates/fabro-server/src/lib.rs\n@@ -22,7 +22,6 @@ pub mod diagnostics;\n pub mod error;\n pub mod github_webhooks;\n pub mod install;\n-pub mod ip_allowlist;\n pub mod jwt_auth;\n pub mod manifest_validation;\n mod migrations;\ndiff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs\nindex b2ff5c7ce..f4a3c5089 100644\n--- a/lib/crates/fabro-server/src/serve.rs\n+++ b/lib/crates/fabro-server/src/serve.rs\n@@ -1,5 +1,4 @@\n use std::future::{Future, IntoFuture};\n-use std::net::SocketAddr;\n use std::path::{Path, PathBuf};\n use std::sync::{Arc, RwLock};\n use std::time::Duration;\n@@ -34,7 +33,6 @@ use tracing::{error, info, warn};\n \n use crate::canonical_origin::resolve_canonical_origin;\n use crate::github_webhooks::{TailscaleFunnelManager, WEBHOOK_ROUTE, WEBHOOK_SECRET_ENV};\n-use crate::ip_allowlist::{GitHubMetaResolver, IpAllowlistConfig, resolve_ip_allowlist_config};\n use crate::server::{\n AppState, AppStateConfig, ResolvedAppStateSettings, RouterOptions, build_app_state,\n build_router_with_options, reconcile_incomplete_runs_on_startup, shutdown_active_workers,\n@@ -251,40 +249,6 @@ fn serve_overrides(args: &ServeArgs) -> (Option<RunLayer>, Option<ServerLayer>)\n )\n }\n \n-async fn resolve_github_webhook_ip_allowlist(\n- resolved_server_settings: &ServerNamespace,\n- github_meta_resolver: &GitHubMetaResolver,\n-) -> anyhow::Result<Arc<IpAllowlistConfig>> {\n- let config = resolve_ip_allowlist_config(\n- &resolved_server_settings.ip_allowlist,\n- resolved_server_settings\n- .integrations\n- .github\n- .webhooks\n- .as_ref()\n- .and_then(|webhooks| webhooks.ip_allowlist.as_ref()),\n- github_meta_resolver,\n- )\n- .await\n- .context(\"resolving GitHub webhook IP allowlist\")?;\n-\n- Ok(Arc::new(config))\n-}\n-\n-async fn resolve_startup_github_webhook_ip_allowlist(\n- resolved_server_settings: &ServerNamespace,\n- github_meta_resolver: &GitHubMetaResolver,\n- webhook_secret_present: bool,\n-) -> anyhow::Result<Option<Arc<IpAllowlistConfig>>> {\n- if !webhook_secret_present {\n- return Ok(None);\n- }\n-\n- resolve_github_webhook_ip_allowlist(resolved_server_settings, github_meta_resolver)\n- .await\n- .map(Some)\n-}\n-\n enum WebhookPreconditions {\n Ready {\n app_id: String,\n@@ -769,8 +733,6 @@ where\n } else {\n false\n };\n- let github_meta_resolver = GitHubMetaResolver::from_cache_dir(&storage.cache_dir())?;\n-\n let (object_store, slatedb_prefix, flush_interval, disk_cache) =\n build_slatedb_store_with_server_secrets(&resolved_server_settings, &server_secrets)?;\n let cache_path = if disk_cache {\n@@ -823,33 +785,12 @@ where\n );\n }\n spawn_scheduler(Arc::clone(&state));\n- let default_ip_allowlist = Arc::new(\n- resolve_ip_allowlist_config(\n- &resolved_server_settings.ip_allowlist,\n- None,\n- &github_meta_resolver,\n- )\n- .await\n- .context(\"resolving server IP allowlist\")?,\n- );\n- let github_webhook_ip_allowlist = resolve_startup_github_webhook_ip_allowlist(\n- &resolved_server_settings,\n- &github_meta_resolver,\n- webhook_secret_present,\n- )\n- .await?;\n- let router = build_router_with_options(\n- Arc::clone(&state),\n- &auth_mode,\n- Arc::clone(&default_ip_allowlist),\n- RouterOptions {\n- web_enabled,\n- github_webhook_ip_allowlist,\n- #[cfg(debug_assertions)]\n- watch_web,\n- ..RouterOptions::default()\n- },\n- );\n+ let router = build_router_with_options(Arc::clone(&state), &auth_mode, RouterOptions {\n+ web_enabled,\n+ #[cfg(debug_assertions)]\n+ watch_web,\n+ ..RouterOptions::default()\n+ });\n let bound_listener = bind_listener(&bind_request).await?;\n let bind_addr = bound_listener.bind.clone();\n \n@@ -993,11 +934,7 @@ where\n BoundListener::Tcp(listener) => {\n announce_server_ready(&bind_addr, styles);\n serve_until_shutdown(\n- axum::serve(\n- listener,\n- router.into_make_service_with_connect_info::<SocketAddr>(),\n- )\n- .with_graceful_shutdown({\n+ axum::serve(listener, router).with_graceful_shutdown({\n let token = shutdown.clone();\n async move { token.cancelled().await }\n }),\n@@ -1247,13 +1184,12 @@ mod tests {\n use tokio_util::sync::CancellationToken;\n \n use super::{\n- GitHubMetaResolver, SHUTDOWN_GRACE_PERIOD, ServeArgs, ServerTitlePhase,\n- apply_effective_log_destination, bind_tcp_host_with_fallback,\n- build_local_object_store_with_preference, build_object_store_from_settings_with_lookup,\n- build_slatedb_store, force_exit_after_shutdown, resolve_bind_request_from_server_settings,\n- resolve_github_webhook_ip_allowlist, resolve_interp,\n- resolve_startup_github_webhook_ip_allowlist, serve_overrides, serve_until_shutdown,\n- server_bind_title, server_title, spawn_shutdown_orchestrator_inner,\n+ SHUTDOWN_GRACE_PERIOD, ServeArgs, ServerTitlePhase, apply_effective_log_destination,\n+ bind_tcp_host_with_fallback, build_local_object_store_with_preference,\n+ build_object_store_from_settings_with_lookup, build_slatedb_store,\n+ force_exit_after_shutdown, resolve_bind_request_from_server_settings, resolve_interp,\n+ serve_overrides, serve_until_shutdown, server_bind_title, server_title,\n+ spawn_shutdown_orchestrator_inner,\n };\n use crate::server::ResolvedAppStateSettings;\n \n@@ -1816,78 +1752,4 @@ disk_cache = true\n assert_ne!(resolved.port(), occupied_port);\n assert!(bound.used_random_port_fallback);\n }\n-\n- #[tokio::test]\n- async fn resolve_github_webhook_ip_allowlist_propagates_resolution_errors() {\n- let settings = server_settings(\n- r#\"\n-_version = 1\n-\n-[server.listen]\n-type = \"tcp\"\n-address = \"127.0.0.1:0\"\n-\n-[server.integrations.github]\n-strategy = \"app\"\n-app_id = \"123\"\n-\n-[server.integrations.github.webhooks.ip_allowlist]\n-entries = [\"github_meta_hooks\"]\n-\"#,\n- )\n- .server;\n-\n- let cache_dir = tempfile::tempdir().unwrap();\n- let listener = std::net::TcpListener::bind(\"127.0.0.1:0\").unwrap();\n- let port = listener.local_addr().unwrap().port();\n- drop(listener);\n- let resolver = GitHubMetaResolver::new(\n- fabro_http::test_http_client().unwrap(),\n- format!(\"http://127.0.0.1:{port}/meta\"),\n- cache_dir.path().join(\"github-meta.json\"),\n- );\n-\n- let error = resolve_github_webhook_ip_allowlist(&settings, &resolver)\n- .await\n- .expect_err(\"github webhook allowlist resolution should fail closed\");\n-\n- assert!(error.to_string().contains(\"GitHub webhook IP allowlist\"));\n- }\n-\n- #[tokio::test]\n- async fn resolve_startup_github_webhook_ip_allowlist_skips_resolution_without_webhook_secret() {\n- let settings = server_settings(\n- r#\"\n-_version = 1\n-\n-[server.listen]\n-type = \"tcp\"\n-address = \"127.0.0.1:0\"\n-\n-[server.integrations.github]\n-strategy = \"app\"\n-app_id = \"123\"\n-\n-[server.integrations.github.webhooks.ip_allowlist]\n-entries = [\"github_meta_hooks\"]\n-\"#,\n- )\n- .server;\n-\n- let cache_dir = tempfile::tempdir().unwrap();\n- let listener = std::net::TcpListener::bind(\"127.0.0.1:0\").unwrap();\n- let port = listener.local_addr().unwrap().port();\n- drop(listener);\n- let resolver = GitHubMetaResolver::new(\n- fabro_http::test_http_client().unwrap(),\n- format!(\"http://127.0.0.1:{port}/meta\"),\n- cache_dir.path().join(\"github-meta.json\"),\n- );\n-\n- let allowlist = resolve_startup_github_webhook_ip_allowlist(&settings, &resolver, false)\n- .await\n- .expect(\"inactive webhook route should skip GitHub meta resolution\");\n-\n- assert!(allowlist.is_none());\n- }\n }\ndiff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs\nindex 44357b2a4..529a374dd 100644\n--- a/lib/crates/fabro-server/src/server.rs\n+++ b/lib/crates/fabro-server/src/server.rs\n@@ -145,7 +145,6 @@ use crate::error::ApiError;\n use crate::github_webhooks::{\n WEBHOOK_ROUTE, WEBHOOK_SECRET_ENV, parse_event_metadata, verify_signature,\n };\n-use crate::ip_allowlist::{IpAllowlistConfig, ip_allowlist_middleware};\n use crate::jwt_auth::{self, AuthMode};\n use crate::principal_middleware::{\n AuthContextSlot, RequestAuth, RequestAuthContext, RequireRunBlob, RequireRunManagementTarget,\n@@ -1589,35 +1588,28 @@ fn start_optional_slack_service(state: &Arc<AppState>) {\n reason = \"Public router helper keeps the existing ergonomic API and forwards by reference.\"\n )]\n pub fn build_router(state: Arc<AppState>, auth_mode: AuthMode) -> Router {\n- build_router_with_options(\n- state,\n- &auth_mode,\n- Arc::new(IpAllowlistConfig::default()),\n- RouterOptions::default(),\n- )\n+ build_router_with_options(state, &auth_mode, RouterOptions::default())\n }\n \n #[derive(Clone, Debug)]\n pub struct RouterOptions {\n- pub web_enabled: bool,\n- pub static_asset_root: Option<PathBuf>,\n- pub github_endpoints: Option<Arc<GithubEndpoints>>,\n- pub github_webhook_ip_allowlist: Option<Arc<IpAllowlistConfig>>,\n+ pub web_enabled: bool,\n+ pub static_asset_root: Option<PathBuf>,\n+ pub github_endpoints: Option<Arc<GithubEndpoints>>,\n /// Set when serving with the `--watch-web` dev flag. The static-file\n /// handler then refuses to fall back to the embedded SPA snapshot and\n /// returns a 503 \"build in progress\" page on miss, so developers see\n /// their edits or a clear signal — never stale embedded bytes.\n- pub watch_web: bool,\n+ pub watch_web: bool,\n }\n \n impl Default for RouterOptions {\n fn default() -> Self {\n Self {\n- web_enabled: true,\n- static_asset_root: None,\n- github_endpoints: None,\n- github_webhook_ip_allowlist: None,\n- watch_web: false,\n+ web_enabled: true,\n+ static_asset_root: None,\n+ github_endpoints: None,\n+ watch_web: false,\n }\n }\n }\n@@ -1630,20 +1622,19 @@ fn removed_web_route(path: &str) -> bool {\n pub fn build_router_with_options(\n state: Arc<AppState>,\n auth_mode: &AuthMode,\n- ip_allowlist_config: Arc<IpAllowlistConfig>,\n options: RouterOptions,\n ) -> Router {\n start_optional_slack_service(&state);\n- let web_enabled = options.web_enabled;\n- let static_asset_root = options.static_asset_root.clone();\n- let watch_web = options.watch_web;\n- let webhook_ip_allowlist = options.github_webhook_ip_allowlist;\n+ let RouterOptions {\n+ web_enabled,\n+ static_asset_root,\n+ github_endpoints,\n+ watch_web,\n+ } = options;\n let translation_state = Arc::clone(&state);\n let state_for_canonical_host = Arc::clone(&state);\n- let github_endpoints = options\n- .github_endpoints\n- .clone()\n- .unwrap_or_else(|| Arc::new(GithubEndpoints::production_defaults()));\n+ let github_endpoints =\n+ github_endpoints.unwrap_or_else(|| Arc::new(GithubEndpoints::production_defaults()));\n let webhook_secret = state.vault_secret(WEBHOOK_SECRET_ENV);\n let principal_layer = middleware::from_fn_with_state(Arc::clone(&state), principal_middleware);\n let api_common = if web_enabled {\n@@ -1725,10 +1716,6 @@ pub fn build_router_with_options(\n }\n }));\n \n- app_router = app_router.layer(middleware::from_fn_with_state(\n- Arc::clone(&ip_allowlist_config),\n- ip_allowlist_middleware,\n- ));\n app_router = app_router.layer(middleware::from_fn_with_state(\n translation_state,\n auth_translation_middleware,\n@@ -1738,9 +1725,8 @@ pub fn build_router_with_options(\n \n let mut router = app_router;\n if let Some(secret) = webhook_secret {\n- let allowlist = webhook_ip_allowlist.unwrap_or(ip_allowlist_config);\n let secret: Arc<[u8]> = Arc::from(secret.into_bytes().into_boxed_slice());\n- router = github_webhook_routes(secret, allowlist).merge(router);\n+ router = github_webhook_routes(secret).merge(router);\n }\n \n router\n@@ -1849,14 +1835,10 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp\n response\n }\n \n-fn github_webhook_routes(secret: Arc<[u8]>, ip_allowlist_config: Arc<IpAllowlistConfig>) -> Router {\n+fn github_webhook_routes(secret: Arc<[u8]>) -> Router {\n Router::new()\n .route(WEBHOOK_ROUTE, post(github_webhook))\n .with_state(secret)\n- .layer(middleware::from_fn_with_state(\n- ip_allowlist_config,\n- ip_allowlist_middleware,\n- ))\n }\n \n async fn github_webhook(\ndiff --git a/lib/crates/fabro-server/src/server/tests.rs b/lib/crates/fabro-server/src/server/tests.rs\nindex e53312627..df5d26152 100644\n--- a/lib/crates/fabro-server/src/server/tests.rs\n+++ b/lib/crates/fabro-server/src/server/tests.rs\n@@ -82,14 +82,10 @@ fn resolved_runtime_settings_from_toml(source: &str) -> ResolvedAppStateSettings\n \n fn test_app_with() -> Router {\n let state = test_app_state();\n- crate::test_support::build_test_router_with_options(\n- state,\n- Arc::new(IpAllowlistConfig::default()),\n- RouterOptions {\n- static_asset_root: Some(spa_fixture_root()),\n- ..RouterOptions::default()\n- },\n- )\n+ crate::test_support::build_test_router_with_options(state, RouterOptions {\n+ static_asset_root: Some(spa_fixture_root()),\n+ ..RouterOptions::default()\n+ })\n }\n \n fn spa_fixture_root() -> PathBuf {\n@@ -491,15 +487,10 @@ fn webhook_test_app(auth_mode: AuthMode) -> Router {\n .expect(\"test vault should not be locked\")\n .set(WEBHOOK_SECRET_ENV, &secret, SecretType::Token, None)\n .unwrap();\n- build_router_with_options(\n- state,\n- &auth_mode,\n- Arc::new(IpAllowlistConfig::default()),\n- RouterOptions {\n- web_enabled: false,\n- ..RouterOptions::default()\n- },\n- )\n+ build_router_with_options(state, &auth_mode, RouterOptions {\n+ web_enabled: false,\n+ ..RouterOptions::default()\n+ })\n }\n \n fn webhook_request(\n@@ -711,11 +702,7 @@ fn canonical_host_test_app() -> Router {\n RunLayer::default(),\n 5,\n );\n- crate::test_support::build_test_router_with_options(\n- state,\n- Arc::new(IpAllowlistConfig::default()),\n- RouterOptions::default(),\n- )\n+ crate::test_support::build_test_router_with_options(state, RouterOptions::default())\n }\n \n #[tokio::test]\ndiff --git a/lib/crates/fabro-server/src/test_support.rs b/lib/crates/fabro-server/src/test_support.rs\nindex 54b0972c1..afd26f624 100644\n--- a/lib/crates/fabro-server/src/test_support.rs\n+++ b/lib/crates/fabro-server/src/test_support.rs\n@@ -29,7 +29,6 @@ use tokio_util::sync::CancellationToken;\n use ulid::Ulid;\n \n use crate::auth;\n-use crate::ip_allowlist::IpAllowlistConfig;\n use crate::jwt_auth::{AuthMode, ConfiguredAuth};\n #[cfg(test)]\n use crate::principal_middleware::{AuthContextSlot, RequestAuthContext};\n@@ -521,15 +520,10 @@ pub fn build_test_router(state: Arc<AppState>) -> Router {\n with_test_user(server::build_router(state, test_auth_mode()))\n }\n \n-pub fn build_test_router_with_options(\n- state: Arc<AppState>,\n- ip_allowlist_config: Arc<IpAllowlistConfig>,\n- options: RouterOptions,\n-) -> Router {\n+pub fn build_test_router_with_options(state: Arc<AppState>, options: RouterOptions) -> Router {\n with_test_user(server::build_router_with_options(\n state,\n &test_auth_mode(),\n- ip_allowlist_config,\n options,\n ))\n }\ndiff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs\nindex 5f10aba10..033e46db6 100644\n--- a/lib/crates/fabro-server/src/web_auth.rs\n+++ b/lib/crates/fabro-server/src/web_auth.rs\n@@ -1401,7 +1401,6 @@ client_id = \"github-client-id\"\n let app = server::build_router_with_options(\n state,\n &github_auth_mode(),\n- Arc::new(crate::ip_allowlist::IpAllowlistConfig::default()),\n server::RouterOptions::default(),\n );\n \n@@ -1494,10 +1493,9 @@ client_id = \"github-client-id\"\n let app = crate::server::build_router_with_options(\n state,\n &github_auth_mode(),\n- Arc::new(crate::ip_allowlist::IpAllowlistConfig::default()),\n crate::server::RouterOptions {\n- web_enabled: true,\n- github_endpoints: Some(Arc::new(GithubEndpoints::with_bases(\n+ web_enabled: true,\n+ github_endpoints: Some(Arc::new(GithubEndpoints::with_bases(\n \"http://127.0.0.1:12345/\"\n .parse()\n .expect(\"oauth base should parse\"),\n@@ -1505,9 +1503,8 @@ client_id = \"github-client-id\"\n .parse()\n .expect(\"api base should parse\"),\n ))),\n- github_webhook_ip_allowlist: None,\n- static_asset_root: None,\n- watch_web: false,\n+ static_asset_root: None,\n+ watch_web: false,\n },\n );\n \n@@ -1700,19 +1697,15 @@ client_id = \"github-client-id\"\n None,\n )\n .unwrap();\n- let app = server::build_router_with_options(\n- state,\n- &github_auth_mode(),\n- Arc::new(crate::ip_allowlist::IpAllowlistConfig::default()),\n- server::RouterOptions {\n+ let app =\n+ server::build_router_with_options(state, &github_auth_mode(), server::RouterOptions {\n web_enabled: true,\n github_endpoints: Some(Arc::new(GithubEndpoints::with_bases(\n github.url(\"/\").parse().expect(\"oauth base should parse\"),\n github.url(\"/api/\").parse().expect(\"api base should parse\"),\n ))),\n ..server::RouterOptions::default()\n- },\n- );\n+ });\n let key = test_cookie_key();\n let mut jar = cookie::CookieJar::new();\n super::add_oauth_state_cookie(\ndiff --git a/lib/crates/fabro-server/tests/it/api/auth_sessions.rs b/lib/crates/fabro-server/tests/it/api/auth_sessions.rs\nindex 0fcd2e521..0ba7d71d8 100644\n--- a/lib/crates/fabro-server/tests/it/api/auth_sessions.rs\n+++ b/lib/crates/fabro-server/tests/it/api/auth_sessions.rs\n@@ -5,7 +5,6 @@ use std::time::Duration;\n use axum::body::Body;\n use axum::http::{Request, StatusCode, header};\n use cookie::{Cookie, CookieJar, Key};\n-use fabro_server::ip_allowlist::IpAllowlistConfig;\n use fabro_server::jwt_auth::resolve_auth_mode_with_lookup;\n use fabro_server::server::{RouterOptions, build_router_with_options};\n use fabro_server::test_support::{TEST_SESSION_SECRET, TestAppStateBuilder};\n@@ -45,12 +44,7 @@ fn test_app(source: &str) -> (axum::Router, Arc<Database>) {\n TEST_SESSION_SECRET.to_string(),\n )]))\n .build();\n- let app = build_router_with_options(\n- state,\n- &auth_mode,\n- Arc::new(IpAllowlistConfig::default()),\n- RouterOptions::default(),\n- );\n+ let app = build_router_with_options(state, &auth_mode, RouterOptions::default());\n (app, store)\n }\n \ndiff --git a/lib/crates/fabro-server/tests/it/api/cli_auth_token.rs b/lib/crates/fabro-server/tests/it/api/cli_auth_token.rs\nindex 6694501e6..2d0b743cb 100644\n--- a/lib/crates/fabro-server/tests/it/api/cli_auth_token.rs\n+++ b/lib/crates/fabro-server/tests/it/api/cli_auth_token.rs\n@@ -4,7 +4,6 @@ use std::time::Duration;\n use axum::body::Body;\n use axum::http::{Request, StatusCode, header};\n use base64::Engine;\n-use fabro_server::ip_allowlist::IpAllowlistConfig;\n use fabro_server::jwt_auth::resolve_auth_mode_with_lookup;\n use fabro_server::server::{RouterOptions, build_router_with_options};\n use fabro_server::test_support::test_app_state_with_store_and_runtime_settings;\n@@ -42,7 +41,6 @@ fn test_app(source: &str) -> (axum::Router, Arc<Database>) {\n artifact_store,\n ),\n &auth_mode,\n- Arc::new(IpAllowlistConfig::default()),\n RouterOptions::default(),\n );\n (app, store)\ndiff --git a/lib/crates/fabro-server/tests/it/api/routing.rs b/lib/crates/fabro-server/tests/it/api/routing.rs\nindex d63b5c4e7..5a4d945f5 100644\n--- a/lib/crates/fabro-server/tests/it/api/routing.rs\n+++ b/lib/crates/fabro-server/tests/it/api/routing.rs\n@@ -1,12 +1,8 @@\n-use std::net::{IpAddr, Ipv4Addr, SocketAddr};\n use std::path::PathBuf;\n-use std::sync::Arc;\n \n use axum::body::Body;\n-use axum::extract::ConnectInfo;\n use axum::http::{Method, Request, StatusCode};\n use fabro_config::ServerSettingsBuilder;\n-use fabro_server::ip_allowlist::{IpAllowlist, IpAllowlistConfig};\n use fabro_server::jwt_auth::{AuthMode, resolve_auth_mode_with_lookup};\n use fabro_server::server::RouterOptions;\n use fabro_server::test_support::{\n@@ -63,7 +59,6 @@ async fn old_unversioned_routes_return_404() {\n async fn root_and_health_stay_at_root() {\n let app = fabro_server::test_support::build_test_router_with_options(\n test_app_state(),\n- Arc::new(IpAllowlistConfig::default()),\n RouterOptions {\n static_asset_root: Some(spa_fixture_root()),\n ..RouterOptions::default()\n@@ -163,7 +158,6 @@ async fn web_enabled_serves_web_only_routes() {\n let app = fabro_server::server::build_router_with_options(\n test_app_state(),\n &auth_mode,\n- Arc::new(IpAllowlistConfig::default()),\n RouterOptions {\n static_asset_root: Some(spa_fixture_root()),\n ..RouterOptions::default()\n@@ -256,7 +250,6 @@ async fn web_enabled_serves_web_only_routes() {\n async fn security_headers_are_applied_to_all_responses() {\n let app = fabro_server::test_support::build_test_router_with_options(\n test_app_state(),\n- Arc::new(IpAllowlistConfig::default()),\n RouterOptions {\n static_asset_root: Some(spa_fixture_root()),\n ..RouterOptions::default()\n@@ -404,7 +397,6 @@ enabled = false\n settings.manifest_run_defaults,\n 5,\n ),\n- Arc::new(IpAllowlistConfig::default()),\n RouterOptions {\n web_enabled: false,\n ..RouterOptions::default()\n@@ -463,7 +455,6 @@ enabled = false\n settings.manifest_run_defaults,\n 5,\n ),\n- Arc::new(IpAllowlistConfig::default()),\n RouterOptions {\n web_enabled: false,\n ..RouterOptions::default()\n@@ -481,60 +472,3 @@ enabled = false\n let response = app.oneshot(request).await.unwrap();\n response_status(response, StatusCode::NOT_FOUND, \"GET /api/v1/runs/{id}\").await;\n }\n-\n-#[tokio::test]\n-async fn allowlist_blocks_non_allowlisted_api_requests() {\n- let app = fabro_server::test_support::build_test_router_with_options(\n- test_app_state(),\n- Arc::new(IpAllowlistConfig {\n- allowlist: IpAllowlist::new(vec![\"10.0.0.0/8\".parse().unwrap()]),\n- trusted_proxy_count: 0,\n- }),\n- RouterOptions::default(),\n- );\n-\n- let response = app\n- .oneshot(request_with_connect_info(\n- \"/api/v1/runs\",\n- IpAddr::V4(Ipv4Addr::new(203, 0, 113, 10)),\n- ))\n- .await\n- .unwrap();\n-\n- response_status(response, StatusCode::FORBIDDEN, \"GET /api/v1/runs\").await;\n-}\n-\n-#[tokio::test]\n-async fn allowlist_exempts_health_checks() {\n- let app = fabro_server::test_support::build_test_router_with_options(\n- test_app_state(),\n- Arc::new(IpAllowlistConfig {\n- allowlist: IpAllowlist::new(vec![\"10.0.0.0/8\".parse().unwrap()]),\n- trusted_proxy_count: 0,\n- }),\n- RouterOptions::default(),\n- );\n-\n- let response = app\n- .oneshot(request_with_connect_info(\n- \"/health\",\n- IpAddr::V4(Ipv4Addr::new(203, 0, 113, 10)),\n- ))\n- .await\n- .unwrap();\n-\n- response_status(response, StatusCode::OK, \"GET /health\").await;\n-}\n-\n-fn request_with_connect_info(path: &str, ip: IpAddr) -> Request<Body> {\n- let request = Request::builder()\n- .method(\"GET\")\n- .uri(path)\n- .body(Body::empty())\n- .expect(\"routing test request should build\");\n- let mut request = request;\n- request\n- .extensions_mut()\n- .insert(ConnectInfo(SocketAddr::new(ip, 8080)));\n- request\n-}\ndiff --git a/lib/crates/fabro-server/tests/it/api/settings.rs b/lib/crates/fabro-server/tests/it/api/settings.rs\nindex f40a6ec39..458e9d6b5 100644\n--- a/lib/crates/fabro-server/tests/it/api/settings.rs\n+++ b/lib/crates/fabro-server/tests/it/api/settings.rs\n@@ -28,7 +28,12 @@ methods = [\"dev-token\", \"github\"]\n allowed_usernames = [\"alice\"]\n \n [server.integrations.github]\n+strategy = \"app\"\n+app_id = \"12345\"\n client_id = \"Iv1.abcdef\"\n+\n+[server.integrations.github.webhooks]\n+strategy = \"tailscale_funnel\"\n \"#,\n );\n let app = fabro_server::test_support::build_test_router(\n@@ -66,6 +71,16 @@ client_id = \"Iv1.abcdef\"\n body[\"server\"][\"integrations\"][\"github\"][\"client_id\"],\n \"Iv1.abcdef\"\n );\n+ assert!(\n+ body[\"server\"].get(\"ip_allowlist\").is_none(),\n+ \"settings response should not expose removed server IP allowlist\"\n+ );\n+ assert!(\n+ body[\"server\"][\"integrations\"][\"github\"][\"webhooks\"]\n+ .get(\"ip_allowlist\")\n+ .is_none(),\n+ \"settings response should not expose removed GitHub webhook IP allowlist\"\n+ );\n assert!(body.get(\"features\").is_none());\n assert!(body.get(\"cli\").is_none());\n assert!(body.get(\"run\").is_none());\ndiff --git a/lib/crates/fabro-server/tests/it/api/tcp.rs b/lib/crates/fabro-server/tests/it/api/tcp.rs\nindex 906c16be4..d9ea549e6 100644\n--- a/lib/crates/fabro-server/tests/it/api/tcp.rs\n+++ b/lib/crates/fabro-server/tests/it/api/tcp.rs\n@@ -5,13 +5,11 @@\n \n use std::net::SocketAddr;\n use std::path::{Path, PathBuf};\n-use std::sync::Arc;\n use std::time::Duration;\n \n use axum::http::StatusCode;\n use fabro_config::bind::Bind;\n use fabro_config::{RuntimeDirectory, ServerSettingsBuilder};\n-use fabro_server::ip_allowlist::{IpAllowlist, IpAllowlistConfig};\n use fabro_server::jwt_auth::{AuthMode, resolve_auth_mode_with_lookup};\n use fabro_server::serve::{ServeArgs, serve_command};\n use fabro_server::server::{RouterOptions, build_router_with_options};\n@@ -24,7 +22,7 @@ use tokio::time::sleep;\n \n use crate::helpers::{api, reqwest_status};\n \n-async fn start_tcp_server(auth_mode: AuthMode, ip_allowlist: Arc<IpAllowlistConfig>) -> SocketAddr {\n+async fn start_tcp_server(auth_mode: AuthMode) -> SocketAddr {\n let listener = TcpListener::bind(\"127.0.0.1:0\")\n .await\n .expect(\"test TCP listener should bind\");\n@@ -33,15 +31,10 @@ async fn start_tcp_server(auth_mode: AuthMode, ip_allowlist: Arc<IpAllowlistConf\n .expect(\"test TCP listener should have a local address\");\n \n let state = test_app_state();\n- let router =\n- build_router_with_options(state, &auth_mode, ip_allowlist, RouterOptions::default());\n+ let router = build_router_with_options(state, &auth_mode, RouterOptions::default());\n \n tokio::spawn(async move {\n- let _ = axum::serve(\n- listener,\n- router.into_make_service_with_connect_info::<SocketAddr>(),\n- )\n- .await;\n+ let _ = axum::serve(listener, router).await;\n });\n \n addr\n@@ -177,7 +170,7 @@ methods = [\"dev-token\"]\n _ => None,\n })\n .expect(\"auth mode should resolve\");\n- let addr = start_tcp_server(auth_mode, Arc::new(IpAllowlistConfig::default())).await;\n+ let addr = start_tcp_server(auth_mode).await;\n let client = fabro_http::test_http_client().unwrap();\n let url = format!(\"http://127.0.0.1:{}{}\", addr.port(), api(\"/runs\"));\n \n@@ -229,24 +222,3 @@ methods = [\"dev-token\"]\n reqwest_status(response, StatusCode::OK, \"GET /api/v1/runs\").await;\n handle.abort();\n }\n-\n-#[tokio::test]\n-async fn tcp_ip_allowlist_uses_connect_info() {\n- let addr = start_tcp_server(\n- fabro_server::test_support::test_auth_mode(),\n- Arc::new(IpAllowlistConfig {\n- allowlist: IpAllowlist::new(vec![\"10.0.0.0/8\".parse().unwrap()]),\n- trusted_proxy_count: 0,\n- }),\n- )\n- .await;\n- let client = fabro_http::test_http_client().unwrap();\n-\n- let response = client\n- .get(format!(\"http://127.0.0.1:{}{}\", addr.port(), api(\"/runs\")))\n- .send()\n- .await\n- .unwrap();\n-\n- reqwest_status(response, StatusCode::FORBIDDEN, \"GET /api/v1/runs\").await;\n-}\ndiff --git a/lib/crates/fabro-types/Cargo.toml b/lib/crates/fabro-types/Cargo.toml\nindex c0a6dfc1b..03631959a 100644\n--- a/lib/crates/fabro-types/Cargo.toml\n+++ b/lib/crates/fabro-types/Cargo.toml\n@@ -24,7 +24,6 @@ dirs.workspace = true\n fabro-model = { path = \"../fabro-model\" }\n fabro-util = { path = \"../fabro-util\" }\n hex.workspace = true\n-ipnet = { version = \"2.11.0\", features = [\"serde\"] }\n serde.workspace = true\n serde_json.workspace = true\n sha2.workspace = true\ndiff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs\nindex 4ad60b494..2e087844a 100644\n--- a/lib/crates/fabro-types/src/settings/mod.rs\n+++ b/lib/crates/fabro-types/src/settings/mod.rs\n@@ -45,12 +45,11 @@ pub use run::{\n RunScmSettings, ScmGitHubSettings, TlsMode,\n };\n pub use server::{\n- GithubIntegrationSettings, IntegrationWebhooksSettings, IpAllowEntry, LogDestination,\n- ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings,\n- ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings,\n- ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, ServerListenSettings,\n- ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings,\n- ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,\n+ GithubIntegrationSettings, IntegrationWebhooksSettings, LogDestination, ObjectStoreSettings,\n+ ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod,\n+ ServerAuthSettings, ServerIntegrationsSettings, ServerListenSettings, ServerLoggingSettings,\n+ ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings,\n+ ServerWebSettings, SlackIntegrationSettings,\n };\n pub use size::{ParseSizeError, Size};\n pub use workflow::WorkflowNamespace;\ndiff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs\nindex bf540dfbe..806788d37 100644\n--- a/lib/crates/fabro-types/src/settings/server.rs\n+++ b/lib/crates/fabro-types/src/settings/server.rs\n@@ -8,7 +8,6 @@\n use std::net::SocketAddr;\n use std::time::Duration as StdDuration;\n \n-use ipnet::IpNet;\n use serde::de::Error as _;\n use serde::{Deserialize, Deserializer, Serialize, Serializer};\n \n@@ -28,7 +27,6 @@ pub struct ServerNamespace {\n pub api: ServerApiSettings,\n pub web: ServerWebSettings,\n pub auth: ServerAuthSettings,\n- pub ip_allowlist: ServerIpAllowlistSettings,\n pub sandbox: ServerSandboxSettings,\n pub storage: ServerStorageSettings,\n pub artifacts: ServerArtifactsSettings,\n@@ -50,7 +48,6 @@ impl ServerNamespace {\n api: ServerApiSettings::default(),\n web: ServerWebSettings::default(),\n auth: ServerAuthSettings::default(),\n- ip_allowlist: ServerIpAllowlistSettings::default(),\n sandbox: ServerSandboxSettings::default(),\n storage: ServerStorageSettings::default(),\n artifacts: ServerArtifactsSettings::default(),\n@@ -123,18 +120,6 @@ pub struct ServerAuthGithubSettings {\n pub allowed_usernames: Vec<String>,\n }\n \n-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\n-pub struct ServerIpAllowlistSettings {\n- pub entries: Vec<IpAllowEntry>,\n- pub trusted_proxy_count: u32,\n-}\n-\n-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\n-pub struct ServerIpAllowlistOverrideSettings {\n- pub entries: Option<Vec<IpAllowEntry>>,\n- pub trusted_proxy_count: Option<u32>,\n-}\n-\n #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\n pub struct ServerSandboxSettings {\n pub providers: ServerSandboxProvidersSettings,\n@@ -175,24 +160,6 @@ impl Default for ServerSandboxProviderSettings {\n }\n }\n \n-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n-pub enum IpAllowEntry {\n- Literal(IpNet),\n- GitHubMetaHooks,\n-}\n-\n-impl IpAllowEntry {\n- pub const GITHUB_META_HOOKS_KEYWORD: &str = \"github_meta_hooks\";\n-\n- pub fn parse_literal(value: &str) -> Result<Self, String> {\n- value\n- .parse::<IpNet>()\n- .or_else(|_| value.parse::<std::net::IpAddr>().map(IpNet::from))\n- .map_err(|error| error.to_string())\n- .map(Self::Literal)\n- }\n-}\n-\n #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n pub struct ServerStorageSettings {\n pub root: InterpString,\n@@ -331,8 +298,7 @@ impl Default for SlackIntegrationSettings {\n \n #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\n pub struct IntegrationWebhooksSettings {\n- pub strategy: Option<WebhookStrategy>,\n- pub ip_allowlist: Option<ServerIpAllowlistOverrideSettings>,\n+ pub strategy: Option<WebhookStrategy>,\n }\n \n fn serialize_socket_addr<S>(value: &SocketAddr, serializer: S) -> Result<S::Ok, S::Error>\ndiff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\nindex d8f4ee3a4..513fea0b3 100644\n--- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n+++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n@@ -56,6 +56,7 @@ models/auth-session-user.ts\n models/auth-session.ts\n models/auth-sessions-response.ts\n models/automation-api-trigger.ts\n+models/automation-list-meta.ts\n models/automation-list-response.ts\n models/automation-ref.ts\n models/automation-schedule-trigger.ts\n@@ -143,7 +144,6 @@ models/fork-response.ts\n models/fork-source-ref.ts\n models/git-author-settings.ts\n models/git-context.ts\n-models/git-hub-meta-hooks-entry.ts\n models/github-integration-settings.ts\n models/github-integration-strategy.ts\n models/health-response.ts\n@@ -184,9 +184,7 @@ models/integration-webhooks-settings.ts\n models/interview-option.ts\n models/interview-provider-settings.ts\n models/interview-question-record.ts\n-models/ip-allow-entry.ts\n models/link-run-pull-request-request.ts\n-models/literal-ip-allow-entry.ts\n models/log-destination.ts\n models/manifest-args.ts\n models/manifest-config.ts\n@@ -417,8 +415,6 @@ models/server-auth-github-settings.ts\n models/server-auth-method.ts\n models/server-auth-settings.ts\n models/server-integrations-settings.ts\n-models/server-ip-allowlist-override-settings.ts\n-models/server-ip-allowlist-settings.ts\n models/server-listen-settings.ts\n models/server-listen-tcp-settings.ts\n models/server-listen-unix-settings.ts\ndiff --git a/lib/packages/fabro-api-client/src/models/automation-list-response.ts b/lib/packages/fabro-api-client/src/models/automation-list-response.ts\nindex e100bb637..58258b67d 100644\n--- a/lib/packages/fabro-api-client/src/models/automation-list-response.ts\n+++ b/lib/packages/fabro-api-client/src/models/automation-list-response.ts\n@@ -16,6 +16,8 @@\n // May contain unused imports in some cases\n // @ts-ignore\n import type { Automation } from './automation';\n+// May contain unused imports in some cases\n+// @ts-ignore\n import type { AutomationListMeta } from './automation-list-meta';\n \n /**\ndiff --git a/lib/packages/fabro-api-client/src/models/git-hub-meta-hooks-entry.ts b/lib/packages/fabro-api-client/src/models/git-hub-meta-hooks-entry.ts\ndeleted file mode 100644\nindex c9b03e94e..000000000\n--- a/lib/packages/fabro-api-client/src/models/git-hub-meta-hooks-entry.ts\n+++ /dev/null\n@@ -1,22 +0,0 @@\n-/* tslint:disable */\n-/* eslint-disable */\n-/**\n- * Fabro Run API\n- * HTTP API for managing Fabro workflow run executions.\n- *\n- * The version of the OpenAPI document: 0.1.0\n- *\n- *\n- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n- * https://openapi-generator.tech\n- * Do not edit the class manually.\n- */\n-\n-\n-\n-\n-export const GitHubMetaHooksEntry = {\n- GIT_HUB_META_HOOKS: 'GitHubMetaHooks'\n-} as const;\n-\n-export type GitHubMetaHooksEntry = typeof GitHubMetaHooksEntry[keyof typeof GitHubMetaHooksEntry];\ndiff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts\nindex e7a9dc111..e66b048ca 100644\n--- a/lib/packages/fabro-api-client/src/models/index.ts\n+++ b/lib/packages/fabro-api-client/src/models/index.ts\n@@ -118,7 +118,6 @@ export * from './fork-response';\n export * from './fork-source-ref';\n export * from './git-author-settings';\n export * from './git-context';\n-export * from './git-hub-meta-hooks-entry';\n export * from './github-integration-settings';\n export * from './github-integration-strategy';\n export * from './health-response';\n@@ -158,9 +157,7 @@ export * from './integration-webhooks-settings';\n export * from './interview-option';\n export * from './interview-provider-settings';\n export * from './interview-question-record';\n-export * from './ip-allow-entry';\n export * from './link-run-pull-request-request';\n-export * from './literal-ip-allow-entry';\n export * from './log-destination';\n export * from './manifest-args';\n export * from './manifest-config';\n@@ -391,8 +388,6 @@ export * from './server-auth-github-settings';\n export * from './server-auth-method';\n export * from './server-auth-settings';\n export * from './server-integrations-settings';\n-export * from './server-ip-allowlist-override-settings';\n-export * from './server-ip-allowlist-settings';\n export * from './server-listen-settings';\n export * from './server-listen-tcp-settings';\n export * from './server-listen-unix-settings';\ndiff --git a/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts b/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts\nindex c9f286718..367922a74 100644\n--- a/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts\n+++ b/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts\n@@ -13,14 +13,10 @@\n */\n \n \n-// May contain unused imports in some cases\n-// @ts-ignore\n-import type { ServerIpAllowlistOverrideSettings } from './server-ip-allowlist-override-settings';\n // May contain unused imports in some cases\n // @ts-ignore\n import type { WebhookStrategy } from './webhook-strategy';\n \n export interface IntegrationWebhooksSettings {\n 'strategy': WebhookStrategy | null;\n- 'ip_allowlist': ServerIpAllowlistOverrideSettings | null;\n }\ndiff --git a/lib/packages/fabro-api-client/src/models/ip-allow-entry.ts b/lib/packages/fabro-api-client/src/models/ip-allow-entry.ts\ndeleted file mode 100644\nindex bff3a8d9c..000000000\n--- a/lib/packages/fabro-api-client/src/models/ip-allow-entry.ts\n+++ /dev/null\n@@ -1,26 +0,0 @@\n-/* tslint:disable */\n-/* eslint-disable */\n-/**\n- * Fabro Run API\n- * HTTP API for managing Fabro workflow run executions.\n- *\n- * The version of the OpenAPI document: 0.1.0\n- *\n- *\n- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n- * https://openapi-generator.tech\n- * Do not edit the class manually.\n- */\n-\n-\n-// May contain unused imports in some cases\n-// @ts-ignore\n-import type { GitHubMetaHooksEntry } from './git-hub-meta-hooks-entry';\n-// May contain unused imports in some cases\n-// @ts-ignore\n-import type { LiteralIpAllowEntry } from './literal-ip-allow-entry';\n-\n-/**\n- * @type IpAllowEntry\n- */\n-export type IpAllowEntry = GitHubMetaHooksEntry | LiteralIpAllowEntry;\ndiff --git a/lib/packages/fabro-api-client/src/models/literal-ip-allow-entry.ts b/lib/packages/fabro-api-client/src/models/literal-ip-allow-entry.ts\ndeleted file mode 100644\nindex dfde31f95..000000000\n--- a/lib/packages/fabro-api-client/src/models/literal-ip-allow-entry.ts\n+++ /dev/null\n@@ -1,19 +0,0 @@\n-/* tslint:disable */\n-/* eslint-disable */\n-/**\n- * Fabro Run API\n- * HTTP API for managing Fabro workflow run executions.\n- *\n- * The version of the OpenAPI document: 0.1.0\n- *\n- *\n- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n- * https://openapi-generator.tech\n- * Do not edit the class manually.\n- */\n-\n-\n-\n-export interface LiteralIpAllowEntry {\n- 'Literal': string;\n-}\ndiff --git a/lib/packages/fabro-api-client/src/models/server-ip-allowlist-override-settings.ts b/lib/packages/fabro-api-client/src/models/server-ip-allowlist-override-settings.ts\ndeleted file mode 100644\nindex 9b6b5fd19..000000000\n--- a/lib/packages/fabro-api-client/src/models/server-ip-allowlist-override-settings.ts\n+++ /dev/null\n@@ -1,23 +0,0 @@\n-/* tslint:disable */\n-/* eslint-disable */\n-/**\n- * Fabro Run API\n- * HTTP API for managing Fabro workflow run executions.\n- *\n- * The version of the OpenAPI document: 0.1.0\n- *\n- *\n- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n- * https://openapi-generator.tech\n- * Do not edit the class manually.\n- */\n-\n-\n-// May contain unused imports in some cases\n-// @ts-ignore\n-import type { IpAllowEntry } from './ip-allow-entry';\n-\n-export interface ServerIpAllowlistOverrideSettings {\n- 'entries': Array<IpAllowEntry> | null;\n- 'trusted_proxy_count': number | null;\n-}\ndiff --git a/lib/packages/fabro-api-client/src/models/server-ip-allowlist-settings.ts b/lib/packages/fabro-api-client/src/models/server-ip-allowlist-settings.ts\ndeleted file mode 100644\nindex bccf9fdf0..000000000\n--- a/lib/packages/fabro-api-client/src/models/server-ip-allowlist-settings.ts\n+++ /dev/null\n@@ -1,23 +0,0 @@\n-/* tslint:disable */\n-/* eslint-disable */\n-/**\n- * Fabro Run API\n- * HTTP API for managing Fabro workflow run executions.\n- *\n- * The version of the OpenAPI document: 0.1.0\n- *\n- *\n- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n- * https://openapi-generator.tech\n- * Do not edit the class manually.\n- */\n-\n-\n-// May contain unused imports in some cases\n-// @ts-ignore\n-import type { IpAllowEntry } from './ip-allow-entry';\n-\n-export interface ServerIpAllowlistSettings {\n- 'entries': Array<IpAllowEntry>;\n- 'trusted_proxy_count': number;\n-}\ndiff --git a/lib/packages/fabro-api-client/src/models/server-namespace.ts b/lib/packages/fabro-api-client/src/models/server-namespace.ts\nindex 315243e88..153195ed8 100644\n--- a/lib/packages/fabro-api-client/src/models/server-namespace.ts\n+++ b/lib/packages/fabro-api-client/src/models/server-namespace.ts\n@@ -27,9 +27,6 @@ import type { ServerAuthSettings } from './server-auth-settings';\n import type { ServerIntegrationsSettings } from './server-integrations-settings';\n // May contain unused imports in some cases\n // @ts-ignore\n-import type { ServerIpAllowlistSettings } from './server-ip-allowlist-settings';\n-// May contain unused imports in some cases\n-// @ts-ignore\n import type { ServerListenSettings } from './server-listen-settings';\n // May contain unused imports in some cases\n // @ts-ignore\n@@ -55,7 +52,6 @@ export interface ServerNamespace {\n 'api': ServerApiSettings;\n 'web': ServerWebSettings;\n 'auth': ServerAuthSettings;\n- 'ip_allowlist': ServerIpAllowlistSettings;\n 'sandbox': ServerSandboxSettings;\n 'storage': ServerStorageSettings;\n 'artifacts': ServerArtifactsSettings;\n",
|
|
"summary": {
|
|
"files_changed": 43,
|
|
"additions": 178,
|
|
"deletions": 1631
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"seq": 1072,
|
|
"checkpoint": {
|
|
"timestamp": "2026-05-28T00:59:00.017037Z",
|
|
"current_node": "simplify_opus",
|
|
"completed_nodes": [
|
|
"start",
|
|
"toolchain",
|
|
"preflight_compile",
|
|
"preflight_lint",
|
|
"implement",
|
|
"simplify_opus"
|
|
],
|
|
"node_retries": {},
|
|
"context_values": {
|
|
"response.implement": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.integrations.github.webhooks.ip_allowlist]`\n - `IpAllowEntry` and related settings/types.\n- Deleted server runtime enforcement:\n - Removed `ip_allowlist` middleware/module.\n - Removed `IpAllowlistConfig`, client-IP extraction, trusted proxy handling, and GitHub `/meta` hook range resolution.\n - Simplified router/startup wiring and removed `ConnectInfo` serving requirements that only existed for allowlisting.\n- Preserved unrelated allowlists:\n - GitHub username allowlists remain.\n - Sandbox/Daytona CIDR allow lists remain.\n- Updated OpenAPI and generated clients:\n - Removed allowlist schemas/fields from `fabro-api.yaml`.\n - Regenerated Rust API and TypeScript API client.\n - Deleted stale generated TS allowlist model files.\n- Updated web UI:\n - Removed Settings > Security network allowlist display.\n - Adjusted Security page/nav copy.\n- Updated tests/docs:\n - Added unknown-field tests proving stale IP allowlist config is now rejected.\n - Added settings API assertions that removed fields are absent.\n - Updated security docs and changelog to direct operators to upstream network controls.\n- Updated dependency graph:\n - Removed direct `ipnet` deps from `fabro-types` and `fabro-server`; retained transitive/use in `fabro-config` for sandbox CIDR validation.\n\nRed/green TDD:\n- Added stale-config rejection tests and verified they failed before schema removal, then passed after removal.\n\nValidation run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo build -p fabro-cli --bin fabro`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server --no-fail-fast`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `git diff --check`\n- Stale-symbol search confirmed remaining `ip_allowlist`/IP allowlist references are only intentional docs/tests/assertions.\n\nNo commits were created.",
|
|
"graph.rankdir": "LR",
|
|
"last_response": "## Review Summary\n\nAll three review agents came back essentially clean.\n\n- **Code Reuse Review** (e7156f0c): Clean. All new code follows existing local patterns (parse + `expect_err`, `body.get(x).is_",
|
|
"thread.start.current_node": "toolchain",
|
|
"internal.thread_id": "implement",
|
|
"thread.preflight_compile.current_node": "preflight_lint",
|
|
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
|
"internal.retry_count.start": 0,
|
|
"internal.retry_count.preflight_lint": 0,
|
|
"internal.node_visit_count": 1,
|
|
"internal.fidelity": "compact",
|
|
"failure_class": "",
|
|
"internal.retry_count.preflight_compile": 0,
|
|
"internal.retry_count.simplify_opus": 0,
|
|
"internal.retry_count.toolchain": 0,
|
|
"internal.run_id": "01KSNYVQCKXPCSCEGWMZ1Q574Z",
|
|
"graph.goal": "---\ntitle: \"refactor: Remove IP allowlisting\"\ntype: refactor\nstatus: active\ndate: 2026-05-27\n---\n\n# refactor: Remove IP allowlisting\n\n## Overview\n\nRemove Fabro's inbound server IP allowlisting feature completely. This removes the\nserver-wide `[server.ip_allowlist]` setting, the GitHub webhook-specific\n`[server.integrations.github.webhooks.ip_allowlist]` overlay, request middleware,\nclient-IP extraction, GitHub `/meta` hook-range expansion, settings API exposure,\ngenerated API client types, and the Settings > Security UI display.\n\nExisting configs that still contain removed IP allowlist keys should fail as\nunknown fields. This is an intentional hard removal, not a compatibility\ndeprecation.\n\n## Problem Frame\n\nThe feature is being removed from Fabro rather than maintained as an in-process\nnetwork access-control layer. Network source restrictions should be handled\noutside Fabro by reverse proxies, firewalls, VPNs, Tailscale, platform ingress,\nor other deployment-layer controls.\n\n## Requirements Trace\n\n- R1. Remove all runtime enforcement of inbound source-IP allowlisting from web,\n API, static asset, and GitHub webhook routes.\n- R2. Remove the server settings schema for global and GitHub webhook IP\n allowlists.\n- R3. Remove `IpAllowEntry` and related API schema/client types from public\n settings payloads.\n- R4. Keep unrelated allowlists intact: GitHub username allowlists, sandbox\n egress CIDR allow lists, and internal policy/test allowlists.\n- R5. Preserve existing authentication, GitHub webhook HMAC verification,\n routing, health checks, request logging, and settings hot-reload behavior.\n- R6. Treat old IP allowlist config as invalid after removal.\n- R7. Update operator-facing docs/changelog so users know to move source-IP\n restrictions upstream.\n\n## Scope Boundaries\n\n- Do not remove `[server.auth.github].allowed_usernames`.\n- Do not remove Daytona/sandbox `cidr_allow_list` network policy support.\n- Do not remove generic uses of \"allowlist\" in policy tests, markdown rendering,\n model controls, or other unrelated domains.\n- Do not add a migration, warning-only parser, or fallback compatibility path for\n old IP allowlist settings.\n- Do not change GitHub webhook signature verification or webhook route strategy.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- Settings use sparse config layers in `lib/crates/fabro-config/src/layers/`\n and dense resolved types in `lib/crates/fabro-types/src/settings/`.\n- The OpenAPI spec at `docs/public/api-reference/fabro-api.yaml` is the source\n of truth for API wire shape; `cargo build -p fabro-api` regenerates Rust API\n code, and `cd lib/packages/fabro-api-client && bun run generate` regenerates\n the TypeScript client.\n- `lib/crates/fabro-server/src/server.rs` builds the router and currently wires\n IP allowlist middleware before other route dispatch behavior.\n- `lib/crates/fabro-server/src/serve.rs` currently resolves server and webhook\n allowlist configs at startup and creates a GitHub `/meta` resolver.\n- `apps/fabro-web/app/routes/settings-security.tsx` displays the settings API's\n `server.ip_allowlist` state.\n\n### Main Removal Surfaces\n\n- Config/types: `fabro-types`, `fabro-config`, config tests.\n- Server runtime: `fabro-server/src/ip_allowlist.rs`, router wiring, startup\n wiring, test helpers, routing/TCP tests.\n- API/contracts: OpenAPI schema, `fabro-api` type replacements/exports, generated\n TypeScript API client models.\n- Frontend/docs: Settings > Security copy, settings navigation copy, changelog.\n\n## Key Technical Decisions\n\n- **Hard removal:** Removed config keys should fail through existing\n `deny_unknown_fields` behavior. This keeps the change simple and makes stale\n deployment config visible immediately.\n- **Delete, do not stub:** Remove `IpAllowlistConfig` and middleware parameters\n instead of passing default empty configs through the router. Empty stubs would\n keep the feature shape alive and make future cleanup harder.\n- **Keep webhook auth unchanged:** GitHub webhook source-IP filtering goes away,\n but HMAC signature verification remains the security boundary for webhook\n payload authenticity.\n- **Generated clients follow OpenAPI:** Update the OpenAPI schemas first, then\n regenerate Rust and TypeScript API outputs rather than hand-editing generated\n client files except as a short-term cleanup if generation leaves stale exports.\n- **Docs mention upstream controls:** The changelog and security guidance should\n direct operators to network-layer controls, not a replacement Fabro setting.\n\n## Implementation Units\n\n- [ ] **Unit 1: Remove config schema and resolved types**\n\n**Goal:** Delete the IP allowlist settings contract from config parsing and dense\nserver settings.\n\n**Requirements:** R2, R3, R4, R6\n\n**Dependencies:** None\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/settings/server.rs`\n- Modify: `lib/crates/fabro-types/src/settings/mod.rs`\n- Modify: `lib/crates/fabro-types/Cargo.toml`\n- Modify: `lib/crates/fabro-config/src/layers/server.rs`\n- Modify: `lib/crates/fabro-config/src/layers/mod.rs`\n- Modify: `lib/crates/fabro-config/src/lib.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/server.rs`\n- Modify: `lib/crates/fabro-config/src/tests/resolve_server.rs`\n\n**Approach:**\n- Remove `ServerNamespace.ip_allowlist` and its `test_default()` population.\n- Delete `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, and\n `IpAllowEntry`.\n- Remove `ServerLayer.ip_allowlist`, `ServerIpAllowlistLayer`,\n `ServerIpAllowlistOverrideLayer`, and `IntegrationWebhooksLayer.ip_allowlist`.\n- Remove resolver functions and validation for global and webhook IP allowlists,\n including `github_meta_hooks` parsing and Unix socket trusted-proxy checks.\n- Remove `ipnet` from `fabro-types`. Keep `ipnet` in `fabro-config` because\n `resolve/environment.rs` still validates sandbox CIDR policy.\n- Replace old positive/negative IP allowlist config tests with unknown-field\n tests for `[server.ip_allowlist]` and\n `[server.integrations.github.webhooks.ip_allowlist]`.\n\n**Patterns to follow:**\n- Existing unknown-field tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`\n for retired server settings.\n- Existing `ServerLayer`/`ServerNamespace` layer-to-resolved pattern for removing\n a server subdomain cleanly.\n\n**Test scenarios:**\n- Error path: TOML with `[server.ip_allowlist]` fails parsing/resolution with an\n unknown-field diagnostic mentioning `ip_allowlist`.\n- Error path: TOML with `[server.integrations.github.webhooks.ip_allowlist]`\n fails with an unknown-field diagnostic mentioning `ip_allowlist`.\n- Happy path: minimal valid server settings still resolve without any\n `ip_allowlist` field.\n- Integration: serialized `ServerSettings` JSON no longer includes\n `server.ip_allowlist`.\n\n**Verification:**\n- `fabro-config` and `fabro-types` compile without removed IP allowlist symbols.\n- Config tests prove stale allowlist settings are rejected.\n\n- [ ] **Unit 2: Remove server middleware and startup resolution**\n\n**Goal:** Remove all runtime source-IP filtering and GitHub `/meta` range\nresolution from `fabro-server`.\n\n**Requirements:** R1, R4, R5\n\n**Dependencies:** Unit 1\n\n**Files:**\n- Delete: `lib/crates/fabro-server/src/ip_allowlist.rs`\n- Modify: `lib/crates/fabro-server/src/lib.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Modify: `lib/crates/fabro-server/src/serve.rs`\n- Modify: `lib/crates/fabro-server/src/test_support.rs`\n- Modify: `lib/crates/fabro-server/Cargo.toml`\n- Modify: `lib/crates/fabro-server/src/auth/translate.rs`\n- Modify: `lib/crates/fabro-server/src/web_auth.rs`\n- Modify: `lib/crates/fabro-cli/tests/it/support/auth_harness.rs`\n\n**Approach:**\n- Remove the public `ip_allowlist` module export.\n- Delete `IpAllowlistConfig`, `IpAllowlist`, `GitHubMetaResolver`, client-IP\n extraction, middleware, and GitHub meta cache helpers.\n- Simplify `build_router_with_options` by removing its `Arc<IpAllowlistConfig>`\n parameter and removing `RouterOptions.github_webhook_ip_allowlist`.\n- Remove the global allowlist middleware layer from the main app router.\n- Simplify `github_webhook_routes` so it only receives the webhook secret and no\n route-specific allowlist config.\n- Remove startup creation of `GitHubMetaResolver`, `default_ip_allowlist`,\n `resolve_github_webhook_ip_allowlist`, and\n `resolve_startup_github_webhook_ip_allowlist`.\n- Replace all test/helper call sites that pass `IpAllowlistConfig::default()`\n with the simplified router signature.\n- Remove `ipnet` and any now-unused HTTP mocking/test-only dependencies from\n `fabro-server` if they are only used by the deleted module.\n- Consider whether `serve.rs` still needs `SocketAddr` for TCP\n `ConnectInfo`; if it was only present for allowlisting tests, remove the\n connect-info service wrapper and imports.\n\n**Patterns to follow:**\n- Keep router layers ordered as they are after the allowlist layer is removed:\n auth translation, demo routing, auth extension, canonical host, security\n headers, HTTP logging, request ID.\n- Existing webhook route HMAC tests and auth tests should remain the source of\n truth for webhook behavior.\n\n**Test scenarios:**\n- Happy path: API requests from any TCP remote address route normally when auth\n requirements are satisfied.\n- Happy path: `/health` remains accessible.\n- Integration: GitHub webhook route still rejects missing/invalid signatures and\n accepts valid signatures exactly as before.\n- Cleanup: there are no references to `IpAllowlistConfig`, `ip_allowlist_middleware`,\n `GitHubMetaResolver`, `github_meta_hooks`, or `github-meta-hooks.json`.\n\n**Verification:**\n- `fabro-server` and CLI auth harness tests compile against the simplified router\n API.\n- Runtime startup no longer performs any GitHub `/meta` fetch for webhook IP\n ranges.\n\n- [ ] **Unit 3: Update OpenAPI and generated API clients**\n\n**Goal:** Remove IP allowlist fields and schemas from public settings API\ncontracts and regenerated clients.\n\n**Requirements:** R3, R4\n\n**Dependencies:** Units 1 and 2\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs`\n- Modify/generated: `lib/packages/fabro-api-client/src/models/*`\n\n**Approach:**\n- Remove `ip_allowlist` from `ServerNamespace.required` and\n `ServerNamespace.properties`.\n- Remove `ServerIpAllowlistSettings`,\n `ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,\n `LiteralIpAllowEntry`, and `GitHubMetaHooksEntry` schemas.\n- Remove `IntegrationWebhooksSettings.ip_allowlist` from required/properties.\n- Remove `with_replacement` entries and public re-exports for removed types in\n `fabro-api`.\n- Regenerate Rust API code by building `fabro-api`.\n- Regenerate TypeScript Axios client and ensure stale allowlist model files and\n index exports are gone.\n- Strengthen round-trip tests to assert settings JSON omits\n `server.ip_allowlist` and webhook `ip_allowlist`.\n\n**Patterns to follow:**\n- Existing OpenAPI-first workflow in `AGENTS.md`.\n- Existing `server_settings_family_reuses_domain_types` assertions for shared\n domain type identity.\n\n**Test scenarios:**\n- Contract: `ServerSettings` round-trips through API types without any IP\n allowlist field.\n- Contract: OpenAPI-generated TypeScript `ServerNamespace` has no\n `ip_allowlist` property.\n- Contract: `IntegrationWebhooksSettings` has only webhook strategy fields after\n removal.\n\n**Verification:**\n- Generated Rust and TypeScript clients match the updated OpenAPI spec.\n- `rg \"ServerIpAllowlist|IpAllowEntry|server-ip-allowlist|literal-ip-allow-entry\"`\n finds no remaining generated/public API references.\n\n- [ ] **Unit 4: Update web settings UI**\n\n**Goal:** Remove IP allowlist display from Settings > Security and align copy\nwith the new settings shape.\n\n**Requirements:** R3, R7\n\n**Dependencies:** Unit 3\n\n**Files:**\n- Modify: `apps/fabro-web/app/routes/settings-security.tsx`\n- Modify: `apps/fabro-web/app/routes/settings.tsx`\n\n**Approach:**\n- Change the Security page description from \"Authentication methods and network\n allowlist\" to authentication-only wording.\n- Remove destructuring and rendering of `settings.server.ip_allowlist`.\n- Remove the unused `Count` and `plural` imports if no longer used.\n- Update the Settings nav description for Security from \"Authentication and\n network allowlist\" to authentication-focused copy.\n\n**Patterns to follow:**\n- Existing `settings-panel` row layout for Auth methods and Allowed usernames.\n\n**Test scenarios:**\n- Typecheck: the route compiles against the regenerated API client with no\n `server.ip_allowlist` property.\n- UI behavior: Security page still renders auth methods and allowed usernames.\n- Cleanup: no frontend references to `ip_allowlist`, `IP allowlist`, or\n `trusted_proxy_count` remain.\n\n**Verification:**\n- `apps/fabro-web` typecheck passes against the new generated client.\n\n- [ ] **Unit 5: Update tests, docs, changelog, and dependency lockfile**\n\n**Goal:** Remove stale references and document the operator-facing behavior\nchange.\n\n**Requirements:** R4, R6, R7\n\n**Dependencies:** Units 1 through 4\n\n**Files:**\n- Modify: `lib/crates/fabro-server/tests/it/api/routing.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/tcp.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/settings.rs`\n- Modify: `docs/public/administration/security.mdx`\n- Create or modify: `docs/public/changelog/2026-05-27.mdx`\n- Modify: `Cargo.lock` if dependency graph changes\n\n**Approach:**\n- Delete route/TCP tests that only prove IP allowlist enforcement or\n `ConnectInfo` behavior.\n- Adjust any router setup helpers after the signature simplification.\n- Add a settings API assertion that `server.ip_allowlist` is absent from\n `/api/v1/settings`.\n- Update security docs to explicitly state that Fabro does not provide inbound\n source-IP allowlisting and operators should use upstream network controls.\n- Add a changelog entry dated 2026-05-27 describing the hard removal and the\n expected replacement at the deployment layer.\n- Run dependency resolution after removing direct `ipnet`/test dependencies;\n keep transitive or unrelated `ipnet` entries needed by sandbox CIDR validation.\n\n**Patterns to follow:**\n- Existing changelog style in `docs/public/changelog/2026-05-26.mdx`.\n- Existing settings API integration test style in\n `lib/crates/fabro-server/tests/it/api/settings.rs`.\n\n**Test scenarios:**\n- Settings API: response contains server auth, listen, storage, scheduler, and\n integrations fields, but not `server.ip_allowlist`.\n- Config compatibility: old IP allowlist TOML fails before startup rather than\n being silently ignored.\n- Docs validation: security docs no longer imply Fabro can restrict inbound\n source IPs internally.\n\n**Verification:**\n- `rg -n \"ip_allowlist|trusted_proxy_count|github_meta_hooks|IP allowlist|ip allowlist\"`\n returns only historical archived plan/brainstorm/spec references or unrelated\n non-server allowlist text.\n\n## System-Wide Impact\n\n- **Public API:** `GET /api/v1/settings` response shape changes by removing\n `server.ip_allowlist` and webhook `ip_allowlist`.\n- **Config compatibility:** Existing `settings.toml` files containing the removed\n keys become invalid. This is intentional.\n- **Runtime security posture:** Fabro no longer blocks requests based on source\n IP. Operators must enforce network source restrictions upstream.\n- **Webhook handling:** GitHub webhook HMAC verification remains unchanged; only\n optional source-IP filtering is removed.\n- **Generated clients:** Downstream TypeScript/Rust consumers that read\n `server.ip_allowlist` must update.\n\n## Risks & Mitigations\n\n| Risk | Mitigation |\n|------|------------|\n| Operators accidentally expose a server that previously relied on Fabro IP allowlisting | Changelog and security docs explicitly call out the removal and direct operators to upstream controls. |\n| Generated API clients retain stale types | Update OpenAPI first, regenerate both Rust and TypeScript clients, and run search checks for stale symbols. |\n| Unrelated allowlist functionality is removed by broad search/replace | Scope searches to `IpAllow`, `ip_allowlist`, `trusted_proxy_count`, and `github_meta_hooks`; preserve sandbox CIDR and GitHub username allowlists. |\n| Router signature cleanup breaks many tests | Update shared test helpers first, then compile-driven cleanup of remaining call sites. |\n| `ipnet` is removed where still needed | Keep `fabro-config` dependency if environment CIDR validation still imports `ipnet::IpNet`. |\n\n## Test Plan\n\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun test`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n\n## Assumptions\n\n- The accepted compatibility policy is hard removal: no warning-only parser, no\n migration, and no custom compatibility error path.\n- Public API consumers can tolerate a breaking settings shape change for this\n removed feature.\n- Historical documents in `docs/plans/`, `docs/brainstorms/`, and\n `docs/superpowers/` are archival and do not need rewriting.\n\n## Sources & References\n\n- Previous feature requirements:\n `docs/brainstorms/2026-04-15-ip-whitelist-requirements.md`\n- Previous implementation plan:\n `docs/plans/2026-04-15-002-feat-ip-allowlist-plan.md`\n- API workflow guidance: `AGENTS.md`\n- OpenAPI source of truth: `docs/public/api-reference/fabro-api.yaml`\n",
|
|
"last_stage": "simplify_opus",
|
|
"failure_signature": "",
|
|
"response.simplify_opus": "## Review Summary\n\nAll three review agents came back essentially clean.\n\n- **Code Reuse Review** (e7156f0c): Clean. All new code follows existing local patterns (parse + `expect_err`, `body.get(x).is_none()`, `RouterOptions` destructure). No missed utilities.\n- **Code Quality Review** (59feb42b): Clean. Symbol audit confirms full removal of `IpAllowlistConfig`, `IpAllowlist`, `IpAllowEntry`, `LiteralIpAllowEntry`, `ip_allowlist_middleware`, `GitHubMetaResolver`, `github_meta_hooks`, `trusted_proxy_count`, `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, `extract_client_ip`, `GitHubMetaCache`, and frontend `ip_allowlist`/`Count`/`plural` wiring. `deny_unknown_fields` enforces hard removal at the TOML boundary. No stubs, dead imports, or vestigial parameters from the refactor.\n- **Efficiency Review** (51c928ad): Clean. Per-request IP allowlist middleware (two layers — main app and webhook) gone. Startup GitHub `/meta` fetch + on-disk cache gone. `ConnectInfo<SocketAddr>` plumbing removed from `serve.rs` (both Tcp and Unix variants) and from `tests/it/api/tcp.rs`. No new hot-path work introduced anywhere.\n\n**One out-of-scope finding skipped:** the efficiency agent flagged `install.rs:710` as still using `into_make_service_with_connect_info::<SocketAddr>()` despite nothing in `install.rs` extracting `ConnectInfo` (the `detect_remote_ip` helper only reads the `x-forwarded-for` header). I verified this is true — it's the last `ConnectInfo` caller in the crate — but it's pre-existing code in the install flow, not introduced by this refactor, and unrelated to IP allowlisting. The agent rated it low-severity cleanup. Per the scope discipline of this refactor (hard removal of IP allowlisting only, no scope creep) and the system prompt's \"only make changes that are directly requested or clearly necessary,\" I'm leaving it alone.\n\nNo fixes applied — the diff was already clean.",
|
|
"internal.work_dir": "/home/daytona/workspace/fabro",
|
|
"internal.retry_count.implement": 0,
|
|
"outcome": "succeeded",
|
|
"current_node": "simplify_opus",
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
|
|
"thread.implement.current_node": "simplify_opus",
|
|
"thread.preflight_lint.current_node": "implement",
|
|
"thread.toolchain.current_node": "preflight_compile"
|
|
},
|
|
"node_outcomes": {
|
|
"simplify_opus": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"response.simplify_opus": "## Review Summary\n\nAll three review agents came back essentially clean.\n\n- **Code Reuse Review** (e7156f0c): Clean. All new code follows existing local patterns (parse + `expect_err`, `body.get(x).is_none()`, `RouterOptions` destructure). No missed utilities.\n- **Code Quality Review** (59feb42b): Clean. Symbol audit confirms full removal of `IpAllowlistConfig`, `IpAllowlist`, `IpAllowEntry`, `LiteralIpAllowEntry`, `ip_allowlist_middleware`, `GitHubMetaResolver`, `github_meta_hooks`, `trusted_proxy_count`, `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, `extract_client_ip`, `GitHubMetaCache`, and frontend `ip_allowlist`/`Count`/`plural` wiring. `deny_unknown_fields` enforces hard removal at the TOML boundary. No stubs, dead imports, or vestigial parameters from the refactor.\n- **Efficiency Review** (51c928ad): Clean. Per-request IP allowlist middleware (two layers — main app and webhook) gone. Startup GitHub `/meta` fetch + on-disk cache gone. `ConnectInfo<SocketAddr>` plumbing removed from `serve.rs` (both Tcp and Unix variants) and from `tests/it/api/tcp.rs`. No new hot-path work introduced anywhere.\n\n**One out-of-scope finding skipped:** the efficiency agent flagged `install.rs:710` as still using `into_make_service_with_connect_info::<SocketAddr>()` despite nothing in `install.rs` extracting `ConnectInfo` (the `detect_remote_ip` helper only reads the `x-forwarded-for` header). I verified this is true — it's the last `ConnectInfo` caller in the crate — but it's pre-existing code in the install flow, not introduced by this refactor, and unrelated to IP allowlisting. The agent rated it low-severity cleanup. Per the scope discipline of this refactor (hard removal of IP allowlisting only, no scope creep) and the system prompt's \"only make changes that are directly requested or clearly necessary,\" I'm leaving it alone.\n\nNo fixes applied — the diff was already clean.",
|
|
"last_response": "## Review Summary\n\nAll three review agents came back essentially clean.\n\n- **Code Reuse Review** (e7156f0c): Clean. All new code follows existing local patterns (parse + `expect_err`, `body.get(x).is_",
|
|
"last_stage": "simplify_opus"
|
|
},
|
|
"notes": "Stage completed: simplify_opus",
|
|
"usage": {
|
|
"input": {
|
|
"usage": {
|
|
"model": {
|
|
"provider": "anthropic",
|
|
"model_id": "claude-opus-4-7"
|
|
},
|
|
"tokens": {
|
|
"input_tokens": 21129,
|
|
"output_tokens": 5543,
|
|
"reasoning_tokens": 0,
|
|
"cache_read_tokens": 251482,
|
|
"cache_write_tokens": 61150
|
|
}
|
|
},
|
|
"facts": {
|
|
"algorithm": "anthropic",
|
|
"cache_write_5m_tokens": 61150,
|
|
"cache_write_1h_tokens": 0
|
|
}
|
|
},
|
|
"total_usd_micros": 752148
|
|
},
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 83152,
|
|
"tool_time_ms": 271255,
|
|
"active_time_ms": 354407
|
|
}
|
|
},
|
|
"start": {
|
|
"status": "succeeded",
|
|
"usage": null
|
|
},
|
|
"preflight_lint": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
|
},
|
|
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 142983,
|
|
"active_time_ms": 142983
|
|
}
|
|
},
|
|
"toolchain": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
|
},
|
|
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 1482,
|
|
"active_time_ms": 1482
|
|
}
|
|
},
|
|
"preflight_compile": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
|
},
|
|
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 135614,
|
|
"active_time_ms": 135614
|
|
}
|
|
},
|
|
"implement": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"last_stage": "implement",
|
|
"last_response": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.int",
|
|
"response.implement": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.integrations.github.webhooks.ip_allowlist]`\n - `IpAllowEntry` and related settings/types.\n- Deleted server runtime enforcement:\n - Removed `ip_allowlist` middleware/module.\n - Removed `IpAllowlistConfig`, client-IP extraction, trusted proxy handling, and GitHub `/meta` hook range resolution.\n - Simplified router/startup wiring and removed `ConnectInfo` serving requirements that only existed for allowlisting.\n- Preserved unrelated allowlists:\n - GitHub username allowlists remain.\n - Sandbox/Daytona CIDR allow lists remain.\n- Updated OpenAPI and generated clients:\n - Removed allowlist schemas/fields from `fabro-api.yaml`.\n - Regenerated Rust API and TypeScript API client.\n - Deleted stale generated TS allowlist model files.\n- Updated web UI:\n - Removed Settings > Security network allowlist display.\n - Adjusted Security page/nav copy.\n- Updated tests/docs:\n - Added unknown-field tests proving stale IP allowlist config is now rejected.\n - Added settings API assertions that removed fields are absent.\n - Updated security docs and changelog to direct operators to upstream network controls.\n- Updated dependency graph:\n - Removed direct `ipnet` deps from `fabro-types` and `fabro-server`; retained transitive/use in `fabro-config` for sandbox CIDR validation.\n\nRed/green TDD:\n- Added stale-config rejection tests and verified they failed before schema removal, then passed after removal.\n\nValidation run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo build -p fabro-cli --bin fabro`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server --no-fail-fast`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `git diff --check`\n- Stale-symbol search confirmed remaining `ip_allowlist`/IP allowlist references are only intentional docs/tests/assertions.\n\nNo commits were created."
|
|
},
|
|
"notes": "Stage completed: implement",
|
|
"usage": {
|
|
"input": {
|
|
"usage": {
|
|
"model": {
|
|
"provider": "openai",
|
|
"model_id": "gpt-5.5"
|
|
},
|
|
"tokens": {
|
|
"input_tokens": 2556897,
|
|
"output_tokens": 26084,
|
|
"reasoning_tokens": 9280,
|
|
"cache_read_tokens": 17923072,
|
|
"cache_write_tokens": 0
|
|
}
|
|
},
|
|
"facts": {
|
|
"algorithm": "openai"
|
|
}
|
|
},
|
|
"total_usd_micros": 22806941
|
|
},
|
|
"files_touched": [
|
|
"/home/daytona/workspace/fabro/docs/public/changelog/2026-05-27.mdx"
|
|
],
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 1238818,
|
|
"tool_time_ms": 594726,
|
|
"active_time_ms": 1833544
|
|
}
|
|
}
|
|
},
|
|
"next_node_id": "simplify_gpt",
|
|
"git_commit_sha": "3a043fd3e797a3ca687b89857f961b4ca470bbd2",
|
|
"node_visits": {
|
|
"toolchain": 1,
|
|
"preflight_compile": 1,
|
|
"simplify_opus": 1,
|
|
"preflight_lint": 1,
|
|
"implement": 1,
|
|
"start": 1
|
|
}
|
|
},
|
|
"diff": {
|
|
"summary": {
|
|
"files_changed": 43,
|
|
"additions": 178,
|
|
"deletions": 1631
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"seq": 1288,
|
|
"checkpoint": {
|
|
"timestamp": "2026-05-28T01:02:40.658985Z",
|
|
"current_node": "simplify_gpt",
|
|
"completed_nodes": [
|
|
"start",
|
|
"toolchain",
|
|
"preflight_compile",
|
|
"preflight_lint",
|
|
"implement",
|
|
"simplify_opus",
|
|
"simplify_gpt"
|
|
],
|
|
"node_retries": {},
|
|
"context_values": {
|
|
"graph.rankdir": "LR",
|
|
"last_stage": "simplify_gpt",
|
|
"thread.implement.current_node": "simplify_opus",
|
|
"internal.retry_count.start": 0,
|
|
"internal.run_id": "01KSNYVQCKXPCSCEGWMZ1Q574Z",
|
|
"internal.retry_count.toolchain": 0,
|
|
"outcome": "succeeded",
|
|
"internal.thread_id": "simplify_opus",
|
|
"internal.retry_count.implement": 0,
|
|
"last_response": "Review complete.\n\nWhat I checked:\n- Diff against merge base `a992a7d76...`\n- Remaining references to removed IP allowlist symbols/settings outside archival/build output\n- Key config/server/API/web/doc",
|
|
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
|
|
"graph.goal": "---\ntitle: \"refactor: Remove IP allowlisting\"\ntype: refactor\nstatus: active\ndate: 2026-05-27\n---\n\n# refactor: Remove IP allowlisting\n\n## Overview\n\nRemove Fabro's inbound server IP allowlisting feature completely. This removes the\nserver-wide `[server.ip_allowlist]` setting, the GitHub webhook-specific\n`[server.integrations.github.webhooks.ip_allowlist]` overlay, request middleware,\nclient-IP extraction, GitHub `/meta` hook-range expansion, settings API exposure,\ngenerated API client types, and the Settings > Security UI display.\n\nExisting configs that still contain removed IP allowlist keys should fail as\nunknown fields. This is an intentional hard removal, not a compatibility\ndeprecation.\n\n## Problem Frame\n\nThe feature is being removed from Fabro rather than maintained as an in-process\nnetwork access-control layer. Network source restrictions should be handled\noutside Fabro by reverse proxies, firewalls, VPNs, Tailscale, platform ingress,\nor other deployment-layer controls.\n\n## Requirements Trace\n\n- R1. Remove all runtime enforcement of inbound source-IP allowlisting from web,\n API, static asset, and GitHub webhook routes.\n- R2. Remove the server settings schema for global and GitHub webhook IP\n allowlists.\n- R3. Remove `IpAllowEntry` and related API schema/client types from public\n settings payloads.\n- R4. Keep unrelated allowlists intact: GitHub username allowlists, sandbox\n egress CIDR allow lists, and internal policy/test allowlists.\n- R5. Preserve existing authentication, GitHub webhook HMAC verification,\n routing, health checks, request logging, and settings hot-reload behavior.\n- R6. Treat old IP allowlist config as invalid after removal.\n- R7. Update operator-facing docs/changelog so users know to move source-IP\n restrictions upstream.\n\n## Scope Boundaries\n\n- Do not remove `[server.auth.github].allowed_usernames`.\n- Do not remove Daytona/sandbox `cidr_allow_list` network policy support.\n- Do not remove generic uses of \"allowlist\" in policy tests, markdown rendering,\n model controls, or other unrelated domains.\n- Do not add a migration, warning-only parser, or fallback compatibility path for\n old IP allowlist settings.\n- Do not change GitHub webhook signature verification or webhook route strategy.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- Settings use sparse config layers in `lib/crates/fabro-config/src/layers/`\n and dense resolved types in `lib/crates/fabro-types/src/settings/`.\n- The OpenAPI spec at `docs/public/api-reference/fabro-api.yaml` is the source\n of truth for API wire shape; `cargo build -p fabro-api` regenerates Rust API\n code, and `cd lib/packages/fabro-api-client && bun run generate` regenerates\n the TypeScript client.\n- `lib/crates/fabro-server/src/server.rs` builds the router and currently wires\n IP allowlist middleware before other route dispatch behavior.\n- `lib/crates/fabro-server/src/serve.rs` currently resolves server and webhook\n allowlist configs at startup and creates a GitHub `/meta` resolver.\n- `apps/fabro-web/app/routes/settings-security.tsx` displays the settings API's\n `server.ip_allowlist` state.\n\n### Main Removal Surfaces\n\n- Config/types: `fabro-types`, `fabro-config`, config tests.\n- Server runtime: `fabro-server/src/ip_allowlist.rs`, router wiring, startup\n wiring, test helpers, routing/TCP tests.\n- API/contracts: OpenAPI schema, `fabro-api` type replacements/exports, generated\n TypeScript API client models.\n- Frontend/docs: Settings > Security copy, settings navigation copy, changelog.\n\n## Key Technical Decisions\n\n- **Hard removal:** Removed config keys should fail through existing\n `deny_unknown_fields` behavior. This keeps the change simple and makes stale\n deployment config visible immediately.\n- **Delete, do not stub:** Remove `IpAllowlistConfig` and middleware parameters\n instead of passing default empty configs through the router. Empty stubs would\n keep the feature shape alive and make future cleanup harder.\n- **Keep webhook auth unchanged:** GitHub webhook source-IP filtering goes away,\n but HMAC signature verification remains the security boundary for webhook\n payload authenticity.\n- **Generated clients follow OpenAPI:** Update the OpenAPI schemas first, then\n regenerate Rust and TypeScript API outputs rather than hand-editing generated\n client files except as a short-term cleanup if generation leaves stale exports.\n- **Docs mention upstream controls:** The changelog and security guidance should\n direct operators to network-layer controls, not a replacement Fabro setting.\n\n## Implementation Units\n\n- [ ] **Unit 1: Remove config schema and resolved types**\n\n**Goal:** Delete the IP allowlist settings contract from config parsing and dense\nserver settings.\n\n**Requirements:** R2, R3, R4, R6\n\n**Dependencies:** None\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/settings/server.rs`\n- Modify: `lib/crates/fabro-types/src/settings/mod.rs`\n- Modify: `lib/crates/fabro-types/Cargo.toml`\n- Modify: `lib/crates/fabro-config/src/layers/server.rs`\n- Modify: `lib/crates/fabro-config/src/layers/mod.rs`\n- Modify: `lib/crates/fabro-config/src/lib.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/server.rs`\n- Modify: `lib/crates/fabro-config/src/tests/resolve_server.rs`\n\n**Approach:**\n- Remove `ServerNamespace.ip_allowlist` and its `test_default()` population.\n- Delete `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, and\n `IpAllowEntry`.\n- Remove `ServerLayer.ip_allowlist`, `ServerIpAllowlistLayer`,\n `ServerIpAllowlistOverrideLayer`, and `IntegrationWebhooksLayer.ip_allowlist`.\n- Remove resolver functions and validation for global and webhook IP allowlists,\n including `github_meta_hooks` parsing and Unix socket trusted-proxy checks.\n- Remove `ipnet` from `fabro-types`. Keep `ipnet` in `fabro-config` because\n `resolve/environment.rs` still validates sandbox CIDR policy.\n- Replace old positive/negative IP allowlist config tests with unknown-field\n tests for `[server.ip_allowlist]` and\n `[server.integrations.github.webhooks.ip_allowlist]`.\n\n**Patterns to follow:**\n- Existing unknown-field tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`\n for retired server settings.\n- Existing `ServerLayer`/`ServerNamespace` layer-to-resolved pattern for removing\n a server subdomain cleanly.\n\n**Test scenarios:**\n- Error path: TOML with `[server.ip_allowlist]` fails parsing/resolution with an\n unknown-field diagnostic mentioning `ip_allowlist`.\n- Error path: TOML with `[server.integrations.github.webhooks.ip_allowlist]`\n fails with an unknown-field diagnostic mentioning `ip_allowlist`.\n- Happy path: minimal valid server settings still resolve without any\n `ip_allowlist` field.\n- Integration: serialized `ServerSettings` JSON no longer includes\n `server.ip_allowlist`.\n\n**Verification:**\n- `fabro-config` and `fabro-types` compile without removed IP allowlist symbols.\n- Config tests prove stale allowlist settings are rejected.\n\n- [ ] **Unit 2: Remove server middleware and startup resolution**\n\n**Goal:** Remove all runtime source-IP filtering and GitHub `/meta` range\nresolution from `fabro-server`.\n\n**Requirements:** R1, R4, R5\n\n**Dependencies:** Unit 1\n\n**Files:**\n- Delete: `lib/crates/fabro-server/src/ip_allowlist.rs`\n- Modify: `lib/crates/fabro-server/src/lib.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Modify: `lib/crates/fabro-server/src/serve.rs`\n- Modify: `lib/crates/fabro-server/src/test_support.rs`\n- Modify: `lib/crates/fabro-server/Cargo.toml`\n- Modify: `lib/crates/fabro-server/src/auth/translate.rs`\n- Modify: `lib/crates/fabro-server/src/web_auth.rs`\n- Modify: `lib/crates/fabro-cli/tests/it/support/auth_harness.rs`\n\n**Approach:**\n- Remove the public `ip_allowlist` module export.\n- Delete `IpAllowlistConfig`, `IpAllowlist`, `GitHubMetaResolver`, client-IP\n extraction, middleware, and GitHub meta cache helpers.\n- Simplify `build_router_with_options` by removing its `Arc<IpAllowlistConfig>`\n parameter and removing `RouterOptions.github_webhook_ip_allowlist`.\n- Remove the global allowlist middleware layer from the main app router.\n- Simplify `github_webhook_routes` so it only receives the webhook secret and no\n route-specific allowlist config.\n- Remove startup creation of `GitHubMetaResolver`, `default_ip_allowlist`,\n `resolve_github_webhook_ip_allowlist`, and\n `resolve_startup_github_webhook_ip_allowlist`.\n- Replace all test/helper call sites that pass `IpAllowlistConfig::default()`\n with the simplified router signature.\n- Remove `ipnet` and any now-unused HTTP mocking/test-only dependencies from\n `fabro-server` if they are only used by the deleted module.\n- Consider whether `serve.rs` still needs `SocketAddr` for TCP\n `ConnectInfo`; if it was only present for allowlisting tests, remove the\n connect-info service wrapper and imports.\n\n**Patterns to follow:**\n- Keep router layers ordered as they are after the allowlist layer is removed:\n auth translation, demo routing, auth extension, canonical host, security\n headers, HTTP logging, request ID.\n- Existing webhook route HMAC tests and auth tests should remain the source of\n truth for webhook behavior.\n\n**Test scenarios:**\n- Happy path: API requests from any TCP remote address route normally when auth\n requirements are satisfied.\n- Happy path: `/health` remains accessible.\n- Integration: GitHub webhook route still rejects missing/invalid signatures and\n accepts valid signatures exactly as before.\n- Cleanup: there are no references to `IpAllowlistConfig`, `ip_allowlist_middleware`,\n `GitHubMetaResolver`, `github_meta_hooks`, or `github-meta-hooks.json`.\n\n**Verification:**\n- `fabro-server` and CLI auth harness tests compile against the simplified router\n API.\n- Runtime startup no longer performs any GitHub `/meta` fetch for webhook IP\n ranges.\n\n- [ ] **Unit 3: Update OpenAPI and generated API clients**\n\n**Goal:** Remove IP allowlist fields and schemas from public settings API\ncontracts and regenerated clients.\n\n**Requirements:** R3, R4\n\n**Dependencies:** Units 1 and 2\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs`\n- Modify/generated: `lib/packages/fabro-api-client/src/models/*`\n\n**Approach:**\n- Remove `ip_allowlist` from `ServerNamespace.required` and\n `ServerNamespace.properties`.\n- Remove `ServerIpAllowlistSettings`,\n `ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,\n `LiteralIpAllowEntry`, and `GitHubMetaHooksEntry` schemas.\n- Remove `IntegrationWebhooksSettings.ip_allowlist` from required/properties.\n- Remove `with_replacement` entries and public re-exports for removed types in\n `fabro-api`.\n- Regenerate Rust API code by building `fabro-api`.\n- Regenerate TypeScript Axios client and ensure stale allowlist model files and\n index exports are gone.\n- Strengthen round-trip tests to assert settings JSON omits\n `server.ip_allowlist` and webhook `ip_allowlist`.\n\n**Patterns to follow:**\n- Existing OpenAPI-first workflow in `AGENTS.md`.\n- Existing `server_settings_family_reuses_domain_types` assertions for shared\n domain type identity.\n\n**Test scenarios:**\n- Contract: `ServerSettings` round-trips through API types without any IP\n allowlist field.\n- Contract: OpenAPI-generated TypeScript `ServerNamespace` has no\n `ip_allowlist` property.\n- Contract: `IntegrationWebhooksSettings` has only webhook strategy fields after\n removal.\n\n**Verification:**\n- Generated Rust and TypeScript clients match the updated OpenAPI spec.\n- `rg \"ServerIpAllowlist|IpAllowEntry|server-ip-allowlist|literal-ip-allow-entry\"`\n finds no remaining generated/public API references.\n\n- [ ] **Unit 4: Update web settings UI**\n\n**Goal:** Remove IP allowlist display from Settings > Security and align copy\nwith the new settings shape.\n\n**Requirements:** R3, R7\n\n**Dependencies:** Unit 3\n\n**Files:**\n- Modify: `apps/fabro-web/app/routes/settings-security.tsx`\n- Modify: `apps/fabro-web/app/routes/settings.tsx`\n\n**Approach:**\n- Change the Security page description from \"Authentication methods and network\n allowlist\" to authentication-only wording.\n- Remove destructuring and rendering of `settings.server.ip_allowlist`.\n- Remove the unused `Count` and `plural` imports if no longer used.\n- Update the Settings nav description for Security from \"Authentication and\n network allowlist\" to authentication-focused copy.\n\n**Patterns to follow:**\n- Existing `settings-panel` row layout for Auth methods and Allowed usernames.\n\n**Test scenarios:**\n- Typecheck: the route compiles against the regenerated API client with no\n `server.ip_allowlist` property.\n- UI behavior: Security page still renders auth methods and allowed usernames.\n- Cleanup: no frontend references to `ip_allowlist`, `IP allowlist`, or\n `trusted_proxy_count` remain.\n\n**Verification:**\n- `apps/fabro-web` typecheck passes against the new generated client.\n\n- [ ] **Unit 5: Update tests, docs, changelog, and dependency lockfile**\n\n**Goal:** Remove stale references and document the operator-facing behavior\nchange.\n\n**Requirements:** R4, R6, R7\n\n**Dependencies:** Units 1 through 4\n\n**Files:**\n- Modify: `lib/crates/fabro-server/tests/it/api/routing.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/tcp.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/settings.rs`\n- Modify: `docs/public/administration/security.mdx`\n- Create or modify: `docs/public/changelog/2026-05-27.mdx`\n- Modify: `Cargo.lock` if dependency graph changes\n\n**Approach:**\n- Delete route/TCP tests that only prove IP allowlist enforcement or\n `ConnectInfo` behavior.\n- Adjust any router setup helpers after the signature simplification.\n- Add a settings API assertion that `server.ip_allowlist` is absent from\n `/api/v1/settings`.\n- Update security docs to explicitly state that Fabro does not provide inbound\n source-IP allowlisting and operators should use upstream network controls.\n- Add a changelog entry dated 2026-05-27 describing the hard removal and the\n expected replacement at the deployment layer.\n- Run dependency resolution after removing direct `ipnet`/test dependencies;\n keep transitive or unrelated `ipnet` entries needed by sandbox CIDR validation.\n\n**Patterns to follow:**\n- Existing changelog style in `docs/public/changelog/2026-05-26.mdx`.\n- Existing settings API integration test style in\n `lib/crates/fabro-server/tests/it/api/settings.rs`.\n\n**Test scenarios:**\n- Settings API: response contains server auth, listen, storage, scheduler, and\n integrations fields, but not `server.ip_allowlist`.\n- Config compatibility: old IP allowlist TOML fails before startup rather than\n being silently ignored.\n- Docs validation: security docs no longer imply Fabro can restrict inbound\n source IPs internally.\n\n**Verification:**\n- `rg -n \"ip_allowlist|trusted_proxy_count|github_meta_hooks|IP allowlist|ip allowlist\"`\n returns only historical archived plan/brainstorm/spec references or unrelated\n non-server allowlist text.\n\n## System-Wide Impact\n\n- **Public API:** `GET /api/v1/settings` response shape changes by removing\n `server.ip_allowlist` and webhook `ip_allowlist`.\n- **Config compatibility:** Existing `settings.toml` files containing the removed\n keys become invalid. This is intentional.\n- **Runtime security posture:** Fabro no longer blocks requests based on source\n IP. Operators must enforce network source restrictions upstream.\n- **Webhook handling:** GitHub webhook HMAC verification remains unchanged; only\n optional source-IP filtering is removed.\n- **Generated clients:** Downstream TypeScript/Rust consumers that read\n `server.ip_allowlist` must update.\n\n## Risks & Mitigations\n\n| Risk | Mitigation |\n|------|------------|\n| Operators accidentally expose a server that previously relied on Fabro IP allowlisting | Changelog and security docs explicitly call out the removal and direct operators to upstream controls. |\n| Generated API clients retain stale types | Update OpenAPI first, regenerate both Rust and TypeScript clients, and run search checks for stale symbols. |\n| Unrelated allowlist functionality is removed by broad search/replace | Scope searches to `IpAllow`, `ip_allowlist`, `trusted_proxy_count`, and `github_meta_hooks`; preserve sandbox CIDR and GitHub username allowlists. |\n| Router signature cleanup breaks many tests | Update shared test helpers first, then compile-driven cleanup of remaining call sites. |\n| `ipnet` is removed where still needed | Keep `fabro-config` dependency if environment CIDR validation still imports `ipnet::IpNet`. |\n\n## Test Plan\n\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun test`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n\n## Assumptions\n\n- The accepted compatibility policy is hard removal: no warning-only parser, no\n migration, and no custom compatibility error path.\n- Public API consumers can tolerate a breaking settings shape change for this\n removed feature.\n- Historical documents in `docs/plans/`, `docs/brainstorms/`, and\n `docs/superpowers/` are archival and do not need rewriting.\n\n## Sources & References\n\n- Previous feature requirements:\n `docs/brainstorms/2026-04-15-ip-whitelist-requirements.md`\n- Previous implementation plan:\n `docs/plans/2026-04-15-002-feat-ip-allowlist-plan.md`\n- API workflow guidance: `AGENTS.md`\n- OpenAPI source of truth: `docs/public/api-reference/fabro-api.yaml`\n",
|
|
"response.implement": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.integrations.github.webhooks.ip_allowlist]`\n - `IpAllowEntry` and related settings/types.\n- Deleted server runtime enforcement:\n - Removed `ip_allowlist` middleware/module.\n - Removed `IpAllowlistConfig`, client-IP extraction, trusted proxy handling, and GitHub `/meta` hook range resolution.\n - Simplified router/startup wiring and removed `ConnectInfo` serving requirements that only existed for allowlisting.\n- Preserved unrelated allowlists:\n - GitHub username allowlists remain.\n - Sandbox/Daytona CIDR allow lists remain.\n- Updated OpenAPI and generated clients:\n - Removed allowlist schemas/fields from `fabro-api.yaml`.\n - Regenerated Rust API and TypeScript API client.\n - Deleted stale generated TS allowlist model files.\n- Updated web UI:\n - Removed Settings > Security network allowlist display.\n - Adjusted Security page/nav copy.\n- Updated tests/docs:\n - Added unknown-field tests proving stale IP allowlist config is now rejected.\n - Added settings API assertions that removed fields are absent.\n - Updated security docs and changelog to direct operators to upstream network controls.\n- Updated dependency graph:\n - Removed direct `ipnet` deps from `fabro-types` and `fabro-server`; retained transitive/use in `fabro-config` for sandbox CIDR validation.\n\nRed/green TDD:\n- Added stale-config rejection tests and verified they failed before schema removal, then passed after removal.\n\nValidation run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo build -p fabro-cli --bin fabro`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server --no-fail-fast`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `git diff --check`\n- Stale-symbol search confirmed remaining `ip_allowlist`/IP allowlist references are only intentional docs/tests/assertions.\n\nNo commits were created.",
|
|
"internal.fidelity": "compact",
|
|
"current_node": "simplify_gpt",
|
|
"response.simplify_gpt": "Review complete.\n\nWhat I checked:\n- Diff against merge base `a992a7d76...`\n- Remaining references to removed IP allowlist symbols/settings outside archival/build output\n- Key config/server/API/web/docs changes for cleanup issues\n- Parallel review agents were launched for reuse, quality, and efficiency review; no actionable cleanup changes were needed.\n\nNo code changes were made during this review.\n\nVerification passed:\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n - 1372 passed\n- `cd apps/fabro-web && bun run typecheck`\n - passed\n- Cleanup search:\n - `rg` found no remaining non-archival references to `ip_allowlist`, `trusted_proxy_count`, `github_meta_hooks`, `IpAllow`, `GitHubMetaResolver`, etc. excluding `target/` and archival docs.\n\nWorking tree is clean.",
|
|
"internal.retry_count.preflight_compile": 0,
|
|
"response.simplify_opus": "## Review Summary\n\nAll three review agents came back essentially clean.\n\n- **Code Reuse Review** (e7156f0c): Clean. All new code follows existing local patterns (parse + `expect_err`, `body.get(x).is_none()`, `RouterOptions` destructure). No missed utilities.\n- **Code Quality Review** (59feb42b): Clean. Symbol audit confirms full removal of `IpAllowlistConfig`, `IpAllowlist`, `IpAllowEntry`, `LiteralIpAllowEntry`, `ip_allowlist_middleware`, `GitHubMetaResolver`, `github_meta_hooks`, `trusted_proxy_count`, `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, `extract_client_ip`, `GitHubMetaCache`, and frontend `ip_allowlist`/`Count`/`plural` wiring. `deny_unknown_fields` enforces hard removal at the TOML boundary. No stubs, dead imports, or vestigial parameters from the refactor.\n- **Efficiency Review** (51c928ad): Clean. Per-request IP allowlist middleware (two layers — main app and webhook) gone. Startup GitHub `/meta` fetch + on-disk cache gone. `ConnectInfo<SocketAddr>` plumbing removed from `serve.rs` (both Tcp and Unix variants) and from `tests/it/api/tcp.rs`. No new hot-path work introduced anywhere.\n\n**One out-of-scope finding skipped:** the efficiency agent flagged `install.rs:710` as still using `into_make_service_with_connect_info::<SocketAddr>()` despite nothing in `install.rs` extracting `ConnectInfo` (the `detect_remote_ip` helper only reads the `x-forwarded-for` header). I verified this is true — it's the last `ConnectInfo` caller in the crate — but it's pre-existing code in the install flow, not introduced by this refactor, and unrelated to IP allowlisting. The agent rated it low-severity cleanup. Per the scope discipline of this refactor (hard removal of IP allowlisting only, no scope creep) and the system prompt's \"only make changes that are directly requested or clearly necessary,\" I'm leaving it alone.\n\nNo fixes applied — the diff was already clean.",
|
|
"failure_class": "",
|
|
"thread.preflight_compile.current_node": "preflight_lint",
|
|
"thread.preflight_lint.current_node": "implement",
|
|
"thread.simplify_opus.current_node": "simplify_gpt",
|
|
"failure_signature": "",
|
|
"internal.retry_count.simplify_gpt": 0,
|
|
"internal.node_visit_count": 1,
|
|
"internal.retry_count.simplify_opus": 0,
|
|
"thread.start.current_node": "toolchain",
|
|
"internal.work_dir": "/home/daytona/workspace/fabro",
|
|
"internal.retry_count.preflight_lint": 0,
|
|
"thread.toolchain.current_node": "preflight_compile"
|
|
},
|
|
"node_outcomes": {
|
|
"implement": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"last_stage": "implement",
|
|
"last_response": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.int",
|
|
"response.implement": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.integrations.github.webhooks.ip_allowlist]`\n - `IpAllowEntry` and related settings/types.\n- Deleted server runtime enforcement:\n - Removed `ip_allowlist` middleware/module.\n - Removed `IpAllowlistConfig`, client-IP extraction, trusted proxy handling, and GitHub `/meta` hook range resolution.\n - Simplified router/startup wiring and removed `ConnectInfo` serving requirements that only existed for allowlisting.\n- Preserved unrelated allowlists:\n - GitHub username allowlists remain.\n - Sandbox/Daytona CIDR allow lists remain.\n- Updated OpenAPI and generated clients:\n - Removed allowlist schemas/fields from `fabro-api.yaml`.\n - Regenerated Rust API and TypeScript API client.\n - Deleted stale generated TS allowlist model files.\n- Updated web UI:\n - Removed Settings > Security network allowlist display.\n - Adjusted Security page/nav copy.\n- Updated tests/docs:\n - Added unknown-field tests proving stale IP allowlist config is now rejected.\n - Added settings API assertions that removed fields are absent.\n - Updated security docs and changelog to direct operators to upstream network controls.\n- Updated dependency graph:\n - Removed direct `ipnet` deps from `fabro-types` and `fabro-server`; retained transitive/use in `fabro-config` for sandbox CIDR validation.\n\nRed/green TDD:\n- Added stale-config rejection tests and verified they failed before schema removal, then passed after removal.\n\nValidation run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo build -p fabro-cli --bin fabro`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server --no-fail-fast`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `git diff --check`\n- Stale-symbol search confirmed remaining `ip_allowlist`/IP allowlist references are only intentional docs/tests/assertions.\n\nNo commits were created."
|
|
},
|
|
"notes": "Stage completed: implement",
|
|
"usage": {
|
|
"input": {
|
|
"usage": {
|
|
"model": {
|
|
"provider": "openai",
|
|
"model_id": "gpt-5.5"
|
|
},
|
|
"tokens": {
|
|
"input_tokens": 2556897,
|
|
"output_tokens": 26084,
|
|
"reasoning_tokens": 9280,
|
|
"cache_read_tokens": 17923072,
|
|
"cache_write_tokens": 0
|
|
}
|
|
},
|
|
"facts": {
|
|
"algorithm": "openai"
|
|
}
|
|
},
|
|
"total_usd_micros": 22806941
|
|
},
|
|
"files_touched": [
|
|
"/home/daytona/workspace/fabro/docs/public/changelog/2026-05-27.mdx"
|
|
],
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 1238818,
|
|
"tool_time_ms": 594726,
|
|
"active_time_ms": 1833544
|
|
}
|
|
},
|
|
"simplify_opus": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"response.simplify_opus": "## Review Summary\n\nAll three review agents came back essentially clean.\n\n- **Code Reuse Review** (e7156f0c): Clean. All new code follows existing local patterns (parse + `expect_err`, `body.get(x).is_none()`, `RouterOptions` destructure). No missed utilities.\n- **Code Quality Review** (59feb42b): Clean. Symbol audit confirms full removal of `IpAllowlistConfig`, `IpAllowlist`, `IpAllowEntry`, `LiteralIpAllowEntry`, `ip_allowlist_middleware`, `GitHubMetaResolver`, `github_meta_hooks`, `trusted_proxy_count`, `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, `extract_client_ip`, `GitHubMetaCache`, and frontend `ip_allowlist`/`Count`/`plural` wiring. `deny_unknown_fields` enforces hard removal at the TOML boundary. No stubs, dead imports, or vestigial parameters from the refactor.\n- **Efficiency Review** (51c928ad): Clean. Per-request IP allowlist middleware (two layers — main app and webhook) gone. Startup GitHub `/meta` fetch + on-disk cache gone. `ConnectInfo<SocketAddr>` plumbing removed from `serve.rs` (both Tcp and Unix variants) and from `tests/it/api/tcp.rs`. No new hot-path work introduced anywhere.\n\n**One out-of-scope finding skipped:** the efficiency agent flagged `install.rs:710` as still using `into_make_service_with_connect_info::<SocketAddr>()` despite nothing in `install.rs` extracting `ConnectInfo` (the `detect_remote_ip` helper only reads the `x-forwarded-for` header). I verified this is true — it's the last `ConnectInfo` caller in the crate — but it's pre-existing code in the install flow, not introduced by this refactor, and unrelated to IP allowlisting. The agent rated it low-severity cleanup. Per the scope discipline of this refactor (hard removal of IP allowlisting only, no scope creep) and the system prompt's \"only make changes that are directly requested or clearly necessary,\" I'm leaving it alone.\n\nNo fixes applied — the diff was already clean.",
|
|
"last_response": "## Review Summary\n\nAll three review agents came back essentially clean.\n\n- **Code Reuse Review** (e7156f0c): Clean. All new code follows existing local patterns (parse + `expect_err`, `body.get(x).is_",
|
|
"last_stage": "simplify_opus"
|
|
},
|
|
"notes": "Stage completed: simplify_opus",
|
|
"usage": {
|
|
"input": {
|
|
"usage": {
|
|
"model": {
|
|
"provider": "anthropic",
|
|
"model_id": "claude-opus-4-7"
|
|
},
|
|
"tokens": {
|
|
"input_tokens": 21129,
|
|
"output_tokens": 5543,
|
|
"reasoning_tokens": 0,
|
|
"cache_read_tokens": 251482,
|
|
"cache_write_tokens": 61150
|
|
}
|
|
},
|
|
"facts": {
|
|
"algorithm": "anthropic",
|
|
"cache_write_5m_tokens": 61150,
|
|
"cache_write_1h_tokens": 0
|
|
}
|
|
},
|
|
"total_usd_micros": 752148
|
|
},
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 83152,
|
|
"tool_time_ms": 271255,
|
|
"active_time_ms": 354407
|
|
}
|
|
},
|
|
"toolchain": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
|
},
|
|
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 1482,
|
|
"active_time_ms": 1482
|
|
}
|
|
},
|
|
"preflight_compile": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
|
},
|
|
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 135614,
|
|
"active_time_ms": 135614
|
|
}
|
|
},
|
|
"simplify_gpt": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"last_response": "Review complete.\n\nWhat I checked:\n- Diff against merge base `a992a7d76...`\n- Remaining references to removed IP allowlist symbols/settings outside archival/build output\n- Key config/server/API/web/doc",
|
|
"last_stage": "simplify_gpt",
|
|
"response.simplify_gpt": "Review complete.\n\nWhat I checked:\n- Diff against merge base `a992a7d76...`\n- Remaining references to removed IP allowlist symbols/settings outside archival/build output\n- Key config/server/API/web/docs changes for cleanup issues\n- Parallel review agents were launched for reuse, quality, and efficiency review; no actionable cleanup changes were needed.\n\nNo code changes were made during this review.\n\nVerification passed:\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n - 1372 passed\n- `cd apps/fabro-web && bun run typecheck`\n - passed\n- Cleanup search:\n - `rg` found no remaining non-archival references to `ip_allowlist`, `trusted_proxy_count`, `github_meta_hooks`, `IpAllow`, `GitHubMetaResolver`, etc. excluding `target/` and archival docs.\n\nWorking tree is clean."
|
|
},
|
|
"notes": "Stage completed: simplify_gpt",
|
|
"usage": {
|
|
"input": {
|
|
"usage": {
|
|
"model": {
|
|
"provider": "openai",
|
|
"model_id": "gpt-5.5"
|
|
},
|
|
"tokens": {
|
|
"input_tokens": 445377,
|
|
"output_tokens": 3232,
|
|
"reasoning_tokens": 1131,
|
|
"cache_read_tokens": 909312,
|
|
"cache_write_tokens": 0
|
|
}
|
|
},
|
|
"facts": {
|
|
"algorithm": "openai"
|
|
}
|
|
},
|
|
"total_usd_micros": 2812431
|
|
},
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 172391,
|
|
"tool_time_ms": 42057,
|
|
"active_time_ms": 214448
|
|
}
|
|
},
|
|
"start": {
|
|
"status": "succeeded",
|
|
"usage": null
|
|
},
|
|
"preflight_lint": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
|
},
|
|
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 142983,
|
|
"active_time_ms": 142983
|
|
}
|
|
}
|
|
},
|
|
"next_node_id": "verify",
|
|
"git_commit_sha": "0d99f030ace48d6d8b03845a7a2fd75b11e1a2c0",
|
|
"node_visits": {
|
|
"start": 1,
|
|
"preflight_compile": 1,
|
|
"simplify_gpt": 1,
|
|
"implement": 1,
|
|
"preflight_lint": 1,
|
|
"simplify_opus": 1,
|
|
"toolchain": 1
|
|
}
|
|
},
|
|
"diff": {
|
|
"summary": {
|
|
"files_changed": 43,
|
|
"additions": 178,
|
|
"deletions": 1631
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"seq": 0,
|
|
"checkpoint": {
|
|
"timestamp": "2026-05-28T01:02:42.286337Z",
|
|
"current_node": "verify",
|
|
"completed_nodes": [
|
|
"start",
|
|
"toolchain",
|
|
"preflight_compile",
|
|
"preflight_lint",
|
|
"implement",
|
|
"simplify_opus",
|
|
"simplify_gpt",
|
|
"verify"
|
|
],
|
|
"node_retries": {},
|
|
"context_values": {
|
|
"internal.retry_count.verify": 0,
|
|
"thread.start.current_node": "toolchain",
|
|
"failure_class": "deterministic",
|
|
"internal.work_dir": "/home/daytona/workspace/fabro",
|
|
"graph.goal": "---\ntitle: \"refactor: Remove IP allowlisting\"\ntype: refactor\nstatus: active\ndate: 2026-05-27\n---\n\n# refactor: Remove IP allowlisting\n\n## Overview\n\nRemove Fabro's inbound server IP allowlisting feature completely. This removes the\nserver-wide `[server.ip_allowlist]` setting, the GitHub webhook-specific\n`[server.integrations.github.webhooks.ip_allowlist]` overlay, request middleware,\nclient-IP extraction, GitHub `/meta` hook-range expansion, settings API exposure,\ngenerated API client types, and the Settings > Security UI display.\n\nExisting configs that still contain removed IP allowlist keys should fail as\nunknown fields. This is an intentional hard removal, not a compatibility\ndeprecation.\n\n## Problem Frame\n\nThe feature is being removed from Fabro rather than maintained as an in-process\nnetwork access-control layer. Network source restrictions should be handled\noutside Fabro by reverse proxies, firewalls, VPNs, Tailscale, platform ingress,\nor other deployment-layer controls.\n\n## Requirements Trace\n\n- R1. Remove all runtime enforcement of inbound source-IP allowlisting from web,\n API, static asset, and GitHub webhook routes.\n- R2. Remove the server settings schema for global and GitHub webhook IP\n allowlists.\n- R3. Remove `IpAllowEntry` and related API schema/client types from public\n settings payloads.\n- R4. Keep unrelated allowlists intact: GitHub username allowlists, sandbox\n egress CIDR allow lists, and internal policy/test allowlists.\n- R5. Preserve existing authentication, GitHub webhook HMAC verification,\n routing, health checks, request logging, and settings hot-reload behavior.\n- R6. Treat old IP allowlist config as invalid after removal.\n- R7. Update operator-facing docs/changelog so users know to move source-IP\n restrictions upstream.\n\n## Scope Boundaries\n\n- Do not remove `[server.auth.github].allowed_usernames`.\n- Do not remove Daytona/sandbox `cidr_allow_list` network policy support.\n- Do not remove generic uses of \"allowlist\" in policy tests, markdown rendering,\n model controls, or other unrelated domains.\n- Do not add a migration, warning-only parser, or fallback compatibility path for\n old IP allowlist settings.\n- Do not change GitHub webhook signature verification or webhook route strategy.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- Settings use sparse config layers in `lib/crates/fabro-config/src/layers/`\n and dense resolved types in `lib/crates/fabro-types/src/settings/`.\n- The OpenAPI spec at `docs/public/api-reference/fabro-api.yaml` is the source\n of truth for API wire shape; `cargo build -p fabro-api` regenerates Rust API\n code, and `cd lib/packages/fabro-api-client && bun run generate` regenerates\n the TypeScript client.\n- `lib/crates/fabro-server/src/server.rs` builds the router and currently wires\n IP allowlist middleware before other route dispatch behavior.\n- `lib/crates/fabro-server/src/serve.rs` currently resolves server and webhook\n allowlist configs at startup and creates a GitHub `/meta` resolver.\n- `apps/fabro-web/app/routes/settings-security.tsx` displays the settings API's\n `server.ip_allowlist` state.\n\n### Main Removal Surfaces\n\n- Config/types: `fabro-types`, `fabro-config`, config tests.\n- Server runtime: `fabro-server/src/ip_allowlist.rs`, router wiring, startup\n wiring, test helpers, routing/TCP tests.\n- API/contracts: OpenAPI schema, `fabro-api` type replacements/exports, generated\n TypeScript API client models.\n- Frontend/docs: Settings > Security copy, settings navigation copy, changelog.\n\n## Key Technical Decisions\n\n- **Hard removal:** Removed config keys should fail through existing\n `deny_unknown_fields` behavior. This keeps the change simple and makes stale\n deployment config visible immediately.\n- **Delete, do not stub:** Remove `IpAllowlistConfig` and middleware parameters\n instead of passing default empty configs through the router. Empty stubs would\n keep the feature shape alive and make future cleanup harder.\n- **Keep webhook auth unchanged:** GitHub webhook source-IP filtering goes away,\n but HMAC signature verification remains the security boundary for webhook\n payload authenticity.\n- **Generated clients follow OpenAPI:** Update the OpenAPI schemas first, then\n regenerate Rust and TypeScript API outputs rather than hand-editing generated\n client files except as a short-term cleanup if generation leaves stale exports.\n- **Docs mention upstream controls:** The changelog and security guidance should\n direct operators to network-layer controls, not a replacement Fabro setting.\n\n## Implementation Units\n\n- [ ] **Unit 1: Remove config schema and resolved types**\n\n**Goal:** Delete the IP allowlist settings contract from config parsing and dense\nserver settings.\n\n**Requirements:** R2, R3, R4, R6\n\n**Dependencies:** None\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/settings/server.rs`\n- Modify: `lib/crates/fabro-types/src/settings/mod.rs`\n- Modify: `lib/crates/fabro-types/Cargo.toml`\n- Modify: `lib/crates/fabro-config/src/layers/server.rs`\n- Modify: `lib/crates/fabro-config/src/layers/mod.rs`\n- Modify: `lib/crates/fabro-config/src/lib.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/server.rs`\n- Modify: `lib/crates/fabro-config/src/tests/resolve_server.rs`\n\n**Approach:**\n- Remove `ServerNamespace.ip_allowlist` and its `test_default()` population.\n- Delete `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, and\n `IpAllowEntry`.\n- Remove `ServerLayer.ip_allowlist`, `ServerIpAllowlistLayer`,\n `ServerIpAllowlistOverrideLayer`, and `IntegrationWebhooksLayer.ip_allowlist`.\n- Remove resolver functions and validation for global and webhook IP allowlists,\n including `github_meta_hooks` parsing and Unix socket trusted-proxy checks.\n- Remove `ipnet` from `fabro-types`. Keep `ipnet` in `fabro-config` because\n `resolve/environment.rs` still validates sandbox CIDR policy.\n- Replace old positive/negative IP allowlist config tests with unknown-field\n tests for `[server.ip_allowlist]` and\n `[server.integrations.github.webhooks.ip_allowlist]`.\n\n**Patterns to follow:**\n- Existing unknown-field tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`\n for retired server settings.\n- Existing `ServerLayer`/`ServerNamespace` layer-to-resolved pattern for removing\n a server subdomain cleanly.\n\n**Test scenarios:**\n- Error path: TOML with `[server.ip_allowlist]` fails parsing/resolution with an\n unknown-field diagnostic mentioning `ip_allowlist`.\n- Error path: TOML with `[server.integrations.github.webhooks.ip_allowlist]`\n fails with an unknown-field diagnostic mentioning `ip_allowlist`.\n- Happy path: minimal valid server settings still resolve without any\n `ip_allowlist` field.\n- Integration: serialized `ServerSettings` JSON no longer includes\n `server.ip_allowlist`.\n\n**Verification:**\n- `fabro-config` and `fabro-types` compile without removed IP allowlist symbols.\n- Config tests prove stale allowlist settings are rejected.\n\n- [ ] **Unit 2: Remove server middleware and startup resolution**\n\n**Goal:** Remove all runtime source-IP filtering and GitHub `/meta` range\nresolution from `fabro-server`.\n\n**Requirements:** R1, R4, R5\n\n**Dependencies:** Unit 1\n\n**Files:**\n- Delete: `lib/crates/fabro-server/src/ip_allowlist.rs`\n- Modify: `lib/crates/fabro-server/src/lib.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Modify: `lib/crates/fabro-server/src/serve.rs`\n- Modify: `lib/crates/fabro-server/src/test_support.rs`\n- Modify: `lib/crates/fabro-server/Cargo.toml`\n- Modify: `lib/crates/fabro-server/src/auth/translate.rs`\n- Modify: `lib/crates/fabro-server/src/web_auth.rs`\n- Modify: `lib/crates/fabro-cli/tests/it/support/auth_harness.rs`\n\n**Approach:**\n- Remove the public `ip_allowlist` module export.\n- Delete `IpAllowlistConfig`, `IpAllowlist`, `GitHubMetaResolver`, client-IP\n extraction, middleware, and GitHub meta cache helpers.\n- Simplify `build_router_with_options` by removing its `Arc<IpAllowlistConfig>`\n parameter and removing `RouterOptions.github_webhook_ip_allowlist`.\n- Remove the global allowlist middleware layer from the main app router.\n- Simplify `github_webhook_routes` so it only receives the webhook secret and no\n route-specific allowlist config.\n- Remove startup creation of `GitHubMetaResolver`, `default_ip_allowlist`,\n `resolve_github_webhook_ip_allowlist`, and\n `resolve_startup_github_webhook_ip_allowlist`.\n- Replace all test/helper call sites that pass `IpAllowlistConfig::default()`\n with the simplified router signature.\n- Remove `ipnet` and any now-unused HTTP mocking/test-only dependencies from\n `fabro-server` if they are only used by the deleted module.\n- Consider whether `serve.rs` still needs `SocketAddr` for TCP\n `ConnectInfo`; if it was only present for allowlisting tests, remove the\n connect-info service wrapper and imports.\n\n**Patterns to follow:**\n- Keep router layers ordered as they are after the allowlist layer is removed:\n auth translation, demo routing, auth extension, canonical host, security\n headers, HTTP logging, request ID.\n- Existing webhook route HMAC tests and auth tests should remain the source of\n truth for webhook behavior.\n\n**Test scenarios:**\n- Happy path: API requests from any TCP remote address route normally when auth\n requirements are satisfied.\n- Happy path: `/health` remains accessible.\n- Integration: GitHub webhook route still rejects missing/invalid signatures and\n accepts valid signatures exactly as before.\n- Cleanup: there are no references to `IpAllowlistConfig`, `ip_allowlist_middleware`,\n `GitHubMetaResolver`, `github_meta_hooks`, or `github-meta-hooks.json`.\n\n**Verification:**\n- `fabro-server` and CLI auth harness tests compile against the simplified router\n API.\n- Runtime startup no longer performs any GitHub `/meta` fetch for webhook IP\n ranges.\n\n- [ ] **Unit 3: Update OpenAPI and generated API clients**\n\n**Goal:** Remove IP allowlist fields and schemas from public settings API\ncontracts and regenerated clients.\n\n**Requirements:** R3, R4\n\n**Dependencies:** Units 1 and 2\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs`\n- Modify/generated: `lib/packages/fabro-api-client/src/models/*`\n\n**Approach:**\n- Remove `ip_allowlist` from `ServerNamespace.required` and\n `ServerNamespace.properties`.\n- Remove `ServerIpAllowlistSettings`,\n `ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,\n `LiteralIpAllowEntry`, and `GitHubMetaHooksEntry` schemas.\n- Remove `IntegrationWebhooksSettings.ip_allowlist` from required/properties.\n- Remove `with_replacement` entries and public re-exports for removed types in\n `fabro-api`.\n- Regenerate Rust API code by building `fabro-api`.\n- Regenerate TypeScript Axios client and ensure stale allowlist model files and\n index exports are gone.\n- Strengthen round-trip tests to assert settings JSON omits\n `server.ip_allowlist` and webhook `ip_allowlist`.\n\n**Patterns to follow:**\n- Existing OpenAPI-first workflow in `AGENTS.md`.\n- Existing `server_settings_family_reuses_domain_types` assertions for shared\n domain type identity.\n\n**Test scenarios:**\n- Contract: `ServerSettings` round-trips through API types without any IP\n allowlist field.\n- Contract: OpenAPI-generated TypeScript `ServerNamespace` has no\n `ip_allowlist` property.\n- Contract: `IntegrationWebhooksSettings` has only webhook strategy fields after\n removal.\n\n**Verification:**\n- Generated Rust and TypeScript clients match the updated OpenAPI spec.\n- `rg \"ServerIpAllowlist|IpAllowEntry|server-ip-allowlist|literal-ip-allow-entry\"`\n finds no remaining generated/public API references.\n\n- [ ] **Unit 4: Update web settings UI**\n\n**Goal:** Remove IP allowlist display from Settings > Security and align copy\nwith the new settings shape.\n\n**Requirements:** R3, R7\n\n**Dependencies:** Unit 3\n\n**Files:**\n- Modify: `apps/fabro-web/app/routes/settings-security.tsx`\n- Modify: `apps/fabro-web/app/routes/settings.tsx`\n\n**Approach:**\n- Change the Security page description from \"Authentication methods and network\n allowlist\" to authentication-only wording.\n- Remove destructuring and rendering of `settings.server.ip_allowlist`.\n- Remove the unused `Count` and `plural` imports if no longer used.\n- Update the Settings nav description for Security from \"Authentication and\n network allowlist\" to authentication-focused copy.\n\n**Patterns to follow:**\n- Existing `settings-panel` row layout for Auth methods and Allowed usernames.\n\n**Test scenarios:**\n- Typecheck: the route compiles against the regenerated API client with no\n `server.ip_allowlist` property.\n- UI behavior: Security page still renders auth methods and allowed usernames.\n- Cleanup: no frontend references to `ip_allowlist`, `IP allowlist`, or\n `trusted_proxy_count` remain.\n\n**Verification:**\n- `apps/fabro-web` typecheck passes against the new generated client.\n\n- [ ] **Unit 5: Update tests, docs, changelog, and dependency lockfile**\n\n**Goal:** Remove stale references and document the operator-facing behavior\nchange.\n\n**Requirements:** R4, R6, R7\n\n**Dependencies:** Units 1 through 4\n\n**Files:**\n- Modify: `lib/crates/fabro-server/tests/it/api/routing.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/tcp.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/settings.rs`\n- Modify: `docs/public/administration/security.mdx`\n- Create or modify: `docs/public/changelog/2026-05-27.mdx`\n- Modify: `Cargo.lock` if dependency graph changes\n\n**Approach:**\n- Delete route/TCP tests that only prove IP allowlist enforcement or\n `ConnectInfo` behavior.\n- Adjust any router setup helpers after the signature simplification.\n- Add a settings API assertion that `server.ip_allowlist` is absent from\n `/api/v1/settings`.\n- Update security docs to explicitly state that Fabro does not provide inbound\n source-IP allowlisting and operators should use upstream network controls.\n- Add a changelog entry dated 2026-05-27 describing the hard removal and the\n expected replacement at the deployment layer.\n- Run dependency resolution after removing direct `ipnet`/test dependencies;\n keep transitive or unrelated `ipnet` entries needed by sandbox CIDR validation.\n\n**Patterns to follow:**\n- Existing changelog style in `docs/public/changelog/2026-05-26.mdx`.\n- Existing settings API integration test style in\n `lib/crates/fabro-server/tests/it/api/settings.rs`.\n\n**Test scenarios:**\n- Settings API: response contains server auth, listen, storage, scheduler, and\n integrations fields, but not `server.ip_allowlist`.\n- Config compatibility: old IP allowlist TOML fails before startup rather than\n being silently ignored.\n- Docs validation: security docs no longer imply Fabro can restrict inbound\n source IPs internally.\n\n**Verification:**\n- `rg -n \"ip_allowlist|trusted_proxy_count|github_meta_hooks|IP allowlist|ip allowlist\"`\n returns only historical archived plan/brainstorm/spec references or unrelated\n non-server allowlist text.\n\n## System-Wide Impact\n\n- **Public API:** `GET /api/v1/settings` response shape changes by removing\n `server.ip_allowlist` and webhook `ip_allowlist`.\n- **Config compatibility:** Existing `settings.toml` files containing the removed\n keys become invalid. This is intentional.\n- **Runtime security posture:** Fabro no longer blocks requests based on source\n IP. Operators must enforce network source restrictions upstream.\n- **Webhook handling:** GitHub webhook HMAC verification remains unchanged; only\n optional source-IP filtering is removed.\n- **Generated clients:** Downstream TypeScript/Rust consumers that read\n `server.ip_allowlist` must update.\n\n## Risks & Mitigations\n\n| Risk | Mitigation |\n|------|------------|\n| Operators accidentally expose a server that previously relied on Fabro IP allowlisting | Changelog and security docs explicitly call out the removal and direct operators to upstream controls. |\n| Generated API clients retain stale types | Update OpenAPI first, regenerate both Rust and TypeScript clients, and run search checks for stale symbols. |\n| Unrelated allowlist functionality is removed by broad search/replace | Scope searches to `IpAllow`, `ip_allowlist`, `trusted_proxy_count`, and `github_meta_hooks`; preserve sandbox CIDR and GitHub username allowlists. |\n| Router signature cleanup breaks many tests | Update shared test helpers first, then compile-driven cleanup of remaining call sites. |\n| `ipnet` is removed where still needed | Keep `fabro-config` dependency if environment CIDR validation still imports `ipnet::IpNet`. |\n\n## Test Plan\n\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun test`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n\n## Assumptions\n\n- The accepted compatibility policy is hard removal: no warning-only parser, no\n migration, and no custom compatibility error path.\n- Public API consumers can tolerate a breaking settings shape change for this\n removed feature.\n- Historical documents in `docs/plans/`, `docs/brainstorms/`, and\n `docs/superpowers/` are archival and do not need rewriting.\n\n## Sources & References\n\n- Previous feature requirements:\n `docs/brainstorms/2026-04-15-ip-whitelist-requirements.md`\n- Previous implementation plan:\n `docs/plans/2026-04-15-002-feat-ip-allowlist-plan.md`\n- API workflow guidance: `AGENTS.md`\n- OpenAPI source of truth: `docs/public/api-reference/fabro-api.yaml`\n",
|
|
"failure_signature": "verify|deterministic|script failed with exit code: <n> ## output from https://github.com/fabro-sh/fabro * branch main -> fetch_head <hex>..<hex> main -> origin/main auto-merging cargo.lock auto-merging lib/crates/fabro-api/build.rs auto-merging lib/crates/fabro",
|
|
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
|
"internal.node_visit_count": 1,
|
|
"internal.retry_count.preflight_compile": 0,
|
|
"internal.retry_count.preflight_lint": 0,
|
|
"command.output": "blob://sha256/ba184126dad761158723a25e15818b2cb0ac7327ad06fa7513f7a7ce9377eee6",
|
|
"response.simplify_opus": "## Review Summary\n\nAll three review agents came back essentially clean.\n\n- **Code Reuse Review** (e7156f0c): Clean. All new code follows existing local patterns (parse + `expect_err`, `body.get(x).is_none()`, `RouterOptions` destructure). No missed utilities.\n- **Code Quality Review** (59feb42b): Clean. Symbol audit confirms full removal of `IpAllowlistConfig`, `IpAllowlist`, `IpAllowEntry`, `LiteralIpAllowEntry`, `ip_allowlist_middleware`, `GitHubMetaResolver`, `github_meta_hooks`, `trusted_proxy_count`, `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, `extract_client_ip`, `GitHubMetaCache`, and frontend `ip_allowlist`/`Count`/`plural` wiring. `deny_unknown_fields` enforces hard removal at the TOML boundary. No stubs, dead imports, or vestigial parameters from the refactor.\n- **Efficiency Review** (51c928ad): Clean. Per-request IP allowlist middleware (two layers — main app and webhook) gone. Startup GitHub `/meta` fetch + on-disk cache gone. `ConnectInfo<SocketAddr>` plumbing removed from `serve.rs` (both Tcp and Unix variants) and from `tests/it/api/tcp.rs`. No new hot-path work introduced anywhere.\n\n**One out-of-scope finding skipped:** the efficiency agent flagged `install.rs:710` as still using `into_make_service_with_connect_info::<SocketAddr>()` despite nothing in `install.rs` extracting `ConnectInfo` (the `detect_remote_ip` helper only reads the `x-forwarded-for` header). I verified this is true — it's the last `ConnectInfo` caller in the crate — but it's pre-existing code in the install flow, not introduced by this refactor, and unrelated to IP allowlisting. The agent rated it low-severity cleanup. Per the scope discipline of this refactor (hard removal of IP allowlisting only, no scope creep) and the system prompt's \"only make changes that are directly requested or clearly necessary,\" I'm leaving it alone.\n\nNo fixes applied — the diff was already clean.",
|
|
"outcome": "failed",
|
|
"last_response": "Review complete.\n\nWhat I checked:\n- Diff against merge base `a992a7d76...`\n- Remaining references to removed IP allowlist symbols/settings outside archival/build output\n- Key config/server/API/web/doc",
|
|
"current_node": "verify",
|
|
"internal.retry_count.simplify_opus": 0,
|
|
"last_stage": "simplify_gpt",
|
|
"internal.retry_count.implement": 0,
|
|
"thread.implement.current_node": "simplify_opus",
|
|
"internal.run_id": "01KSNYVQCKXPCSCEGWMZ1Q574Z",
|
|
"thread.simplify_gpt.current_node": "verify",
|
|
"internal.retry_count.start": 0,
|
|
"internal.fidelity": "compact",
|
|
"graph.rankdir": "LR",
|
|
"thread.preflight_compile.current_node": "preflight_lint",
|
|
"thread.preflight_lint.current_node": "implement",
|
|
"internal.retry_count.toolchain": 0,
|
|
"response.implement": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.integrations.github.webhooks.ip_allowlist]`\n - `IpAllowEntry` and related settings/types.\n- Deleted server runtime enforcement:\n - Removed `ip_allowlist` middleware/module.\n - Removed `IpAllowlistConfig`, client-IP extraction, trusted proxy handling, and GitHub `/meta` hook range resolution.\n - Simplified router/startup wiring and removed `ConnectInfo` serving requirements that only existed for allowlisting.\n- Preserved unrelated allowlists:\n - GitHub username allowlists remain.\n - Sandbox/Daytona CIDR allow lists remain.\n- Updated OpenAPI and generated clients:\n - Removed allowlist schemas/fields from `fabro-api.yaml`.\n - Regenerated Rust API and TypeScript API client.\n - Deleted stale generated TS allowlist model files.\n- Updated web UI:\n - Removed Settings > Security network allowlist display.\n - Adjusted Security page/nav copy.\n- Updated tests/docs:\n - Added unknown-field tests proving stale IP allowlist config is now rejected.\n - Added settings API assertions that removed fields are absent.\n - Updated security docs and changelog to direct operators to upstream network controls.\n- Updated dependency graph:\n - Removed direct `ipnet` deps from `fabro-types` and `fabro-server`; retained transitive/use in `fabro-config` for sandbox CIDR validation.\n\nRed/green TDD:\n- Added stale-config rejection tests and verified they failed before schema removal, then passed after removal.\n\nValidation run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo build -p fabro-cli --bin fabro`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server --no-fail-fast`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `git diff --check`\n- Stale-symbol search confirmed remaining `ip_allowlist`/IP allowlist references are only intentional docs/tests/assertions.\n\nNo commits were created.",
|
|
"thread.toolchain.current_node": "preflight_compile",
|
|
"response.simplify_gpt": "Review complete.\n\nWhat I checked:\n- Diff against merge base `a992a7d76...`\n- Remaining references to removed IP allowlist symbols/settings outside archival/build output\n- Key config/server/API/web/docs changes for cleanup issues\n- Parallel review agents were launched for reuse, quality, and efficiency review; no actionable cleanup changes were needed.\n\nNo code changes were made during this review.\n\nVerification passed:\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n - 1372 passed\n- `cd apps/fabro-web && bun run typecheck`\n - passed\n- Cleanup search:\n - `rg` found no remaining non-archival references to `ip_allowlist`, `trusted_proxy_count`, `github_meta_hooks`, `IpAllow`, `GitHubMetaResolver`, etc. excluding `target/` and archival docs.\n\nWorking tree is clean.",
|
|
"internal.retry_count.simplify_gpt": 0,
|
|
"internal.thread_id": "simplify_gpt",
|
|
"thread.simplify_opus.current_node": "simplify_gpt"
|
|
},
|
|
"node_outcomes": {
|
|
"preflight_lint": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
|
},
|
|
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 142983,
|
|
"active_time_ms": 142983
|
|
}
|
|
},
|
|
"simplify_opus": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"response.simplify_opus": "## Review Summary\n\nAll three review agents came back essentially clean.\n\n- **Code Reuse Review** (e7156f0c): Clean. All new code follows existing local patterns (parse + `expect_err`, `body.get(x).is_none()`, `RouterOptions` destructure). No missed utilities.\n- **Code Quality Review** (59feb42b): Clean. Symbol audit confirms full removal of `IpAllowlistConfig`, `IpAllowlist`, `IpAllowEntry`, `LiteralIpAllowEntry`, `ip_allowlist_middleware`, `GitHubMetaResolver`, `github_meta_hooks`, `trusted_proxy_count`, `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`, `extract_client_ip`, `GitHubMetaCache`, and frontend `ip_allowlist`/`Count`/`plural` wiring. `deny_unknown_fields` enforces hard removal at the TOML boundary. No stubs, dead imports, or vestigial parameters from the refactor.\n- **Efficiency Review** (51c928ad): Clean. Per-request IP allowlist middleware (two layers — main app and webhook) gone. Startup GitHub `/meta` fetch + on-disk cache gone. `ConnectInfo<SocketAddr>` plumbing removed from `serve.rs` (both Tcp and Unix variants) and from `tests/it/api/tcp.rs`. No new hot-path work introduced anywhere.\n\n**One out-of-scope finding skipped:** the efficiency agent flagged `install.rs:710` as still using `into_make_service_with_connect_info::<SocketAddr>()` despite nothing in `install.rs` extracting `ConnectInfo` (the `detect_remote_ip` helper only reads the `x-forwarded-for` header). I verified this is true — it's the last `ConnectInfo` caller in the crate — but it's pre-existing code in the install flow, not introduced by this refactor, and unrelated to IP allowlisting. The agent rated it low-severity cleanup. Per the scope discipline of this refactor (hard removal of IP allowlisting only, no scope creep) and the system prompt's \"only make changes that are directly requested or clearly necessary,\" I'm leaving it alone.\n\nNo fixes applied — the diff was already clean.",
|
|
"last_response": "## Review Summary\n\nAll three review agents came back essentially clean.\n\n- **Code Reuse Review** (e7156f0c): Clean. All new code follows existing local patterns (parse + `expect_err`, `body.get(x).is_",
|
|
"last_stage": "simplify_opus"
|
|
},
|
|
"notes": "Stage completed: simplify_opus",
|
|
"usage": {
|
|
"input": {
|
|
"usage": {
|
|
"model": {
|
|
"provider": "anthropic",
|
|
"model_id": "claude-opus-4-7"
|
|
},
|
|
"tokens": {
|
|
"input_tokens": 21129,
|
|
"output_tokens": 5543,
|
|
"reasoning_tokens": 0,
|
|
"cache_read_tokens": 251482,
|
|
"cache_write_tokens": 61150
|
|
}
|
|
},
|
|
"facts": {
|
|
"algorithm": "anthropic",
|
|
"cache_write_5m_tokens": 61150,
|
|
"cache_write_1h_tokens": 0
|
|
}
|
|
},
|
|
"total_usd_micros": 752148
|
|
},
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 83152,
|
|
"tool_time_ms": 271255,
|
|
"active_time_ms": 354407
|
|
}
|
|
},
|
|
"verify": {
|
|
"status": "failed",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/ba184126dad761158723a25e15818b2cb0ac7327ad06fa7513f7a7ce9377eee6"
|
|
},
|
|
"failure": {
|
|
"message": "Script failed with exit code: 1\n\n## output\nFrom https://github.com/fabro-sh/fabro\n * branch main -> FETCH_HEAD\n a992a7d76..475b4ab65 main -> origin/main\nAuto-merging Cargo.lock\nAuto-merging lib/crates/fabro-api/build.rs\nAuto-merging lib/crates/fabro-api/src/lib.rs\nAuto-merging lib/crates/fabro-server/Cargo.toml\nAuto-merging lib/crates/fabro-server/src/lib.rs\nAuto-merging lib/crates/fabro-server/src/serve.rs\nAuto-merging lib/crates/fabro-server/src/server.rs\nAuto-merging lib/crates/fabro-server/src/server/tests.rs\nAuto-merging lib/crates/fabro-server/src/test_support.rs\nCONFLICT (content): Merge conflict in lib/crates/fabro-server/src/test_support.rs\nAutomatic merge failed; fix conflicts and then commit the result.\n",
|
|
"category": "deterministic"
|
|
},
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 1604,
|
|
"active_time_ms": 1604
|
|
}
|
|
},
|
|
"simplify_gpt": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"last_response": "Review complete.\n\nWhat I checked:\n- Diff against merge base `a992a7d76...`\n- Remaining references to removed IP allowlist symbols/settings outside archival/build output\n- Key config/server/API/web/doc",
|
|
"last_stage": "simplify_gpt",
|
|
"response.simplify_gpt": "Review complete.\n\nWhat I checked:\n- Diff against merge base `a992a7d76...`\n- Remaining references to removed IP allowlist symbols/settings outside archival/build output\n- Key config/server/API/web/docs changes for cleanup issues\n- Parallel review agents were launched for reuse, quality, and efficiency review; no actionable cleanup changes were needed.\n\nNo code changes were made during this review.\n\nVerification passed:\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server`\n - 1372 passed\n- `cd apps/fabro-web && bun run typecheck`\n - passed\n- Cleanup search:\n - `rg` found no remaining non-archival references to `ip_allowlist`, `trusted_proxy_count`, `github_meta_hooks`, `IpAllow`, `GitHubMetaResolver`, etc. excluding `target/` and archival docs.\n\nWorking tree is clean."
|
|
},
|
|
"notes": "Stage completed: simplify_gpt",
|
|
"usage": {
|
|
"input": {
|
|
"usage": {
|
|
"model": {
|
|
"provider": "openai",
|
|
"model_id": "gpt-5.5"
|
|
},
|
|
"tokens": {
|
|
"input_tokens": 445377,
|
|
"output_tokens": 3232,
|
|
"reasoning_tokens": 1131,
|
|
"cache_read_tokens": 909312,
|
|
"cache_write_tokens": 0
|
|
}
|
|
},
|
|
"facts": {
|
|
"algorithm": "openai"
|
|
}
|
|
},
|
|
"total_usd_micros": 2812431
|
|
},
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 172391,
|
|
"tool_time_ms": 42057,
|
|
"active_time_ms": 214448
|
|
}
|
|
},
|
|
"toolchain": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
|
},
|
|
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 1482,
|
|
"active_time_ms": 1482
|
|
}
|
|
},
|
|
"implement": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"last_stage": "implement",
|
|
"last_response": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.int",
|
|
"response.implement": "Implemented the full IP allowlisting removal refactor.\n\nSummary:\n- Removed inbound source-IP allowlist config from dense settings and sparse config layers:\n - `[server.ip_allowlist]`\n - `[server.integrations.github.webhooks.ip_allowlist]`\n - `IpAllowEntry` and related settings/types.\n- Deleted server runtime enforcement:\n - Removed `ip_allowlist` middleware/module.\n - Removed `IpAllowlistConfig`, client-IP extraction, trusted proxy handling, and GitHub `/meta` hook range resolution.\n - Simplified router/startup wiring and removed `ConnectInfo` serving requirements that only existed for allowlisting.\n- Preserved unrelated allowlists:\n - GitHub username allowlists remain.\n - Sandbox/Daytona CIDR allow lists remain.\n- Updated OpenAPI and generated clients:\n - Removed allowlist schemas/fields from `fabro-api.yaml`.\n - Regenerated Rust API and TypeScript API client.\n - Deleted stale generated TS allowlist model files.\n- Updated web UI:\n - Removed Settings > Security network allowlist display.\n - Adjusted Security page/nav copy.\n- Updated tests/docs:\n - Added unknown-field tests proving stale IP allowlist config is now rejected.\n - Added settings API assertions that removed fields are absent.\n - Updated security docs and changelog to direct operators to upstream network controls.\n- Updated dependency graph:\n - Removed direct `ipnet` deps from `fabro-types` and `fabro-server`; retained transitive/use in `fabro-config` for sandbox CIDR validation.\n\nRed/green TDD:\n- Added stale-config rejection tests and verified they failed before schema removal, then passed after removal.\n\nValidation run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo build -p fabro-cli --bin fabro`\n- `cargo nextest run -p fabro-config -p fabro-types -p fabro-api -p fabro-server --no-fail-fast`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `git diff --check`\n- Stale-symbol search confirmed remaining `ip_allowlist`/IP allowlist references are only intentional docs/tests/assertions.\n\nNo commits were created."
|
|
},
|
|
"notes": "Stage completed: implement",
|
|
"usage": {
|
|
"input": {
|
|
"usage": {
|
|
"model": {
|
|
"provider": "openai",
|
|
"model_id": "gpt-5.5"
|
|
},
|
|
"tokens": {
|
|
"input_tokens": 2556897,
|
|
"output_tokens": 26084,
|
|
"reasoning_tokens": 9280,
|
|
"cache_read_tokens": 17923072,
|
|
"cache_write_tokens": 0
|
|
}
|
|
},
|
|
"facts": {
|
|
"algorithm": "openai"
|
|
}
|
|
},
|
|
"total_usd_micros": 22806941
|
|
},
|
|
"files_touched": [
|
|
"/home/daytona/workspace/fabro/docs/public/changelog/2026-05-27.mdx"
|
|
],
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 1238818,
|
|
"tool_time_ms": 594726,
|
|
"active_time_ms": 1833544
|
|
}
|
|
},
|
|
"start": {
|
|
"status": "succeeded",
|
|
"usage": null
|
|
},
|
|
"preflight_compile": {
|
|
"status": "succeeded",
|
|
"context_updates": {
|
|
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
|
},
|
|
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
|
"usage": null,
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 135614,
|
|
"active_time_ms": 135614
|
|
}
|
|
}
|
|
},
|
|
"next_node_id": "fixup",
|
|
"node_visits": {
|
|
"start": 1,
|
|
"implement": 1,
|
|
"toolchain": 1,
|
|
"preflight_compile": 1,
|
|
"simplify_gpt": 1,
|
|
"verify": 1,
|
|
"preflight_lint": 1,
|
|
"simplify_opus": 1
|
|
}
|
|
},
|
|
"diff": {}
|
|
}
|
|
],
|
|
"conclusion": null,
|
|
"sandbox": {
|
|
"kind": "ready",
|
|
"plan": {
|
|
"provider": "daytona"
|
|
},
|
|
"instance": {
|
|
"provider": "daytona",
|
|
"snapshot": "fabro-fdb28dec-1233-892c-b9d7-9f88f8353e7a",
|
|
"runtime": {
|
|
"id": "fabro-01KSNYVQCKXPCSCEGWMZ1Q574Z",
|
|
"working_directory": "/home/daytona/workspace/fabro",
|
|
"repo_cloned": true,
|
|
"clone_origin_url": "https://github.com/fabro-sh/fabro",
|
|
"clone_branch": "main",
|
|
"workspace_root": "/home/daytona/workspace",
|
|
"repos_root": "/home/daytona/repos",
|
|
"primary_repo_path": "/home/daytona/repos/fabro-sh/fabro",
|
|
"primary_repo_link": "/home/daytona/workspace/fabro"
|
|
}
|
|
}
|
|
},
|
|
"pull_request": null,
|
|
"superseded_by": null,
|
|
"retried_from": "01KSNHK17CQR4401HJRP000BNW",
|
|
"pending_interviews": {},
|
|
"stages": {
|
|
"start@1": {
|
|
"first_event_seq": 17,
|
|
"prompt": null,
|
|
"response": null,
|
|
"completion": {
|
|
"outcome": "succeeded",
|
|
"notes": null,
|
|
"failure_reason": null,
|
|
"timestamp": "2026-05-28T00:14:00.981746Z"
|
|
},
|
|
"provider_used": null,
|
|
"diff": null,
|
|
"script_invocation": null,
|
|
"script_timing": null,
|
|
"parallel_results": null,
|
|
"output": null,
|
|
"started_at": "2026-05-28T00:14:00.981310Z",
|
|
"handler": "start",
|
|
"timing": {
|
|
"wall_time_ms": 0,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 0,
|
|
"active_time_ms": 0
|
|
},
|
|
"usage": {
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"total_tokens": 0,
|
|
"reasoning_tokens": 0,
|
|
"cache_read_tokens": 0,
|
|
"cache_write_tokens": 0
|
|
},
|
|
"state": "succeeded"
|
|
},
|
|
"implement@1": {
|
|
"first_event_seq": 51,
|
|
"prompt": null,
|
|
"response": null,
|
|
"completion": {
|
|
"outcome": "succeeded",
|
|
"notes": "Stage completed: implement",
|
|
"failure_reason": null,
|
|
"timestamp": "2026-05-28T00:52:56.817979Z"
|
|
},
|
|
"provider_used": {
|
|
"mode": "agent",
|
|
"provider": "openai",
|
|
"model": "gpt-5.5",
|
|
"reasoning_effort": "xhigh"
|
|
},
|
|
"diff": null,
|
|
"script_invocation": null,
|
|
"script_timing": null,
|
|
"parallel_results": null,
|
|
"output": null,
|
|
"started_at": "2026-05-28T00:19:01.909610Z",
|
|
"handler": "agent",
|
|
"timing": {
|
|
"wall_time_ms": 2034891,
|
|
"inference_time_ms": 1238818,
|
|
"tool_time_ms": 594726,
|
|
"active_time_ms": 1833544
|
|
},
|
|
"usage": {
|
|
"input_tokens": 2556897,
|
|
"output_tokens": 26084,
|
|
"total_tokens": 20515333,
|
|
"reasoning_tokens": 9280,
|
|
"cache_read_tokens": 17923072,
|
|
"cache_write_tokens": 0,
|
|
"total_usd_micros": 22806941
|
|
},
|
|
"model": {
|
|
"provider": "openai",
|
|
"model_id": "gpt-5.5"
|
|
},
|
|
"todos": {
|
|
"kind": "openai_plan",
|
|
"list_id": "openai_plan:be67232e-29cc-403c-ae14-84e1292cc798",
|
|
"items": [
|
|
{
|
|
"id": "93a1f33a92044523",
|
|
"status": "completed",
|
|
"order": 0,
|
|
"subject": "Remove config schema and resolved types"
|
|
},
|
|
{
|
|
"id": "9c1c39a1c54a1964",
|
|
"status": "completed",
|
|
"order": 1,
|
|
"subject": "Remove server middleware and startup resolution"
|
|
},
|
|
{
|
|
"id": "c40037d2544ef745",
|
|
"status": "completed",
|
|
"order": 2,
|
|
"subject": "Update OpenAPI and generated API clients"
|
|
},
|
|
{
|
|
"id": "f36afa2e69977883",
|
|
"status": "completed",
|
|
"order": 3,
|
|
"subject": "Update web settings UI"
|
|
},
|
|
{
|
|
"id": "a0e7d85e34e3da02",
|
|
"status": "completed",
|
|
"order": 4,
|
|
"subject": "Update tests, docs, changelog, and dependency lockfile"
|
|
},
|
|
{
|
|
"id": "62a37aa43f82a6c3",
|
|
"status": "completed",
|
|
"order": 5,
|
|
"subject": "Run final verification and review diff"
|
|
}
|
|
]
|
|
},
|
|
"permission_level": "full",
|
|
"agent_tools": [
|
|
{
|
|
"name": "apply_patch",
|
|
"description": "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "write",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "close_agent",
|
|
"description": "Close a running subagent that is no longer needed.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "subagent",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "glob",
|
|
"description": "Find files by file names using a glob pattern. Use path to choose the search root. Prefer this over shell find or ls when locating repository files.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "read",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "grep",
|
|
"description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "read",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "read_file",
|
|
"description": "Read files before editing them. Returns line-numbered text and supports offset/limit for large files. Use this instead of shell cat, head, tail, or sed when inspecting repository files.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "read",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "request_user_input",
|
|
"description": "Ask the human one or more questions and wait for their answers before continuing this stage.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "send_input",
|
|
"description": "Send a follow-up message to a running subagent when new information or corrected instructions are needed.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "subagent",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "shell",
|
|
"description": "Execute shell commands for terminal operations, package managers, tests and builds. Use dedicated tools for file reads, file edits, filename searches, and content searches. Provide timeout_ms for long-running commands.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "shell",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "spawn_agent",
|
|
"description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "subagent",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "update_plan",
|
|
"description": "Update the multi-step plan for the current task. Submit the entire plan; existing steps are reconciled by exact step text.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "wait",
|
|
"description": "Wait for a subagent to complete, then use the result to synthesize the outcome for the user.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "subagent",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "web_fetch",
|
|
"description": "Fetch content from a URL that starts with http:// or https://. Pass a prompt to extract specific information or summarize the page; omit prompt to return the page content.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "web_search",
|
|
"description": "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "write_file",
|
|
"description": "Create new files, or overwrite an existing file only when replacement is explicitly intended. Prefer edit_file for targeted changes to existing files because write_file overwrites the full file content.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "write",
|
|
"invoked": true
|
|
}
|
|
],
|
|
"context_window": {
|
|
"provider": "openai",
|
|
"model": "gpt-5.5",
|
|
"context_window_tokens": 272000,
|
|
"input_tokens": 70749,
|
|
"usage_percent": 26.010661764705883,
|
|
"count_method": "response_usage_scaled_breakdown",
|
|
"staleness": "live",
|
|
"generated_at": "2026-05-28T00:52:56.713576Z",
|
|
"event_seq": 700,
|
|
"breakdown": [
|
|
{
|
|
"category": "system_prompt",
|
|
"tokens": 1079,
|
|
"usage_percent": 0.3966911764705882
|
|
},
|
|
{
|
|
"category": "tools",
|
|
"tokens": 1541,
|
|
"usage_percent": 0.5665441176470588
|
|
},
|
|
{
|
|
"category": "memory",
|
|
"tokens": 3669,
|
|
"usage_percent": 1.3488970588235294
|
|
},
|
|
{
|
|
"category": "conversation",
|
|
"tokens": 64452,
|
|
"usage_percent": 23.695588235294117
|
|
},
|
|
{
|
|
"category": "other",
|
|
"tokens": 8,
|
|
"usage_percent": 0.0029411764705882353
|
|
}
|
|
],
|
|
"warnings": []
|
|
},
|
|
"state": "succeeded"
|
|
},
|
|
"simplify_opus@1": {
|
|
"first_event_seq": 710,
|
|
"prompt": null,
|
|
"response": null,
|
|
"completion": {
|
|
"outcome": "succeeded",
|
|
"notes": "Stage completed: simplify_opus",
|
|
"failure_reason": null,
|
|
"timestamp": "2026-05-28T00:58:56.086100Z"
|
|
},
|
|
"provider_used": {
|
|
"mode": "agent",
|
|
"provider": "anthropic",
|
|
"model": "claude-opus-4-7"
|
|
},
|
|
"diff": null,
|
|
"script_invocation": null,
|
|
"script_timing": null,
|
|
"parallel_results": null,
|
|
"output": null,
|
|
"started_at": "2026-05-28T00:53:01.021083Z",
|
|
"handler": "agent",
|
|
"timing": {
|
|
"wall_time_ms": 355062,
|
|
"inference_time_ms": 83152,
|
|
"tool_time_ms": 271255,
|
|
"active_time_ms": 354407
|
|
},
|
|
"usage": {
|
|
"input_tokens": 21129,
|
|
"output_tokens": 5543,
|
|
"total_tokens": 339304,
|
|
"reasoning_tokens": 0,
|
|
"cache_read_tokens": 251482,
|
|
"cache_write_tokens": 61150,
|
|
"total_usd_micros": 752148
|
|
},
|
|
"model": {
|
|
"provider": "anthropic",
|
|
"model_id": "claude-opus-4-7"
|
|
},
|
|
"subagents": [
|
|
{
|
|
"agent_id": "e7156f0c",
|
|
"depth": 1,
|
|
"task": "You are reviewing a refactor that removes Fabro's IP allowlisting feature entirely. The diff is at `/tmp/refactor.diff` (2443 lines). The repo root is `/home/daytona/workspace/fabro`.\n\n**Focus: Code Reuse Review**\n\nFor each change in the diff:\n\n1. Search for existing utilities and helpers that could replace newly written code. Use grep to find similar patterns in the codebase — common locations include `lib/crates/fabro-util/`, shared modules, and files adjacent to changed ones.\n2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.\n3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, etc.\n\nNote: This is mostly a *deletion* refactor — most \"new\" code is small follow-ups to deletions (test changes, simplified function signatures, changelog entry). Focus on:\n- Any new test assertions that duplicate existing helper patterns\n- Any new code introduced when simplifying router/auth signatures\n- Any new logic in modified files (vs. plain deletions)\n\nRead the diff at /tmp/refactor.diff first to understand the scope. Then enumerate concrete findings. For each finding give:\n- File and approximate line numbers\n- What the new code does\n- The existing utility/pattern to use instead (with path)\n- Severity (must-fix / nice-to-have)\n\nIf the diff is genuinely clean (mostly deletions with no new reusable patterns missed), say so plainly.",
|
|
"status": {
|
|
"kind": "completed",
|
|
"success": true,
|
|
"turns_used": 31
|
|
}
|
|
},
|
|
{
|
|
"agent_id": "59feb42b",
|
|
"depth": 1,
|
|
"task": "You are reviewing a refactor that removes Fabro's IP allowlisting feature entirely. The diff is at `/tmp/refactor.diff` (2443 lines). The repo root is `/home/daytona/workspace/fabro`.\n\n**Focus: Code Quality Review**\n\nReview the changes for hacky patterns:\n\n1. **Redundant state**: state that duplicates existing state, cached values that could be derived\n2. **Parameter sprawl**: adding new parameters when restructuring would be better\n3. **Copy-paste with slight variation**: near-duplicate code blocks\n4. **Leaky abstractions**: exposing internal details, breaking abstraction boundaries\n5. **Stringly-typed code**: raw strings where constants/enums exist\n6. **Dead code left behind**: imports, types, helpers no longer needed after deletions\n7. **Incomplete deletion**: stub functions, default-empty configs, vestigial parameters that remain after the feature is gone\n8. **Hard-removal compliance**: the spec says no compatibility migration, no stub, no warning-only parser. Flag any place that kept an empty-default placeholder instead of fully removing.\n\nRead /tmp/refactor.diff first. Then run grep across the workspace at /home/daytona/workspace/fabro to check whether any of these symbols still exist post-refactor (should NOT exist):\n- `IpAllowlistConfig`, `IpAllowlist`, `IpAllowEntry`, `LiteralIpAllowEntry`\n- `ip_allowlist_middleware`, `GitHubMetaResolver`, `github_meta_hooks`\n- `trusted_proxy_count`, `github-meta-hooks.json`\n- `ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`\n- frontend: `ip_allowlist`, \"IP allowlist\", \"ip allowlist\"\n\nAllowlist exceptions (SHOULD remain — DO NOT flag):\n- `allowed_usernames` (GitHub user allowlist)\n- `cidr_allow_list` (sandbox egress)\n- generic \"allowlist\" usage in policy tests, markdown rendering, model controls\n- archival references in docs/plans/, docs/brainstorms/, docs/superpowers/\n- Cargo.lock transitive entries for `ipnet` if still required by sandbox CIDR\n\nFor each finding give:\n- File:line(s)\n- Problem\n- Suggested fix\n- Severity (must-fix / nice-to-have)\n\nIf the cleanup is complete, say so plainly.",
|
|
"status": {
|
|
"kind": "completed",
|
|
"success": true,
|
|
"turns_used": 123
|
|
}
|
|
},
|
|
{
|
|
"agent_id": "51c928ad",
|
|
"depth": 1,
|
|
"task": "You are reviewing a refactor that removes Fabro's IP allowlisting feature entirely. The diff is at `/tmp/refactor.diff` (2443 lines). The repo root is `/home/daytona/workspace/fabro`.\n\n**Focus: Efficiency Review**\n\nReview the changes for efficiency issues:\n\n1. **Unnecessary work**: redundant computations, repeated file reads, duplicate API calls, N+1 patterns\n2. **Missed concurrency**: independent operations run sequentially\n3. **Hot-path bloat**: new blocking work added to startup or per-request/per-render hot paths\n4. **Unnecessary existence checks**: pre-checking before operating (TOCTOU)\n5. **Memory**: unbounded structures, missing cleanup, listener leaks\n6. **Overly broad operations**: reading entire files when only a portion is needed\n\nNote: This is mostly a deletion refactor. The main efficiency wins here come *from* the refactor:\n- Removed startup GitHub `/meta` fetch\n- Removed per-request IP allowlist middleware\n\nSo focus your review on:\n- Did the deletions actually remove the per-request middleware layer, or did something inadvertently remain?\n- Did the startup `/meta` fetch and its cache get fully removed?\n- Are there now-unnecessary `SocketAddr` / `ConnectInfo` wrappers still in the router that could be removed (they were only needed for IP allowlisting)?\n- Any newly added code in `serve.rs`, `server.rs`, `auth/translate.rs`, `web_auth.rs` that introduces inefficiency?\n\nRead /tmp/refactor.diff first. Then inspect the modified `lib/crates/fabro-server/src/serve.rs` and `server.rs` to confirm `ConnectInfo<SocketAddr>` extraction is removed if it's no longer needed.\n\nFor each finding give file:lines, the issue, the suggested fix, and severity. If the diff is clean efficiency-wise, say so plainly.",
|
|
"status": {
|
|
"kind": "completed",
|
|
"success": true,
|
|
"turns_used": 54
|
|
}
|
|
}
|
|
],
|
|
"permission_level": "full",
|
|
"agent_tools": [
|
|
{
|
|
"name": "AskUserQuestion",
|
|
"description": "Ask the human one or more questions and wait for their answers before continuing this stage.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "TaskCreate",
|
|
"description": "Create pending tasks in the current session. Use concise subjects, descriptions, optional activeForm text, and metadata. Check TaskList first to avoid duplicate tasks.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "TaskGet",
|
|
"description": "Get one task by taskId, including subject, status, description, owner, blockedBy, and blocks.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "TaskList",
|
|
"description": "List tasks for the current session, including status, owner, and blocking dependencies. Use TaskGet with a taskId for full description and dependency details.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "TaskUpdate",
|
|
"description": "Update an existing task's status, text, owner, metadata, or dependencies. Valid statuses are pending, in_progress, completed, and deleted. After completing a task, call TaskList to find newly unblocked work.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "close_agent",
|
|
"description": "Close a running subagent that is no longer needed.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "subagent",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "edit_file",
|
|
"description": "Edit a file by replacing an exact string. The old_string must be an exact match and unique unless replace_all is true; include surrounding context when needed. Read the file first and preserve existing indentation.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "write",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "glob",
|
|
"description": "Find files by file names using a glob pattern. Use path to choose the search root. Prefer this over shell find or ls when locating repository files.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "read",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "grep",
|
|
"description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "read",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "read_file",
|
|
"description": "Read files before editing them. Returns line-numbered text and supports offset/limit for large files. Use this instead of shell cat, head, tail, or sed when inspecting repository files.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "read",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "send_input",
|
|
"description": "Send a follow-up message to a running subagent when new information or corrected instructions are needed.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "subagent",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "shell",
|
|
"description": "Execute shell commands for terminal operations, package managers, tests and builds. Use dedicated tools for file reads, file edits, filename searches, and content searches. Provide timeout_ms for long-running commands.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "shell",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "spawn_agent",
|
|
"description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "subagent",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "wait",
|
|
"description": "Wait for a subagent to complete, then use the result to synthesize the outcome for the user.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "subagent",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "web_fetch",
|
|
"description": "Fetch content from a URL that starts with http:// or https://. Pass a prompt to extract specific information or summarize the page; omit prompt to return the page content.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "web_search",
|
|
"description": "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "write_file",
|
|
"description": "Create new files, or overwrite an existing file only when replacement is explicitly intended. Prefer edit_file for targeted changes to existing files because write_file overwrites the full file content.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "write",
|
|
"invoked": false
|
|
}
|
|
],
|
|
"context_window": {
|
|
"provider": "anthropic",
|
|
"model": "claude-opus-4-7",
|
|
"context_window_tokens": 1000000,
|
|
"input_tokens": 33336,
|
|
"usage_percent": 3.3336,
|
|
"count_method": "response_usage_scaled_breakdown",
|
|
"staleness": "live",
|
|
"generated_at": "2026-05-28T00:58:56.037581Z",
|
|
"event_seq": 1065,
|
|
"breakdown": [
|
|
{
|
|
"category": "system_prompt",
|
|
"tokens": 2542,
|
|
"usage_percent": 0.2542
|
|
},
|
|
{
|
|
"category": "tools",
|
|
"tokens": 2893,
|
|
"usage_percent": 0.2893
|
|
},
|
|
{
|
|
"category": "memory",
|
|
"tokens": 6149,
|
|
"usage_percent": 0.6149
|
|
},
|
|
{
|
|
"category": "conversation",
|
|
"tokens": 21744,
|
|
"usage_percent": 2.1744
|
|
},
|
|
{
|
|
"category": "other",
|
|
"tokens": 8,
|
|
"usage_percent": 0.0008
|
|
}
|
|
],
|
|
"warnings": []
|
|
},
|
|
"state": "succeeded"
|
|
},
|
|
"verify@1": {
|
|
"first_event_seq": 1291,
|
|
"prompt": null,
|
|
"response": null,
|
|
"completion": null,
|
|
"provider_used": null,
|
|
"diff": null,
|
|
"script_invocation": {
|
|
"script": "git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
|
|
"command": "exec 2>&1\ngit fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
|
|
"language": "shell"
|
|
},
|
|
"script_timing": null,
|
|
"parallel_results": null,
|
|
"output": null,
|
|
"started_at": "2026-05-28T01:02:40.662746Z",
|
|
"handler": "command",
|
|
"usage": {
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"total_tokens": 0,
|
|
"reasoning_tokens": 0,
|
|
"cache_read_tokens": 0,
|
|
"cache_write_tokens": 0
|
|
},
|
|
"state": "running"
|
|
},
|
|
"simplify_gpt@1": {
|
|
"first_event_seq": 1075,
|
|
"prompt": null,
|
|
"response": null,
|
|
"completion": {
|
|
"outcome": "succeeded",
|
|
"notes": "Stage completed: simplify_gpt",
|
|
"failure_reason": null,
|
|
"timestamp": "2026-05-28T01:02:35.257089Z"
|
|
},
|
|
"provider_used": {
|
|
"mode": "agent",
|
|
"provider": "openai",
|
|
"model": "gpt-5.5"
|
|
},
|
|
"diff": null,
|
|
"script_invocation": null,
|
|
"script_timing": null,
|
|
"parallel_results": null,
|
|
"output": null,
|
|
"started_at": "2026-05-28T00:59:00.018638Z",
|
|
"handler": "agent",
|
|
"timing": {
|
|
"wall_time_ms": 215237,
|
|
"inference_time_ms": 172391,
|
|
"tool_time_ms": 42057,
|
|
"active_time_ms": 214448
|
|
},
|
|
"usage": {
|
|
"input_tokens": 445377,
|
|
"output_tokens": 3232,
|
|
"total_tokens": 1359052,
|
|
"reasoning_tokens": 1131,
|
|
"cache_read_tokens": 909312,
|
|
"cache_write_tokens": 0,
|
|
"total_usd_micros": 2812431
|
|
},
|
|
"model": {
|
|
"provider": "openai",
|
|
"model_id": "gpt-5.5"
|
|
},
|
|
"todos": {
|
|
"kind": "openai_plan",
|
|
"list_id": "openai_plan:6738c8c7-32d3-4091-bc2e-499b7e91e4fc",
|
|
"items": [
|
|
{
|
|
"id": "37160cc34ec5a7af",
|
|
"status": "completed",
|
|
"order": 0,
|
|
"subject": "Inspect repository diff and changed files"
|
|
},
|
|
{
|
|
"id": "df81c516dbc9cd8a",
|
|
"status": "completed",
|
|
"order": 1,
|
|
"subject": "Run three parallel review agents"
|
|
},
|
|
{
|
|
"id": "f8fa4237ff6b0636",
|
|
"status": "completed",
|
|
"order": 2,
|
|
"subject": "Apply fixes for actionable findings"
|
|
},
|
|
{
|
|
"id": "b55d1ade383a147e",
|
|
"status": "completed",
|
|
"order": 3,
|
|
"subject": "Run targeted verification"
|
|
},
|
|
{
|
|
"id": "a722485c40cda21a",
|
|
"status": "in_progress",
|
|
"order": 4,
|
|
"subject": "Summarize results"
|
|
}
|
|
]
|
|
},
|
|
"subagents": [
|
|
{
|
|
"agent_id": "280a420d",
|
|
"depth": 1,
|
|
"task": "Code Reuse Review for refactor removing IP allowlisting. Read /tmp/fabro-ip-allowlist-removal.diff for the full diff. For each changed area, search the repo for existing utilities/helpers that could replace newly written code, duplicated functions, or inline logic that should use existing utilities. Focus only on actionable issues introduced by this diff. Return concise findings with file paths/lines and suggested fixes; say 'no actionable findings' if clean.",
|
|
"status": {
|
|
"kind": "completed",
|
|
"success": true,
|
|
"turns_used": 9
|
|
}
|
|
},
|
|
{
|
|
"agent_id": "76ce3d85",
|
|
"depth": 1,
|
|
"task": "Code Quality Review for refactor removing IP allowlisting. Read /tmp/fabro-ip-allowlist-removal.diff for the full diff. Review for redundant state, parameter sprawl, copy-paste, leaky abstractions, stringly-typed code, hacky patterns, or incomplete cleanup. Focus only on actionable issues introduced by this diff. Return concise findings with file paths/lines and suggested fixes; say 'no actionable findings' if clean.",
|
|
"status": {
|
|
"kind": "completed",
|
|
"success": true,
|
|
"turns_used": 9
|
|
}
|
|
},
|
|
{
|
|
"agent_id": "8896266b",
|
|
"depth": 1,
|
|
"task": "Efficiency Review for refactor removing IP allowlisting. Read /tmp/fabro-ip-allowlist-removal.diff for the full diff. Review for unnecessary work, missed concurrency, hot-path bloat, TOCTOU checks, memory/leaks, overly broad operations, or incomplete removal of network calls/startup work. Focus only on actionable issues introduced by this diff. Return concise findings with file paths/lines and suggested fixes; say 'no actionable findings' if clean.",
|
|
"status": {
|
|
"kind": "completed",
|
|
"success": true,
|
|
"turns_used": 9
|
|
}
|
|
}
|
|
],
|
|
"permission_level": "full",
|
|
"agent_tools": [
|
|
{
|
|
"name": "apply_patch",
|
|
"description": "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "write",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "close_agent",
|
|
"description": "Close a running subagent that is no longer needed.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "subagent",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "glob",
|
|
"description": "Find files by file names using a glob pattern. Use path to choose the search root. Prefer this over shell find or ls when locating repository files.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "read",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "grep",
|
|
"description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "read",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "read_file",
|
|
"description": "Read files before editing them. Returns line-numbered text and supports offset/limit for large files. Use this instead of shell cat, head, tail, or sed when inspecting repository files.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "read",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "request_user_input",
|
|
"description": "Ask the human one or more questions and wait for their answers before continuing this stage.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "send_input",
|
|
"description": "Send a follow-up message to a running subagent when new information or corrected instructions are needed.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "subagent",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "shell",
|
|
"description": "Execute shell commands for terminal operations, package managers, tests and builds. Use dedicated tools for file reads, file edits, filename searches, and content searches. Provide timeout_ms for long-running commands.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "shell",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "spawn_agent",
|
|
"description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "subagent",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "update_plan",
|
|
"description": "Update the multi-step plan for the current task. Submit the entire plan; existing steps are reconciled by exact step text.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "wait",
|
|
"description": "Wait for a subagent to complete, then use the result to synthesize the outcome for the user.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "subagent",
|
|
"invoked": true
|
|
},
|
|
{
|
|
"name": "web_fetch",
|
|
"description": "Fetch content from a URL that starts with http:// or https://. Pass a prompt to extract specific information or summarize the page; omit prompt to return the page content.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "web_search",
|
|
"description": "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "other",
|
|
"invoked": false
|
|
},
|
|
{
|
|
"name": "write_file",
|
|
"description": "Create new files, or overwrite an existing file only when replacement is explicitly intended. Prefer edit_file for targeted changes to existing files because write_file overwrites the full file content.",
|
|
"source": {
|
|
"kind": "native"
|
|
},
|
|
"category": "write",
|
|
"invoked": false
|
|
}
|
|
],
|
|
"context_window": {
|
|
"provider": "openai",
|
|
"model": "gpt-5.5",
|
|
"context_window_tokens": 272000,
|
|
"input_tokens": 56957,
|
|
"usage_percent": 20.940073529411766,
|
|
"count_method": "response_usage_scaled_breakdown",
|
|
"staleness": "live",
|
|
"generated_at": "2026-05-28T01:02:35.191522Z",
|
|
"event_seq": 1281,
|
|
"breakdown": [
|
|
{
|
|
"category": "system_prompt",
|
|
"tokens": 1038,
|
|
"usage_percent": 0.3816176470588235
|
|
},
|
|
{
|
|
"category": "tools",
|
|
"tokens": 1475,
|
|
"usage_percent": 0.5422794117647058
|
|
},
|
|
{
|
|
"category": "memory",
|
|
"tokens": 3510,
|
|
"usage_percent": 1.2904411764705883
|
|
},
|
|
{
|
|
"category": "conversation",
|
|
"tokens": 50929,
|
|
"usage_percent": 18.72389705882353
|
|
},
|
|
{
|
|
"category": "other",
|
|
"tokens": 5,
|
|
"usage_percent": 0.001838235294117647
|
|
}
|
|
],
|
|
"warnings": []
|
|
},
|
|
"state": "succeeded"
|
|
},
|
|
"toolchain@1": {
|
|
"first_event_seq": 21,
|
|
"prompt": null,
|
|
"response": null,
|
|
"completion": {
|
|
"outcome": "succeeded",
|
|
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
|
"failure_reason": null,
|
|
"timestamp": "2026-05-28T00:14:02.483992Z"
|
|
},
|
|
"provider_used": null,
|
|
"diff": null,
|
|
"script_invocation": {
|
|
"script": "command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
|
"command": "exec 2>&1\ncommand -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
|
"language": "shell"
|
|
},
|
|
"script_timing": {
|
|
"output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c",
|
|
"exit_code": 0,
|
|
"duration_ms": 1482,
|
|
"termination": "exited",
|
|
"output_bytes": 36,
|
|
"live_streaming": true
|
|
},
|
|
"parallel_results": null,
|
|
"output": null,
|
|
"output_bytes": 36,
|
|
"live_streaming": true,
|
|
"termination": "exited",
|
|
"started_at": "2026-05-28T00:14:00.984087Z",
|
|
"handler": "command",
|
|
"timing": {
|
|
"wall_time_ms": 1499,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 1482,
|
|
"active_time_ms": 1482
|
|
},
|
|
"usage": {
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"total_tokens": 0,
|
|
"reasoning_tokens": 0,
|
|
"cache_read_tokens": 0,
|
|
"cache_write_tokens": 0
|
|
},
|
|
"state": "succeeded"
|
|
},
|
|
"preflight_compile@1": {
|
|
"first_event_seq": 31,
|
|
"prompt": null,
|
|
"response": null,
|
|
"completion": {
|
|
"outcome": "succeeded",
|
|
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
|
"failure_reason": null,
|
|
"timestamp": "2026-05-28T00:16:31.052309Z"
|
|
},
|
|
"provider_used": null,
|
|
"diff": null,
|
|
"script_invocation": {
|
|
"script": "cargo check -q --workspace 2>&1",
|
|
"command": "exec 2>&1\ncargo check -q --workspace 2>&1",
|
|
"language": "shell"
|
|
},
|
|
"script_timing": {
|
|
"output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
|
|
"exit_code": 0,
|
|
"duration_ms": 135614,
|
|
"termination": "exited",
|
|
"output_bytes": 0,
|
|
"live_streaming": false
|
|
},
|
|
"parallel_results": null,
|
|
"output": null,
|
|
"output_bytes": 0,
|
|
"live_streaming": false,
|
|
"termination": "exited",
|
|
"started_at": "2026-05-28T00:14:15.430334Z",
|
|
"handler": "command",
|
|
"timing": {
|
|
"wall_time_ms": 135621,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 135614,
|
|
"active_time_ms": 135614
|
|
},
|
|
"usage": {
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"total_tokens": 0,
|
|
"reasoning_tokens": 0,
|
|
"cache_read_tokens": 0,
|
|
"cache_write_tokens": 0
|
|
},
|
|
"state": "succeeded"
|
|
},
|
|
"preflight_lint@1": {
|
|
"first_event_seq": 41,
|
|
"prompt": null,
|
|
"response": null,
|
|
"completion": {
|
|
"outcome": "succeeded",
|
|
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
|
|
"failure_reason": null,
|
|
"timestamp": "2026-05-28T00:18:58.021803Z"
|
|
},
|
|
"provider_used": null,
|
|
"diff": null,
|
|
"script_invocation": {
|
|
"script": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
|
|
"command": "exec 2>&1\ncargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
|
|
"language": "shell"
|
|
},
|
|
"script_timing": {
|
|
"output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
|
|
"exit_code": 0,
|
|
"duration_ms": 142983,
|
|
"termination": "exited",
|
|
"output_bytes": 0,
|
|
"live_streaming": false
|
|
},
|
|
"parallel_results": null,
|
|
"output": null,
|
|
"output_bytes": 0,
|
|
"live_streaming": false,
|
|
"termination": "exited",
|
|
"started_at": "2026-05-28T00:16:35.033240Z",
|
|
"handler": "command",
|
|
"timing": {
|
|
"wall_time_ms": 142988,
|
|
"inference_time_ms": 0,
|
|
"tool_time_ms": 142983,
|
|
"active_time_ms": 142983
|
|
},
|
|
"usage": {
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"total_tokens": 0,
|
|
"reasoning_tokens": 0,
|
|
"cache_read_tokens": 0,
|
|
"cache_write_tokens": 0
|
|
},
|
|
"state": "succeeded"
|
|
}
|
|
}
|
|
} |