Skip to content

Sandcat netbird integration - #107

Draft
wiacekm wants to merge 137 commits into
VirtusLab:masterfrom
wiacekm:sandcat-netbird-integration
Draft

Sandcat netbird integration#107
wiacekm wants to merge 137 commits into
VirtusLab:masterfrom
wiacekm:sandcat-netbird-integration

Conversation

@wiacekm

@wiacekm wiacekm commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

No description provided.

Michał Wiącek and others added 30 commits June 15, 2026 12:44
Co-authored-by: Cursor <cursoragent@cursor.com>
… netbird container

Co-authored-by: Cursor <cursoragent@cursor.com>
Add netbird.env as the single source of truth for version and tarball
checksums, verify downloads in Dockerfile.wg-client before extract, inject
build args at init via apply_netbird_build_args, and fix stale peer-remove
comment in netbird.bash.
Read netbird_api_token and netbird_enrollment_key from user, project, and
local settings layers (env overrides). Export NB_SETUP_KEY before docker
compose in sandcat compose/run/attach/restart-proxy. Seed netbird_api_token
on init --netbird and document settings-based configuration.
Enable cloud/existing/new NetBird management server flows in init and persist the selected management URL so runtime commands and compose wiring resolve it automatically. Add a self-hosted server template scaffold and focused tests/docs so self-hosted onboarding is explicit and repeatable.
Add `sandcat netbird server start|stop|status` as a thin wrapper around
the provisioned self-hosted stack in ~/.config/sandcat/netbird-server.
Improve API error handling for NetBird’s misleading 404-on-invalid-token
responses, keep settings-sourced PATs in a local variable instead of
exporting NB_API_TOKEN, and guard jq pretty-printing when the response
is not valid JSON.
Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce CapabilityRuntimeError hierarchy with domain-specific exceptions for visibility, lease, and bundle version failures.
Introduce LeaseDecision and LeaseManager to issue time-bounded leases with
quota decrement, exhaustion checks, and expiry evaluation for PoC 1 flows.
RevocationManager revokes by lease or ref with fail-closed catalog state.
discover_capabilities returns metadata for Discoverable caps only.
Add CapabilityRuntime class that wires together catalog, leases, revocation,
and observability. Implements all 7 protocol surfaces from spec §3.2:
- check_current_capabilities: returns bundle with visible/leased tools
- request_capability_lease: grants leases with PoC 1 params
- revoke_capability: revokes by lease ID or capability ref
- discover_capabilities: discovers by intent query
- emit events: delegates to observability collector
- record_action: decrements quota and revokes when exhausted
- enforce_action: validates bundle version and visibility

Includes integration test for full PoC 1 lifecycle (create_pr invisible
→ lease → visible → use → gone) plus unit tests for revoke, discovery,
and version mismatch scenarios.

All 23 tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Thin check-then-act wrapper that re-fetches bundle version before each action and records leased tool usage.

Co-authored-by: Cursor <cursoragent@cursor.com>
Wrap MCP-delivered tools via McpToolAdapter with write_note lifecycle
(quota=3, ttl=5m) and capability-specific lease params in the runtime.

Co-authored-by: Cursor <cursoragent@cursor.com>
Demonstrates the §5.1 lease lifecycle with AgentExecutionLoop and agent
adaptation to draft_pr when create_pr is exhausted.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ackend protocol

Co-authored-by: Cursor <cursoragent@cursor.com>
…mentations

Co-authored-by: Cursor <cursoragent@cursor.com>
…etwork revoke

Implements PhysicalRevocationBackend by delegating route and peer removal to an injectable NetBirdClient.

Co-authored-by: Cursor <cursoragent@cursor.com>
Implements Task 4: Catalog network bindings + bundle assembly
- Add network binding storage to catalog (set/get/iter methods)
- Add networks field to CapabilityBundle with default empty list
- Add register_network_capability() to runtime
- Include network capabilities in check_current_capabilities bundle
- Add CallerIdentityMismatch error for test compatibility
- Network capabilities with bindings appear in bundle.networks
- Test verifies visible network caps are included in bundle

TDD approach: wrote test first, watched it fail, implemented minimal code.
All 51 tests pass (excluding pre-existing test_security.py failures).

Co-authored-by: Cursor <cursoragent@cursor.com>
Michał Wiącek and others added 29 commits August 13, 2026 13:47
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Require authenticated, unambiguous lookup and delete the resolved peer ID so replacement cannot silently enroll or remove a different peer.

Co-authored-by: Cursor <cursoragent@cursor.com>
Task 6 requires netbird_mgmt_delete_peer_by_name as a public API. Re-add it
as a thin wrapper over find + delete-by-id while keeping deterministic
replacement (single lookup in netbird_replace_same_name_peer_if_needed).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sandcat now only stores management URLs and keys. Drop --netbird-server,
sandcat netbird server, and --proxy-peer; operators run those stacks from
docs/examples/. Remove demo scripts and engineering-gate wrappers.
Missing-mount copy must not lead with re-init (that overwrites the project catalog). Default-no-restart must be asserted on a mounted project with a zero-call docker stub so compose cannot sneak through. Document that restart does not remount a new inode.
Capability-runtime is leaving this PoC; unknown-option is the operator-facing cut.

