feat: add interactive network approval and domain rejection#698
feat: add interactive network approval and domain rejection#698klassm wants to merge 91 commits into
Conversation
db2ec7c to
29e89e2
Compare
There was a problem hiding this comment.
Code Review
This pull request introduces a network approval feature, allowing users to interactively approve or deny network requests to unknown hosts via OS-level notifications. The implementation includes cross-platform support (Linux, macOS, Windows), a runtime-mutable host filter, and persistence logic for user decisions. I have identified several issues: the approval backend creates a new Tokio runtime on every call, which is inefficient; persistence errors are handled inconsistently; JSON manipulation in the config writer is prone to panics; and the timeout logic is missing or ignored in the notification backends.
29e89e2 to
2282f15
Compare
|
All review comments done. Would be cool to have this - this makes maintaining the allow list a lot easier. |
|
This is a really good idea @klassm - I am away this weekend, but will take a good look first thing Monday - out of interest , it might be good to rig this up to the Approval backend on linux (unless you already have), but that could be a follow up patch. |
|
Great, thanks @lukehinds :-). Approval backend on linux also sounds nice - I would not do it directly, though. I don't have a Linux system, so that's hard to test :-) |
|
Interesting feature! One small UX thought: "allow once" might be ambiguous since it actually allows for the whole session, not just a single request. Maybe something like:
Also, what do you think about a dropdown with those three options plus Allow/Deny buttons instead of four (or more) separate buttons? |
f2586e1 to
10c4c23
Compare
|
@tho I changed the buttons to distinguish between once, session and always. |
1bb15a3 to
273f6a7
Compare
|
@lukehinds Did you have a chance to have a look again? 😇 |
3ec989e to
8912421
Compare
|
hi @klassm , I have and I promise I am keeping an eye on this, its just a big change which commits a lot to the supervisor backend arch, so i need to make sure its malable to other planned changes. |
8912421 to
170d100
Compare
82926e1 to
6b1d919
Compare
6b1d919 to
fc7b7e9
Compare
PR Review SummarySize
Affected crates
Blast radius — BroadThis PR touches: source code,documentation,CI / build tooling,configuration / policy files Updated automatically on each push to this PR. |
Signed-off-by: connrg <conor@alwaysfurther.ai>
…ai#1170) Signed-off-by: connrg <conor@alwaysfurther.ai>
The page opened straight on the proxy modes table, so the commands most people actually want — allow a domain, block the net, inject a key — were buried halfway down. Pull them up into a short "Common cases" block at the top. Signed-off-by: connrg <conor@alwaysfurther.ai>
…-ai#1177) The custom_credentials schema table linked the `proxy` field to #proxy-side-overrides, but the section heading is "Proxy Overrides" (#proxy-overrides). The link resolved to nothing. Point it at the correct anchor and match the link text to the heading. Signed-off-by: connrg <conor@alwaysfurther.ai>
…ns (nolabs-ai#1163) Delay version-update PRs until dependencies are at least three days old, reducing the risk of pulling a freshly compromised release into a tagged build. Security updates are unaffected and still bypass cooldown. Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai>
--allow-cwd doesn't always grant read+write. The access level comes from the active profile's workdir setting, defaulting to read-only when no profile sets one, and granting nothing under the default profile (workdir "none"). The Developer Workflows guide claimed it always grants read+write, which only holds for readwrite profiles and contradicts the flag reference. Align the guide with it and note the none case on the Working Directory section. Signed-off-by: connrg <conor@alwaysfurther.ai>
Bumps [which](https://github.com/harryfei/which-rs) from 8.0.2 to 8.0.3. - [Release notes](https://github.com/harryfei/which-rs/releases) - [Changelog](https://github.com/harryfei/which-rs/blob/master/CHANGELOG.md) - [Commits](harryfei/which-rs@8.0.2...8.0.3) --- updated-dependencies: - dependency-name: which dependency-version: 8.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [cbindgen](https://github.com/mozilla/cbindgen) from 0.29.3 to 0.29.4. - [Release notes](https://github.com/mozilla/cbindgen/releases) - [Changelog](https://github.com/mozilla/cbindgen/blob/main/CHANGES) - [Commits](mozilla/cbindgen@0.29.3...0.29.4) --- updated-dependencies: - dependency-name: cbindgen dependency-version: 0.29.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…1173) * fix(policy) Allow go_runtime to readwrite go-build cache Signed-off-by: Raphael Nestler <raphael.nestler@renuo.ch> * Also add macOS go build cache Signed-off-by: Raphael Nestler <raphael.nestler@renuo.ch> * fix(policy): split go_runtime cache into platform-specific groups Extract the go build cache paths from go_runtime into separate go_runtime_linux and go_runtime_macos groups, each gated by their respective platform field. This ensures only the correct cache path (~/.cache/go-build vs ~/Library/Caches/go-build) is granted per OS. Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai> --------- Signed-off-by: Raphael Nestler <raphael.nestler@renuo.ch> Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai> Co-authored-by: Aleksy Siek <aleksy@alwaysfurther.ai>
…abs-ai#1048) (nolabs-ai#1091) The TLS intercept upstream leg (used for credential injection) always connected directly to the target host, ignoring the configured upstream_proxy / external_proxy. In enterprise environments where the target is only reachable through a corporate proxy, this caused the intercepted request to hang or fail. - Added InterceptUpstreamProxy struct and upstream_proxy field to InterceptCtx so the intercept code can select UpstreamStrategy::ExternalProxy when a proxy is configured. - server.rs now checks the bypass matcher and passes the resolved proxy info into InterceptCtx, mirroring the existing transparent CONNECT path. - Auth-not-implemented check (config.auth.is_some()) fails early and consistently with the external proxy handler. - 3 unit tests for strategy selection (ExternalProxy vs Direct). Signed-off-by: Caio Silva <caio@cdcs.dev> Co-authored-by: Luke Hinds <lukehinds@gmail.com> Co-authored-by: Aleks <121458075+SequeI@users.noreply.github.com>
…l catch-all (nolabs-ai#1132) * fix(proxy): stop _ep_ route from shadowing credential catch-all A credential catch-all (custom_credentials with no endpoint_rules) was shadowed by a credential-less _ep_ endpoint-authorization route on the same upstream: on a path the _ep_ route authorized, selection picked the credential-less _ep_ match ahead of the catch-all and forwarded the request without injecting the managed credential. Partition candidate routes into four buckets (matched_cred, matched_passthrough, catchall_cred, catchall_passthrough). The active credential layer is matched_cred when any credential route matched, otherwise catchall_cred, so a credential catch-all stays in play when only credential-less _ep_ routes matched. Selection then prefers, in order: (1) the single credential route from the active layer, (2) a matched credential-less route (bare endpoint authorization), (3) a credential-less catch-all (passthrough). This injects the token on authorized paths instead of dropping it, and also makes two credential catch-alls for one upstream ambiguous (403) rather than a silent pick. The _ep_ hard-deny gate is unchanged, so non-matching paths are still hard-denied (default-deny preserved) and the host gate is untouched. Extract the selection logic (gate + ambiguity + priority) from tls_intercept/handle.rs into select_route + RouteSelection in route.rs so the decision has a single source of truth shared with its unit tests; the prior test was a hand-copied mirror that did not model the _ep_ gate. Derive Default on RouteConfig so test fixtures can use ..Default::default() (no serde change: prefix/upstream remain required on deserialize). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Leonardo Zanivan <leonardo.zanivan@gmail.com> * chore: trigger CI Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Leonardo Zanivan <leonardo.zanivan@gmail.com> --------- Signed-off-by: Leonardo Zanivan <leonardo.zanivan@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Luke Hinds <lukehinds@gmail.com> Co-authored-by: Aleks <121458075+SequeI@users.noreply.github.com>
* refactor(audit): move attestation logic to core library Move audit attestation signing and verification implementation from `nono-cli` into the `nono` core library. - Consolidate `AuditAttestationVerificationResult` struct, `AuditAttestationPredicate` structs, and related constants. - Extract `write_audit_attestation` and `verify_audit_attestation` implementations into new `sign_audit_attestation_bundle` and `verify_audit_attestation_bundle` functions in `nono::audit`. - `nono-cli` now serves as a thin wrapper around these core functions. This change centralizes the core logic, making it reusable and improving the separation of concerns between the CLI and library. Signed-off-by: Luke Hinds <lukehinds@gmail.com> * refactor(audit): improve attestation statement parsing Streamlines the `extract_audit_attestation_statement` function by directly accessing the DSSE envelope from the Sigstore bundle's `content` field. This eliminates redundant JSON serialization and deserialization steps, making the extraction process more efficient and leveraging the `SignatureContent` enum. The `zeroize::Zeroizing` wrapper is also removed from `scrubbed_command`. Command arguments are not treated as sensitive secrets requiring zeroization. Signed-off-by: Luke Hinds <lukehinds@gmail.com> --------- Signed-off-by: Luke Hinds <lukehinds@gmail.com> Co-authored-by: Aleks <121458075+SequeI@users.noreply.github.com>
…-ai#1178) * feat(output): show blocked macos grants in capability summary Previously, macOS user-granted paths that were silently blocked by deny groups were logged as individual warnings. This could lead to a large number of warnings when a broad user grant (e.g., `~/Library`) overlapped with multiple deny groups. This commit refactors the logic to collect these blocked grants and present them in the capability summary. The new display is collapsed into a single line by default, indicating the number of paths blocked. Using the `-v` (verbose) flag expands the summary to list each specific path and the deny rule that blocks it, along with a hint about how to use `--bypass-protection`. This significantly improves readability and user experience by folding multiple related warnings into a concise summary. Signed-off-by: Luke Hinds <lukehinds@gmail.com> * refactor(output): improve path display and string allocations - Use `Path::to_string_lossy` for displaying paths in `print_blocked_grants`. This provides more robust handling of non-UTF-8 characters and avoids unnecessary `String` allocations compared to `Path::display().to_string()`. - Optimize string handling when grouping blocked grants by deferring `.to_string()` calls until group names are inserted into the `groups` vector, reducing temporary allocations. - Reorder test module imports in `output.rs` for minor style consistency. Signed-off-by: Luke Hinds <lukehinds@gmail.com> --------- Signed-off-by: Luke Hinds <lukehinds@gmail.com>
Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai>
* fix(cli): use XDG config paths consistently Route user config (config.toml, trusted-keys cache) through resolve_user_config_dir() instead of dirs::config_dir(), so macOS matches ~/.config/nono like profiles and packages already did. Add $NONO_CONFIG profile expansion, display path helpers for CLI output, and update schema/docs/scripts to prefer XDG variables in machine-readable paths while keeping ~/.config/nono in user-facing docs and --help. Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai> * fix Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai> --------- Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai>
* AWS: Implement aws_auth config This adds the aws_auth struct to customCredentials, with optional subkeys: profile, region, and service. If the aws_auth subkey is present at all, the route will be signed with AWS credentials in the future The subkey is mutually exclusive with credential_key and auth. The routes with aws_auth are wired through to the proxy, where a 501 not implemented is returned for any route using the config Signed-off-by: Anil Kulkarni <anil@terminal.space> * PR feedback Signed-off-by: Anil Kulkarni <anil@terminal.space> --------- Signed-off-by: Anil Kulkarni <anil@terminal.space>
This function does many different things, from parsing the body headers, choosing what config matches the route, handling the implementation, and logging/auditing. Splitting this into helper functions is better for readability, scope of purpose, and will make the future aws auth implementation easier to implement Signed-off-by: Anil Kulkarni <anil@terminal.space>
* Create failing tests for the proxy activating under the presence of customCredentials Signed-off-by: Anil Kulkarni <anil@terminal.space> * Ensure the proxy starts when customCredentials are passed in by itself Signed-off-by: Anil Kulkarni <anil@terminal.space> --------- Signed-off-by: Anil Kulkarni <anil@terminal.space>
* feat(update-check): discover ci environments on update The update check now includes a coarse classification of the CI environment. This helps us understand how nono well (or not) recent versions are used in automated workflows versus local development. - Detection relies solely on well-known environment variable names (e.g., GITHUB_ACTIONS, CI). - Raw environment variable values are never collected or transmitted. - The update service's privacy assurances remain unchanged: no personally identifiable information is collected. - Updated the `test-update-server.py` script to log the new CI fields. Signed-off-by: Luke Hinds <lukehinds@gmail.com> * refactor(update_check): simplify ci environment detection The `CiEnvironment` struct has been removed to streamline the representation of CI detection. The `detect_ci_environment` function has been renamed to `detect_ci_provider` and now directly returns an `Option<&'static str>` representing the detected CI provider. This simplifies the API by removing the need for an explicit `is_ci` boolean, as its presence can be inferred from `ci_provider.is_some()`. New tests have been added to: - Ensure that environment variables with "falsey" values (e.g., empty string, "0") do not inadvertently trigger CI detection. - Validate that no CI is detected when no relevant environment variables are present. Signed-off-by: Luke Hinds <lukehinds@gmail.com> --------- Signed-off-by: Luke Hinds <lukehinds@gmail.com> Co-authored-by: Aleks <121458075+SequeI@users.noreply.github.com>
…clients (nolabs-ai#1171) * feat(diagnostics): expose structured diagnostics for library and FFI clients Add stable diagnostic codes, remediations, and session reports in nono and nono-proxy; render CLI footers from structured data with --diagnostics-json. Extend C FFI with diagnostic codes and JSON helpers. Refactor diagnostic module and ignore local diagnostics.json output. Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai> * fix: review fix + deduplicate oauth block Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai> * fix: review comments Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai> --------- Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai>
* fix(pty): suspend Ctrl-Z manually in orphaned PTY sessions (nolabs-ai#989) setsid() in setup_child_pty orphans the child process group, preventing kernel delivery of SIGTSTP. PtyProxy now intercepts Ctrl-Z (0x1A), sends uncatchable SIGSTOP to the child, restores terminal, then SIGTSTP-stops nono. On fg: raw mode restored, SIGCONT + SIGWINCH forwarded. Signed-off-by: Caio Silva <caio@cdcs.dev> * fix(pty): retry suspension waitpid on EINTR Signed-off-by: Caio Silva <caio@cdcs.dev> * fix(pty): send SIGWINCH to the foreground process group on resume Signed-off-by: Caio Silva <caio@cdcs.dev> * refactor(pty): expose PTY master fd through a getter Signed-off-by: Caio Silva <caio@cdcs.dev> * fix(pty): detect CSI-u Ctrl-Z and handle nested jobs TUI apps using the kitty keyboard protocol (opencode/opentui) send Ctrl-Z as a CSI-u escape (\x1b[122;5u or \x1b[90;5u) rather than raw 0x1A. Detect both for terminal clients by reusing the existing enhanced-key matcher (control_key_candidates(0x1a) is already [90, 122]); socket-client detach behavior is unchanged. Distinguish nested jobs from the orphaned direct child in handle_pty_suspension: a nested job (e.g. vim under bash -i) is not orphaned, so forward a plain SIGTSTP and let the inner shell drive suspend/resume; only the direct child gets the manual SIGSTOP + self-suspend dance. This fixes a hang where waitpid() blocked on a stopped process that was not nono's child. On resume, repaint the alternate screen from nono's captured vt100 state (mirroring the client re-attach replay) instead of emitting a bare alt-screen-enter and relying on SIGWINCH, which opencode ignores. Use the clearing terminal restore on suspend so repeated Ctrl-Z/fg cycles do not accumulate cursor drift on the normal screen. Signed-off-by: Caio Silva <caio@cdcs.dev> --------- Signed-off-by: Caio Silva <caio@cdcs.dev> Co-authored-by: Luke Hinds <lukehinds@gmail.com>
* refactor(proxy): separate proxy intent from activation Replace the flat `ProxyLaunchOptions` struct with focused intent structs: `DomainFilterIntent`, `EndpointFilterIntent`, `CredentialProxyIntent`, `UpstreamProxyIntent`, `TlsInterceptIntent`, and `OpenUrlIntent`. Remove the stored `active: bool` field. Proxy activation is now derived via `is_active()`, which returns true when any activating intent struct is `Some`. `WithEndpoints` allow-domain entries are split from plain CONNECT-tunnel entries at prepare time, making the TLS-intercept requirement explicit in the type. Infra-only flags (`--proxy-port`, `--proxy-ca-validity`, `--trust-proxy-ca`) now return an error if used without an activating proxy feature instead of silently doing nothing. Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai> * fix: review comments Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai> * cargo fmt Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai> --------- Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai>
Signed-off-by: Aleksy Siek <aleksy@alwaysfurther.ai>
…ials (nolabs-ai#1215) * make custom credentials require explicit activation refactor(credentials): require explicit activation for custom credentials Previously, merely defining custom credentials in a profile would cause them to be automatically activated, leading to the creation of routes and the proxy becoming active even if no services explicitly requested credentials. This change modifies the behavior of `resolve_credentials` and `prepare_proxy_launch_options` to treat custom credentials as definitions or templates. They must now be explicitly listed in the `credentials` section of a policy to be enabled. This aligns their behavior with other credential types, ensuring that the proxy and routes are only activated when genuinely needed. - Custom credential definitions no longer implicitly activate routes. - The proxy will not activate solely due to the presence of custom credential definitions. Fixes: nolabs-ai#1214 Signed-off-by: Luke Hinds <lukehinds@gmail.com> * fix(proxy): correct proxy active state for custom credentials When defining custom credential sources for a proxy, the `is_active` method would previously evaluate to true if a `CredentialProxyIntent` was present, even if that intent contained an empty list of actual login credentials. This incorrectly suggested the proxy was "active" for traffic authentication. This change updates the `is_active` logic to only return true when explicit login credentials are provided. It also ensures that `CredentialProxyIntent` is always constructed and carried forward if custom credential definitions are specified, enabling network profile overrides even when no actual authentication credentials are required. - update `is_active` to check for non-empty credential list - ensure `CredentialProxyIntent` carries custom definitions regardless of login credentials - add test to confirm custom credentials are carried but do not activate proxy Signed-off-by: Luke Hinds <lukehinds@gmail.com> --------- Signed-off-by: Luke Hinds <lukehinds@gmail.com>
- Deny/Approve buttons with Once/Session/Always duration dropdown - Blacklist (reject_domain) hosts auto-denied without prompting - Once approval bypasses runtime filter re-check - Deny(Session/Always) adds to runtime deny filter and persists - Tests for filter blacklist, deny scope, approval scope
The ConfigWriter was using network_profile (a network policy field) to determine which profile file to write persisted hosts to. When network_profile was None (the default), it fell back to writing to 'default' profile — but 'default' is never loaded on restart unless the user explicitly runs --profile default. Now ProxyLaunchOptions carries profile_name (from --profile CLI arg). ConfigWriter uses this to write to the correct user profile file (e.g., ~/.config/nono/profiles/claude-code.json) that will be loaded on the next restart. Signed-off-by: klassm <klassm@users.noreply.github.com>
…r rebase The merge conflicts reverted two security fixes: 1. connect.rs: Both primary and runtime filter checks now short-circuit on DenyLinkLocal in addition to DenyHost, preventing cloud metadata SSRF via DNS rebinding from reaching the approval dialog. 2. types.rs: NetworkApprovalDecision::is_denied() now includes the Timeout variant, so timeout is correctly treated as a denial. Signed-off-by: klassm <klassm@users.noreply.github.com>
When a sandboxed process requests access to a blocked host, the user is now prompted via an OS dialog to approve or deny the request. Approved hosts are allowed for the session; optionally persisted to the active profile for permanent access.