Skip to content

feat(profile): configurable approval backend for supervised-mode prompts - #1677

Merged
connrg merged 4 commits into
nolabs-ai:mainfrom
connrg:feat/configurable-supervised-approval-backend
Aug 19, 2026
Merged

connrg merged 4 commits into
nolabs-ai:mainfrom
connrg:feat/configurable-supervised-approval-backend

Conversation

@connrg

@connrg connrg commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Supervised mode's filesystem/capability approval prompts (the seccomp-notify traps) were hardcoded to the interactive terminal (TerminalApproval). This makes that approval surface configurable, so the prompts can be routed to any of the existing approval backends (terminal, webhook, or chain).

It adds two fields under the profile security section:

  • security.approval_backends — a map of named backends, reusing the exact ApprovalBackendConfig shape already used by command_policies.approval_backends.
  • security.approval_defaults — which backend answers by default.

Design points:

  • Decoupled from command_policies on purpose. Configuring an approval backend here does not switch on the tool-sandbox runtime. You get non-terminal approval routing for supervised mode without opting into a broader runtime.
  • Shared builder. Both the command_policies path and this new security path resolve backends through the same builder, so chain-cycle detection and webhook construction stay identical across surfaces.

Why

nono already lets network (endpoint) and command approvals route to a configurable backend such as a webhook. Supervised-mode filesystem/capability approvals were the exception — they could only be answered by a human at a TTY. That has two downsides this PR addresses:

  1. Non-interactive / headless use. With no human at the terminal, a supervised file/capability prompt effectively goes nowhere. There was no supported way to hand those decisions to an external approver or automated policy service.
  2. Consistency. Three approval surfaces, only one of which was hardwired to the terminal, is a surprising inconsistency. This unifies how all approval prompts are routed.

Security

This change touches an approval/trust boundary, so it was designed to be fail-secure and to leave existing behavior untouched:

  • No weaker default. With nothing configured, the interactive terminal prompt is used exactly as before — the out-of-the-box posture is unchanged.
  • Fail closed, never silently degrade. When a backend is configured but the default can't be built or resolved, that is a hard NonoError — the run aborts rather than quietly falling back to the less-supervised terminal prompt. A lone backend with no explicit default also fails closed rather than being auto-selected.
  • Least privilege. Operators no longer have to enable the broader tool-sandbox runtime just to route supervised approvals off the terminal, keeping the enabled surface minimal.
  • Approval channel is fail-safe. The webhook backend (shared with the existing paths) treats a non-2xx response as Denied, rejects unknown/invalid decision payloads, caps the response body (64 KB) to avoid unbounded reads, and uses the platform TLS verifier. Because a "granted" response authorizes access, operators should point the backend at a trusted endpoint (HTTPS, or a loopback shim) — the routing target is now a security-relevant part of the profile.
  • Explicit and auditable. This only changes who answers an approval prompt; it does not broaden any capability set or bypass an existing check.

Test Plan

  • cargo clippy -p nono-cli --all-targets -- -D warnings -D clippy::unwrap_used — clean
  • cargo fmt --check — clean
  • New unit tests in approval_runtime: configured webhook resolves; a lone backend with no default fails closed; nothing configured → None (terminal); unknown default → hard error.
  • New profile tests: profile merge unions approval_backends and child approval_defaults win; schema parse + validation of a security.approval_backends profile.
  • schema_shape integration test updated for the new SecurityConfig fields.

connrg added 2 commits August 19, 2026 08:23
Add security.approval_backends (named ApprovalBackendConfig map) and security.approval_defaults to the profile schema, decoupled from command_policies so configuring a backend does not activate the tool-sandbox.

Share build_approval_registry_from between the command_policies and profile paths so both surfaces resolve backends identically, including chain-cycle detection and webhook construction.

resolve_supervised_approval_backend returns Ok(None) when no backend is configured (fall back to the interactive terminal prompt), and a hard error when backends are configured but the default cannot resolve -- never a silent fallback to less-supervised behavior.

Merge semantics: child profiles union approval_backends; child approval_defaults win. Update the JSON schema, schema_shape test, and profile parse/merge tests.
… docs

Collapse the supervised-mode backend selection to Option::unwrap_or
(same behavior and lifetimes as the prior match), and reword the
approval-backend comments and JSON schema descriptions in plainer
language while keeping the technical detail: decoupling from
command_policies (no tool-sandbox), and fail-closed resolution
(configured-but-unresolvable is a hard error, never a silent fall back
to the terminal prompt).

No functional change; clippy and the approval/profile/schema tests pass.

Signed-off-by: connrg <conor@nolabs.ai>
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