Co-authored-by: Cursor <cursoragent@cursor.com>
The PoC no longer ships leases, catalog edit, or the Python control plane.

Co-authored-by: Cursor <cursoragent@cursor.com>
Layer 1 remain the only in-proxy deny; mesh policy is the NetBird dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
Peer and ACL administration is the NetBird dashboard; compose still exports enroll env.

Co-authored-by: Cursor <cursoragent@cursor.com>
Host exports literals or op:// pass:// paths; it never calls a secret provider CLI.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace and dns_label keep using a plaintext PAT after in-container lookup.

Co-authored-by: Cursor <cursoragent@cursor.com>
Raw jq into NB_SETUP_KEY skipped object-shaped secrets before in-container resolve.

Co-authored-by: Cursor <cursoragent@cursor.com>
The gateway is a compose PoC; tokens stay in a gitignored .env.

Co-authored-by: Cursor <cursoragent@cursor.com>
Operator docs no longer describe capability leases or sandcat netbird.

Co-authored-by: Cursor <cursoragent@cursor.com>
Mesh permission is dashboard ACL, not a NetBird route on wt0.
Hung op/pass-cli on mitmproxy startup could block gosu; raw jq dumped
object-shaped tokens as garbage PATs. Timeouts, Proton warmup/retry, and
flatten+resolve keep replace fail-closed without blocking mitmweb.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ded relay

Match the combined server Relays list, coerce 0.72 ManagementURL objects, keep
enrollment after replace failures, advertise STUN via docker0, and replace stale
wg-client mesh DNS when peer IPs change.

Co-authored-by: Cursor <cursoragent@cursor.com>
Setup-key-only first enroll was aborting because same-name lookup needs an
API token. Skip replace when no token is present; still fail if a token is
set and delete fails. Reject non-PAT pass-cli sessions before pass://
resolve. Drop the example lifecycle copy and build from the CLI template.
Empty .env.example, cut the second CLI server walkthrough, and remove the
accidental cli/.version.
@adamw

adamw commented Sep 9, 2026

Copy link
Copy Markdown
Member

Automated review. Generated by Claude Code from seven reviewer agents (functionality, security, structure, readability, simplicity, tests, performance) plus a verification pass. Line numbers refer to the PR head (d26bb2d). Findings marked verified were reproduced locally.

Blockers

  • Entrypoint replacement drops master's behavior (cli/lib/composefile.bash:446). enable_netbird deletes the compose entrypoint and replaces it with mitmproxy-init.sh, which only re-does rm -f dns.conf. Master's entrypoint also chowns /mitmproxy-public and publishes the CA cert there, and master's healthcheck gates on that cert. After rebase, mitmproxy never becomes healthy and wg-client never starts. The PR currently conflicts with master in five files. Verified.
  • Healthcheck budget is ~30s but startup can take minutes (compose-proxy.yml:71). netbird_verify_host_management_reachable alone is ~95s worst case (16 × curl --max-time 5 + sleeps), plus daemon wait (30s), wt0 wait (30s), and the addon's new _pass_cli_warmup and per-secret retries (3 × 60s each). Only wg-client got a start_period. The comment in main() says enrollment must not block the proxy, but it runs synchronously before exec.
  • Digit-only or boolean-looking secrets are rejected (cli/lib/netbird.bash:56). yq -o json -r strips quotes, and netbird_flatten_secret_setting re-parses the text as YAML. A token of 0123456789 types as !!float, the flatten fails, and compose/run/attach/restart-proxy all abort. A value of null is silently treated as unset. Verified.
  • Two existing pytest tests fail (test_mitmproxy_addon.py:1132, :1163). They assert timeout=30; the code now uses 60. The unpatched time.sleep in the new retry loop also adds ~20s to the suite. Clean at the merge base. Verified.
  • Stale dns.conf removed too late (mitmproxy-init.sh:377). It is deleted after start_netbird instead of first. On restart with the persisted volume, the healthcheck passes on the previous run's file and wg-client reads a stale upstream.

Security (high)

  • mitmweb is reachable from the whole mesh with the fixed password mitmproxy. enable_netbird puts wt0 in the mitmproxy netns; only OUTPUT rules are added. NetBird's default policy is all-to-all. Any peer can open port 8081 and read intercepted traffic, including resolved secrets. Add an INPUT drop on wt0 for 8081 and the WG port, and generate the password.
  • Repo-controlled peer name triggers an authenticated DELETE (netbird-peer-lifecycle.sh:278-302). netbird_peer_name_proxy is read from the project's committed .sandcat/settings.json. Cloning a hostile repo and starting the stack deletes whatever peer that repo names from the operator's account. Derive the name from the project instead of trusting settings.
  • PAT and setup key are readable from the agent container. netbird_api_token / netbird_enrollment_key are layered over .sandcat/settings*.json, and .sandcat is mounted into the agent. The README recommends this layering. Either drop the project layers for these keys or exclude them from the mount.

Important

  • Peer FQDNs from the management server go into dnsmasq with only a substring match on the domain (mitmproxy-init.sh:281). A name containing / becomes a multi-domain address= line and hijacks a whole TLD inside the sandbox. Anchor the suffix and validate the charset.
  • local=/<domain>/ (mitmproxy-init.sh:303) cancels the server=/<domain>/<ns> line at :272, so wildcard mesh resolution never works once any peer exists.
  • Alias regex ^(.+)-[0-9]{1,3}-[0-9]{1,3}$ (mitmproxy-init.sh:289) strips two of four octets: x-100-64-0-5 yields alias x-100-64. Verified.
  • mv "$tmp" "$profile_file" runs even when jq failed (netbird-peer-lifecycle.sh:179-200), leaving an empty default.json. set -e does not help because the call chain runs inside if start_netbird.
  • Cloud enrollment inherits a stale self-hosted URL from global settings.json (mitmproxy-init.sh:338); the cloud path never clears it.
  • find_repo_root uses $PWD, so sandcat init --path other --netbird reads the current directory's .sandcat/settings.json and bakes that server into the other project (cli/lib/netbird.bash:25).
  • No trap in either script. exec gosu orphans the NetBird daemon and both supervisor loops under mitmweb, so docker stop never runs netbird down. supervise_netbird_daemon also respawns netbird service run without checking the previous one is alive, and re-enrolls every 10s with no backoff, each time fetching the full peer list and possibly deleting the peer.
  • netbird up has no timeout (mitmproxy-init.sh:193, :231). Setup key and PAT are passed on argv to netbird and curl; use --setup-key-file and curl -H @-.
  • netbird-peers.conf is never truncated when there are no peers and the cp is not atomic (mitmproxy-init.sh:308-315).
  • The two-call retry in netbird_resolve_secret_ref (netbird-peer-lifecycle.sh:59-63) writes both attempts to stdout, so a partial first output corrupts the captured secret.
  • restart_dnsmasq starts a second dnsmasq even if the old one never exited, and logs success regardless (wg-client-init.sh:158-175). Every peer change restarts dnsmasq, giving the agent up to ~10s of DNS outage.
  • _volume_timestamp_epoch (cli/lib/volume.bash:13) only strips a trailing Z; a numeric offset from a non-UTC daemon fails on GNU date and is silently wrong on BSD date. Regression from master's date -d.
  • "${COMPOSE_ARGS[@]}" under set -u fails on macOS bash 3.2 when empty (start.sh:149). The CLI already uses the ${a[@]+"${a[@]}"} guard elsewhere.
  • start.sh writes generated server secrets into the tracked netbird-server.env with default umask.
  • wg-client-init.sh re-does the full merge every 5s even when the mtime it computes is unchanged; a one-line early return fixes it.

Cleanup

  • docs/examples/proxy-peer/scripts/proxy-peer-init.sh duplicates ~150 lines of mitmproxy-init.sh and has already drifted. Both source netbird-peer-lifecycle.sh, so the shared home exists. The four tests that grep both copies for the same string go away with it.
  • ~200 lines of NetBird compose logic live in composefile.bash, which sources netbird.bash mid-function. init defines three functions inside init(). apply_secret_provider now branches on the mitmproxy dockerfile.
  • The eight idempotency guards in enable_netbird are dead: the template is copied fresh immediately before.
  • ~26 tests grep source text or assert line order (netbird_contract.bats, netbird_peer_lifecycle.bats:245-290, proxy_peer_compose.bats, wg-client/netbird_dns.bats:138-153). No test covers a failing GET or DELETE in the deregistration path. Several tests are duplicates (three idempotence tests, two --netbird-management-url tests, two --capability tests).
  • Four unrelated changes ride along: the yq SIGSEGV retry in require.bash, BSD date in volume.bash, the pass-cli warmup, and the enabled flag parser (~35 lines for a boolean).
  • Every attach/compose/run/restart-proxy now spawns ~10 yq processes even with NetBird disabled.
  • docs/examples/netbird-server (426 lines) is not used by the CLI. docs/ is now a Sphinx source tree on master; runnable examples belong outside it.
  • netbird_mgmt_delete_peer_by_name and apply_netbird_build_args's wg-client default are only used by their own tests. netbird.env says wg-client consumes it; the consumer is Dockerfile.mitmproxy.
  • New bats files use spaces; .editorconfig mandates tabs.

Test run

In a worktree at the PR head: bats 389 passed, 5 failed (2 pre-existing on master, 3 in netbird_server_start.bats from SHELLOPTS leaking nounset into the pyenv shim). pytest 329 passed, 2 failed (above).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants