Skip to content

[ENG-4057] Share one outbound guard between protections and release it on stop - #300

Merged
patchstackdave merged 2 commits into
mainfrom
fix/egress-lifecycle
Sep 29, 2026
Merged

patchstackdave merged 2 commits into
mainfrom
fix/egress-lifecycle

Conversation

@patchstackdave

@patchstackdave patchstackdave commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Outbound request screening now works the same way however many protections a process creates.

Shared guard. fetch and node:http/node:https are process-wide, so each protection created with egress: true now registers its own screen with one shared guard.

  • A call is refused when any registered screen refuses it, and each protection reports its own refusals through its own onEgressBlock and detections. A host in one protection's allowHosts is still refused when another protection refuses it.
  • On the node:http path, a hostname is resolved once, and that resolution is checked by every screen that resolves the host. The connection is then pinned to addresses all of those screens accepted. A screen whose allowHosts lists the host still skips that host's DNS check, as before.
  • uninstallEgress() removes only that protection's screen. When the last screen leaves, the original functions are restored. As before, a wrapper that another library layered on top is never overwritten.

stop() and stopRefresh().

  • protection.stop() now also removes the protection's outbound screen. A protection created after another has stopped screens with its own rules.
  • protection.stopRefresh() stops only the rule refresh: the poll loop and its recovery retries. Outbound screening and the reporters keep running. It used to be an alias of stop(), so test cleanups that relied on the full shutdown now call stop().
  • AGENT-INSTALL.md and the type declarations describe both.

Unusual fetch input. A fetch call whose arguments the runtime's Request rejects is still handed to the underlying fetch unchanged. Its destination is now screened when it can be read (a URL string, a URL, or an object with url or href). When it cannot be read, the call is counted as an unscreened egress:unrecognised-request skip.

Validation: full suite (3,445 passed, 7 skipped), typecheck, build, and capabilities:check.

Field test outstanding. This changes AGENT-INSTALL.md, so the hostile field test must run immediately after the release that contains it. A run before that release would inspect the previously published docs, so it can't gate this change. Until then, the disclosure tests and capabilities:check are what cover the wording.

Refs ENG-4057

🤖 Generated with Claude Code

fetch and node:http(s) are process-wide, so every protection with
egress enabled now registers its own screen with one shared guard. A
call is refused when any registered screen refuses it, and each
protection reports its own refusals. Uninstalling removes only that
protection's screen; the last one out restores the original functions.
protection.stop() now uninstalls the protection's screen too.

A fetch call whose arguments the runtime's Request rejects is still
screened when its destination can be read, and is counted as an
unscreened call when it cannot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderbuds

coderbuds Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Cohesive implementation of a shared egress guard with thorough lifecycle handling.

🎯 Quality: 87% Excellent · 📦 Size: Large — consider splitting if possible

🛡️ Standards: Not checked — 404 lines changed, over your team's 400-line limit, and nothing checked before it was opened. Coding agents can call the assess-change-fit tool first, while a change this size is still cheap to split.

🤖 Authorship: Agent-written — Claude Code, going by its own attribution. Whether a person read it is unknown; coding agents can call the report-ai-usage tool to say.

📈 This month: Your 161st PR — above team average · Averaging Good

See how your team is trending →

stopRefresh() now stops only the rule refresh: the poll loop and its
recovery retries. The reporters and the protection's outbound screening
keep running; stop() still ends everything, including removing the
outbound screen. Test cleanups that relied on the full shutdown now call
stop().

Docs and declarations now say that when several protections screen
outbound calls, any one of them can refuse a call, so one protection's
allowHosts does not override another's refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@patchstackdave

Copy link
Copy Markdown
Contributor Author

/review

@patchstackdave
patchstackdave merged commit 5c735dc into main Sep 29, 2026
18 checks passed
@patchstackdave
patchstackdave deleted the fix/egress-lifecycle branch September 29, 2026 09:12
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