PR Review Summary

Size

Metric Value
Lines added +514
Lines removed -30
Total changed 544
Classification Large (> 300 lines)

Affected crates

  • crates/nono-cli — CLI changes. Verify argument parsing, flag documentation, and UX behaviour across supported platforms.

Blast radius — Moderate

This PR touches: source code,configuration / policy files


Updated automatically on each push to this PR.

@connrg
connrg marked this pull request as ready for review August 19, 2026 09:15

@nogent-nolabs-ai nogent-nolabs-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nogent code review

No blocking or minor issues found; the changes are well-structured, robust, and correctly adhere to the sandboxing security model.

Findings (not tied to a changed line):

  • 🐛 [MEDIUM · bug] crates/nono-cli/src/profile/mod.rs:3108 — The newly introduced security.approval_backends and security.approval_defaults do not undergo any semantic validation during profile loading or finalization. While their sibling structures under command_policies.approval_backends are rigorously validated by validate_command_policies (checking for cycles/self-chaining, invalid timeout values, webhook URLs missing, invalid configuration modes, and unknown child references), those under security completely bypass these checks. This results in late runtime errors or start-up failures instead of clean, early validation errors in finalize_profile. Update finalize_profile in crates/nono-cli/src/profile/mod.rs to validate the approval configs under security using shared validation logic.

Automated code + security review. CI already covers clippy, rustfmt, tests, cargo-audit and commit-lint.

@connrg
connrg requested review from SequeI and lukehinds August 19, 2026 09:21

@SequeI SequeI left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm, feel free to merge once the comment from nogent has been addressed. Thanks!

command_policies approval backends are validated by
validate_command_policies at load/finalize (per-type field consistency,
missing webhook url, chain mode/children, self-chaining, references to
unknown backends, NUL-in-url, positive timeouts), but the security
surface added for supervised-mode routing skipped all of it -- a
malformed backend only errored later at supervised-launch build time,
and some cases (e.g. a terminal backend with a stray url) were not
caught at all.

Generalize validate_approval_backend/validate_approval_defaults to take
a raw backend map, and reuse them via validate_security_approval_backends
called from finalize_profile, so both surfaces reject the same malformed
configs early with a clean error. No behavior change for valid configs.

Signed-off-by: connrg <conor@nolabs.ai>
@connrg
connrg merged commit 5c0886d into nolabs-ai:main Aug 19, 2026
17 checks passed
levonk pushed a commit to levonk/nono that referenced this pull request Sep 4, 2026
…pts (nolabs-ai#1677)

* feat(profile): route supervised approvals via security.approval_backends

Add security.approval_backends (named ApprovalBackendConfig map) and security.approval_defaults to the profile schema, decoupled from command_policies so configuring a backend does not activate the tool-sandbox.

Share build_approval_registry_from between the command_policies and profile paths so both surfaces resolve backends identically, including chain-cycle detection and webhook construction.

resolve_supervised_approval_backend returns Ok(None) when no backend is configured (fall back to the interactive terminal prompt), and a hard error when backends are configured but the default cannot resolve -- never a silent fallback to less-supervised behavior.

Merge semantics: child profiles union approval_backends; child approval_defaults win. Update the JSON schema, schema_shape test, and profile parse/merge tests.

* refactor(profile): simplify supervised approval selection and clarify docs

Collapse the supervised-mode backend selection to Option::unwrap_or
(same behavior and lifetimes as the prior match), and reword the
approval-backend comments and JSON schema descriptions in plainer
language while keeping the technical detail: decoupling from
command_policies (no tool-sandbox), and fail-closed resolution
(configured-but-unresolvable is a hard error, never a silent fall back
to the terminal prompt).

No functional change; clippy and the approval/profile/schema tests pass.

Signed-off-by: connrg <conor@nolabs.ai>

* fix(profile): validate security.approval_backends at profile load

command_policies approval backends are validated by
validate_command_policies at load/finalize (per-type field consistency,
missing webhook url, chain mode/children, self-chaining, references to
unknown backends, NUL-in-url, positive timeouts), but the security
surface added for supervised-mode routing skipped all of it -- a
malformed backend only errored later at supervised-launch build time,
and some cases (e.g. a terminal backend with a stray url) were not
caught at all.

Generalize validate_approval_backend/validate_approval_defaults to take
a raw backend map, and reuse them via validate_security_approval_backends
called from finalize_profile, so both surfaces reject the same malformed
configs early with a clean error. No behavior change for valid configs.

Signed-off-by: connrg <conor@nolabs.ai>

---------

Signed-off-by: connrg <conor@nolabs.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants