Skip to content

feat: make Firewall the shared approval authority - #336

Merged
dskvr merged 213 commits into
mainfrom
feat/firewall-approvals
Nov 10, 2026
Merged

dskvr merged 213 commits into
mainfrom
feat/firewall-approvals

Conversation

@dskvr

@dskvr dskvr commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Problem and result

Kehto's Firewall was effectively a rate limiter. This change makes Firewall the shared approval policy for NAP interfaces, scoped resources, scoped network destinations, and registered future categories. Rate limiting is an independent runtime budget. A manifest/interface grant no longer confers unrestricted resource or network access.

The runtime now coordinates prompts, trusted principals, lifetimes, pending-work bounds, persistence, revocation, cancellation, and request revalidation. Service handlers keep protocol validation and authorize the actual side effect or destination. Paja admits frames before executing bytes, shows the shared grant UI, and retires the exact source on revocation. Paja supports reviewed development origins through an exact closed CSP. The old Paja firewall simulation controls are rejected because they could not enforce these approvals.

Coverage and release

The change includes package/app consumers, NAP conformance guards, unit and real Chromium regression tests, package/host documentation, compatibility records, and changesets for shipped Firewall, runtime, shell, Paja, and services output.

  • pnpm build and pnpm type-check passed.
  • pnpm test:unit: 207 files, 2,625 tests passed.
  • KEHTO_HARNESS_PORT=43173 pnpm test:e2e: 133 Chromium tests passed. An unrelated local process occupied the default harness port.
  • pnpm docs:check, pnpm changeset status, git diff --check, and npx --yes aislop@0.12.0 scan -d passed; AI-slop score 100/100.

Protocol authority and bounded gaps

Checked the exact NIP-5D draft PR #2303 head 5560820fa4f37f45b8ab742a11aeb3001b17dc67; NAP-SHELL master 19de5ce1cea40cd4c76089cdd8126e9f87648049; NAP-RESOURCE PR #80 ae5b9c64ab8a4d8909f288829a01e99d6bd713bc; NAP-LINK PR #53 50a2cd45e76f06e87b87e93842badf4c61f17bde; NAP-CONNECT PR #19 ec81a3a4a1fc4da1ad2601215353b7f822e44b8f; NAP-NOTIFY PR #11 67877ef6c31ec8acee3125c1d8b4c781010e37dc; and NAP-CVM PR #52 b1ecaa529971ade6d5bc806bc0a990db0dc2356f. Other service refs and the consumer matrix are recorded in .planning/research/firewall-approvals.md.

The NIP-5D/NAP admission and service wire behavior is conformant to those checked refs. Kehto's directive-specific development CSP and approval UI are bounded host policy where the drafts do not prescribe one.

Review focus

Please examine exact principal binding and source retirement, once/session/persistent grant behavior, resource redirect and selected relay authorization, and CSP source matching. The browser tests cover required/optional admission, grant revocation, and approved and blocked network effects.

CI timeout follow-up

The isolation test completed its assertions but hung in HTTP fixture teardown on Node 20. Chromium can retain a speculative TCP connection without sending an HTTP request; server.close() then waits indefinitely. The fixture now stops listening and closes all remaining connections explicitly. CSP and network-denial assertions are unchanged.

Reproduced the original 90-second timeout on Node 20.20.2. The fix passed nine focused browser runs with tracing and all 133 Chromium tests under Node 20.20.2 (2.5 minutes, harness port 43373). Build, type-check, 2,625 unit tests, docs, diff checks, and pinned AI-slop 100/100 passed. Rechecked NIP-5D 5560820fa4f37f45b8ab742a11aeb3001b17dc67 and NAP-RESOURCE ae5b9c64ab8a4d8909f288829a01e99d6bd713bc; behavior remains conformant. This test-only fix changes no shipped package output.

The branch also incorporates main 47b2f1c8 to clear merge conflicts that prevented CI from starting. Conflict resolution preserves the released Paja catalog notifications, controller/title/FS diagnostics, and upload lifetime fixes alongside Firewall ownership. Upload cancellation exceptions cannot interrupt sibling cleanup; upstream upload regressions now wait for prepared uploads and preserve the unowned-status denial. New FS browser fixtures declare optional FS and use the shared session-approval driver. NAP-FS was checked at bd1b9e912f05203bf38f7a1b04f920744fe6d625, NAP-UPLOAD at f5cc981e527e2cc1572793a7a6e5082e5271ac49, and NAP-INTENT at 19de5ce1cea40cd4c76089cdd8126e9f87648049; behavior remains conformant.

Combined validation: build, type-check, 2,637 unit tests, docs, changesets, and AI-slop 100/100 passed. The complete 141-test Chromium run under Node 20.20.2 with CI settings passed (140 passed directly; one playground profile-loading test passed on its second retry). Earlier local runs encountered other playground page-load timeouts; no assertions or timeout thresholds were weakened.

Merging main again (playground retirement)

This merge brings in main at 1758524e, which clears the conflicts. Main retired @kehto/playground, so every playground edit on this branch is gone, along with the playground parts of this PR's description, docs, and tests. Earlier test counts above come from before this merge.

The rest needed real porting, not just conflict picking:

  • NAP-MEDIA: main rewrote the service, so the session approval moved onto the new registry. media.command is now approved too, since it's a napplet-to-shell effect on shell-owned sessions.
  • openStream: it relied on the origin-grant options this PR retired. It now goes through the same resource and network approvals as atomic requests.
  • Scoped Paja services: the runtime plans relay work without handleMessage, so publishes were skipping main's window-scoped service and losing their source context. Planning and revocation now reach it.
  • Media bytes: playback expected a Blob from resource.bytes, but this PR's wire carries base64, so playback now decodes it.
  • Smaller moves: the nested-context lock goes through admitBrowserDocument, and LLM denials and onCapabilityRevoked moved into the extracted modules.

A heads up on test fixtures: main's specs relied on undeclared domains, and each declared domain costs a prompt per load against the 12-per-minute budget, so they now declare only what they use. A signer change reloads the frame and asks again, which could get noisy for real apps that declare many domains. Might be worth a look.

Checked NAP-MEDIA PR #10 at 7806f438 for media.command direction and the policy end reason, and it's conformant. NAP-LLM at 3e10939 only got a read of its consent text; main has since moved its docs to 5e93bdf, and I didn't review that draft separately. NIP-5D draft #2303 has moved to c041f846; I didn't re-chase it here, so the refs above still stand.

Verification: pnpm build, pnpm type-check, 2,786 unit tests, and pnpm docs:check pass. All 122 Chromium tests pass with 1 skipped under CI=1; an earlier run had two tests pass only on retry, and they didn't recur in the final run. That was local Node, not a re-run on 20.20.2.

Known gaps:

  • AI-slop is 99/100. The one warning is installPajaLlmPanel length, unchanged from main.
  • createLlmService keeps NAP-LLM host consent and doesn't call authorizeOperation, so LLM generation isn't under the shared per-operation approval.
  • This merge doesn't address the open review findings from nrahulpub, and I didn't re-verify them against this head.

Merging main again (NAP-LLM chase)

This also brings in main at 8490d72c. The conflicts were only STATE.md and the NAP-LLM ref line in two READMEs, so no code needed porting. After this merge: build, type-check, docs, and 2,855 unit tests pass, and Chromium is 124 passed with 1 skipped under CI=1, with no retries needed. AI-slop stays at 99/100 with the same installPajaLlmPanel warning, in a file that matches main. The tool-use work main added also stays on NAP-LLM host consent, not the shared per-operation approval.

Merging main again (host firewall controls from #404)

This brings in main at 6eb1ea4f. Only three files conflicted, but #404 needed more than a merge, because most of it acts on the legacy gate and that gate never runs once permissions is set.

A couple of things you might want to weigh in on:

  • aclDefaultPolicy: 'restrictive' means a user can approve an interface at admission and then see every operation refused, with no prompt to explain it. It matches "nothing granted", but is that what we want, or should admission refuse sooner?
  • setRateLimit takes a whole config rather than mirroring runtime: setters for the firewall unfocusedMultiplier and burst guard #400's two setters, so hosts spread rateLimit to change one field. Either works if you'd prefer the two names.

Checked NAP-SHELL at napplet/naps@43cb3a4 (still master) for shell.ready and session binding, and it's conformant. No wire behavior changes here. I didn't re-chase the NIP-5D draft or the other refs above.

Verification: pnpm build, pnpm type-check, 2,952 unit tests, and pnpm docs:check pass. Chromium is 124 passed with 1 skipped under CI=1, with no retries. That was local Node, not 20.20.2. AI-slop stays at 99/100 with the same installPajaLlmPanel warning.

Known gaps:

  • A configured host gets no event when a stored deny grant refuses something. onRateLimit only covers budgets, so this part of shell: forward getFocusContext, onFirewallEvent and firewallPersistence through ShellAdapter #397's "show the user what was refused" isn't carried over.
  • The new budgets only use focused, so msSinceFocusGain from getFocusContext is ignored on configured hosts.
  • Paja and the e2e harness don't set filterNappletMessage, so the filter on a configured host is proven by unit tests, not in a browser.
  • nrahulpub's open review findings aren't re-verified against this head.

🤖 Generated with Claude Code

dskvr and others added 30 commits October 17, 2026 11:06
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Independent full-phase review remains pending; resource execution retains its existing atomic gate.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
- route production resource contacts through per-hop authorization\n- begin canonical base64 url/urls wire migration\n\nCo-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
48 focused proxy/server tests and Paja type checks pass. Resource group consumer migration and full combined gates remain outstanding.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
- use url/urls with padded base64 resource envelopes\n- authorize resolver destinations and proxy each Paja HTTP hop\n\nCo-Authored-By: Codex <noreply@openai.com>
Preserve Blossom fallback order, byte integrity, settings isolation, and compatibility warning assertions with canonical resource wire fields.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Regression currently fails: the network approval ends before body consumption, leaving its signal live after grant revocation. Resource execution must close this lifecycle gap before its combined gate.

Co-Authored-By: Codex <noreply@openai.com>
Both HTTPS and Blossom correctly prevent transport but currently misreport network approval denial as network-error instead of blocked-by-policy.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Retained network approval must allow a second hop within the same request while keeping revocation effective. The current held-operation implementation queues the second hop behind itself.

Co-Authored-By: Codex <noreply@openai.com>
dskvr and others added 3 commits October 19, 2026 07:41
Preserve current catalog intent notifications and all released host fixes. Keep exact upload ownership and stale-source suppression while isolating backend cancellation failures; migrate upstream regressions to prepared asynchronous uploads and retain the private unknown-status boundary.

Co-Authored-By: Codex <noreply@openai.com>
Declare the fixture filesystem domain as optional and approve session access before exercising OPFS and host-activated native pickers. Keep the missing-OPFS case runnable without advertising an unavailable domain.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
nrahulpub

This comment was marked as outdated.

@nrahulpub

Copy link
Copy Markdown
Collaborator

Note: This is just a slop review after I understood it on a high level. I haven't had the bandwidth to fully wrap my head around it. Day job :/.

I will do it asap.

@dskvr

dskvr commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

@nrahulpub had some useful bits in it still

dskvr and others added 10 commits October 26, 2026 08:14
Integrate current main while retaining both principal-bound admission and the
released snapshot identity and reliable sharing behavior. Preserve both task
histories in the planning state.

Confidence: high
Scope-risk: moderate
Tested: pnpm build; Paja runtime tab unit tests
Not-tested: Combined review fixes are validated in subsequent commits
Co-Authored-By: Codex <noreply@openai.com>
Route one-shot relay reads through the same frozen destination plans as subscriptions and hold approval cancellation through completion. Scope persisted revocations to matching principals and restore shared temporal budgets at runtime-owned storage, INC and CONFIG dispatch.

Constraint: NAP-RELAY query results retain correlated events or error responses

Rejected: Add approval without retaining query lifetime | would leave transport alive after revocation

Confidence: high

Scope-risk: moderate

Tested: Runtime type-check; 430 runtime tests; NIP-5D static conformance guard and query regressions

Co-Authored-By: Codex <noreply@openai.com>
Single-event OUTBOX reads now share planned authorization with queries, and configured authorization fails closed when a router cannot bind relay destinations. Relay service publishes require destination-aware transport. Theme and identity denial paths settle once using their canonical result shapes. Planned read cancellation closes all transport handles, including when one cleanup throws.

Constraint: NAP-IDENTITY getPublicKey results must not carry errors.
Rejected: Warn and execute an unplannable authorized router | relay destinations would remain unenforced.
Confidence: high
Scope-risk: moderate
Tested: 579 service tests, services type-check, 32 focused review regressions, pinned AI-slop 100/100, diff checks.
Not-tested: Live external relays and physical serial or BLE devices.
Co-Authored-By: Codex <noreply@openai.com>
Canonicalize Paja HTML aliases before issuing proxy capabilities, retain aborted resource signals across delayed network approval, and explain CSP scheme upgrades in shared prompts. Browser regressions distinguish default-port upgrades from Chromium’s stricter non-default-port matching.

Constraint: Browser CSP matching is not literal origin equality
Confidence: high
Scope-risk: narrow
Tested: 73 focused unit tests; shell, Paja and playground type-checks; 3 real Chromium CSP cases with ephemeral TLS fixture
Not-tested: Other browser engines
Co-Authored-By: Codex <noreply@openai.com>
Completing a previously silent denial must not send an old correlation id into a replacement frame. Track the source generation through pending theme authorization and retire it on window destruction or service unregistration.

Confidence: high
Scope-risk: narrow
Tested: Two lifecycle regressions reproduced before fix; 41 theme/review tests, services type-check, pinned AI-slop 100/100 and diff checks passed.
Co-Authored-By: Codex <noreply@openai.com>
Record bounded compatibility diagnostics and their actual regression suites,
link the resource proxy policy, and disclose CSP scheme matching and the
verified Chromium port distinction. Breaking relay/WebRTC changes need 0.x
minor releases. Keep test adapters on the explicit destination contract.

Confidence: high
Scope-risk: narrow
Tested: 2717 unit tests; build and type-check; docs:check; changeset status; AI-slop 100/100; git diff --check
Not-tested: Full browser gate still requires relay receipt integration follow-up
Co-Authored-By: Codex <noreply@openai.com>
Paja social-cache and Blossom decorators dropped the optional plan methods, so enforcing OUTBOX approval correctly denied otherwise supported host reads. Preserve the original plans and cancellation signals while retaining source-scoped cache augmentation and resource observation. Exercise fallback receipts with a separate approved authorless query instead of relying on speculative pre-approval traffic.

Constraint: Execute only relay destinations frozen before approval
Confidence: high
Scope-risk: narrow
Tested: 32 social-cache and Blossom unit tests; Paja type-check/build; all 18 paja-single-window Chromium tests
Related: #336
Co-Authored-By: Codex <noreply@openai.com>
Project throttled STORAGE and INC requests onto their sanctioned error results, keep cleanup available, and reject stale replies after source replacement. Observe CONFIG snapshots/schema registration and INC channel lists without dropping their required responses where the checked drafts define no rate-denial contract.

Constraint: NAP-STORAGE 4f4cf0ca and NAP-INC 19de5ce1 require correlated replies; NAP-CONFIG bed9cb5b has no budget-denial code

Rejected: Invent a CONFIG error code or empty snapshot | violates the checked protocol contract

Directive: CONFIG get/subscribe/registerSchema floods remain diagnostic-only until upstream defines denial or backpressure; registration can still validate and save repeatedly

Confidence: high

Scope-risk: narrow

Tested: Runtime type-check; 444 runtime tests in 27 files; diff checks

Co-Authored-By: Codex <noreply@openai.com>
CONFIG snapshots and schema acknowledgments, and INC channel listings, have
mandatory response contracts without a sanctioned temporal-denial envelope.
Preserve those responses and report excess budget use while enforcing the
other local-domain operations and leaving cleanup available.

Constraint: NAP-CONFIG bed9cb5b and NAP-INC 19de5ce1 require these replies
Rejected: Invent a rate-limit schema code or return empty snapshots | contradicts the checked NAP contracts
Confidence: high
Scope-risk: narrow
Directive: Repeated CONFIG registration still validates and saves; full throttling requires an upstream denial or backpressure contract
Tested: 2733 unit tests; build/type-check; docs:check; pinned AI-slop 100/100
Not-tested: Final combined Chromium suite is running
Co-Authored-By: Codex <noreply@openai.com>
Record each finding, its resolution, the exact protocol authority, and final
combined verification. Retain the CONFIG denial/backpressure gap and Chromium
CSP qualification so review completion cannot imply broader enforcement.

Confidence: high
Scope-risk: narrow
Tested: 2733 unit tests; 159 Chromium tests; build; type-check; docs; changesets; diff; pinned AI-slop 100/100
Not-tested: Firefox/WebKit; physical devices; production relay infrastructure
Co-Authored-By: Codex <noreply@openai.com>
@dskvr
dskvr requested a review from nrahulpub October 26, 2026 07:55
@dskvr

dskvr commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough pass. Addressed the numbered findings and actionable low items, including the main conflicts. The follow-up also covers cancellation/cleanup and duplicate or stale terminal replies.

Two qualifications:

  • For [hyprgate] Track: @napplet/vite-plugin Vite 8 compatibility — Phase 2 smoke test pending #6, CONFIG snapshots/schema ACKs and INC channel listings have mandatory replies but no throttling-error contract. They remain diagnostic-only, so repeated CONFIG schema registration can still validate/persist values. Other enforceable operations are limited and cleanup stays available. Fully throttling those mandatory calls needs an upstream denial/backpressure contract; this residual gap is documented.
  • CSP: Chromium 152 permits the default HTTP→HTTPS port upgrade, but rejects that upgrade on the same non-default port. Added browser regressions and disclosed portable CSP scheme matching in the prompt/docs.

Keeping the planning records here so the assessment and verification remain with the implementation.

Validation: 2,733 unit tests, 159 Chromium tests, build/type-check/docs/changesets/diff checks, and pinned AI-slop 100/100. Checked NAP-RELAY 9f30b350, OUTBOX 8865f73d, RESOURCE ae5b9c64, THEME/IDENTITY/INC/SHELL 19de5ce1, STORAGE 4f4cf0ca, CONFIG bed9cb5b, and NIP-5D 5560820f. Changed wire behavior is conformant; temporal budgets and CSP matching remain documented host policy.

Pushed 4a75cd03 and requested another review.

dskvr and others added 9 commits November 4, 2026 10:44
…ment

Main's playground retirement wins over every playground edit on this branch.
Firewall approvals move onto the code main rewrote: the NAP-MEDIA registry,
host-only openStream, and Paja's window-scoped services. Paja admission now
composes with port-bound identity, the frame-src lock, and the experimental
nested-context lock, and main's LLM denial and capability revocation move into
the modules this branch extracted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g together

The runtime plans relay work without handleMessage, so publishes skipped the
window-scoped service and lost their source context. Route selected planning
and execution through it. Playback now decodes the canonical base64 that
resource.bytes carries instead of expecting a Blob.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixtures declare only the domains they use and answer approvals. Atomic media
sources use a public URL through the proxy endpoint, since the proxy blocks
loopback by design. The visible error row is asserted on a mediated target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the Firewall README sections alongside main's NAP-LLM ref bump, and keep
this branch's planning position.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the host firewall and dispatch controls from #404. Keeps the canonical
NAP-RESOURCE wire over main's servers line in the shell docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#404's setters and firewall hooks act on the legacy gate, which never runs with
RuntimeAdapter.permissions set. Add runtime.permissions.setRateLimit, relax budget
validation to the contract #400 shipped, and warn once with
FW-RUNTIME-LEGACY-IGNORED-001 when legacy inputs do nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cover aclDefaultPolicy as an approval ceiling, and filterNappletMessage release
against budgets, revoked grants and retired sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Register FW-RUNTIME-LEGACY-IGNORED-001 and add #404's setters to the legacy gate's
retirement scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rewall approvals

Resolves conflicts from main (#376, #409, #411, #412):
- runtime.ts: keep main's createServiceRuntimeContext split and custom-domain
  push routing (pushCapabilities), and keep this branch's per-service
  lifetimes and authorizeOperation binding plus the permissions runtime.
- types.ts / service-types.ts: ServiceHandler lives in service-types.ts on
  this branch; carry main's pushCapabilities field over to it.
- relay-handler.ts: a planning relay service owns publish fan-out (main's
  ring-buffer skip); legacy relay services stay denied, so buffer fan-out
  still runs whenever the runtime coordinates the publish via the pool.
- STATE.md: keep both quick-task rows.

Port the new @kehto/mocks relay mock and main's relay delivery test to the
planned-execution relay contract (local-only plans, abort-aware execute),
since this branch denies legacy relay services (FW-RELAY-SERVICE-LEGACY-001).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dskvr
dskvr merged commit 249fe8c into main Nov 10, 2026
6 checks passed
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