Skip to content

fix(tasks): declare the owned FetchUrlTask entitlements on FileLoaderTask - #847

Merged
sroussey merged 1 commit into
mainfrom
claude/zealous-allen-ata6g5-fileloader-entitlements
Aug 20, 2026
Merged

fix(tasks): declare the owned FetchUrlTask entitlements on FileLoaderTask#847
sroussey merged 1 commit into
mainfrom
claude/zealous-allen-ata6g5-fileloader-entitlements

Conversation

@sroussey

Copy link
Copy Markdown
Collaborator

The owned-child snapshot gap

computeGraphEntitlements takes its snapshot over graph.getTasks() at graph start, before any execute() runs. A child created inside execute() via context.own(...) is therefore never in that snapshot, so its entitlements are invisible to the enforcer unless the owning task declares them itself. That is exactly what fetchUrlEntitlementsFor exists for, and its docblock says so.

The cross-platform FileLoaderTask owns a FetchUrlTask inside execute() and declared nothing at all — no static entitlements(), no instance entitlements(), no hasDynamicEntitlements.

Why this one was missed

The recent batch added the owned-child declaration to FileGrepTask, FileSedTask and FileLoaderTask.server — the two grep/sed cross-platform bases and the loader's server subclass. The loader's cross-platform base is the one that fell through the gap, and it is the class browser.ts registers into TaskRegistry, so the build with no filesystem branch at all — the one whose entire surface IS the owned fetch — was the one declaring none of it.

Failure scenario

Under enforceEntitlements: true with a registered enforcer, a graph containing a browser-build FileLoaderTask pointed at http://169.254.169.254/latest/meta-data/… runs to completion. The enforcer sees an empty declaration for the node; the owned FetchUrlTask then runs with allowPrivate: true and its own resolved-destination check is satisfied, because the child's input url IS the private one. The link-local metadata endpoint is reached while the task declared neither network:http nor network:private — the destination the enforcer exists to gate never reaches it.

The fix

FileLoaderTask.ts gains the same three members FileGrepTask carries: hasDynamicEntitlements, a static entitlements() returning FetchUrlTask.entitlements(), and an instance entitlements() returning fetchUrlEntitlementsFor(this.runInputData?.url) — which fails closed to an unscoped network:private when the url is not yet known.

FileLoaderTask.server.ts now merges its filesystem:read onto super.entitlements() instead of restating FetchUrlTask.entitlements(), matching FileGrepTask.server.ts and FileSedTask.server.ts. The base now supplies the same value, so this is behaviour-preserving; it removes the second place to edit when FetchUrlTask's declaration changes. FetchUrlTask became unused in that file and was dropped from the import. The server's instance entitlements(), which already handles the http-vs-path branches, is unchanged.

Breaking Changes

Under enforcement, a graph containing a browser-build FileLoaderTask that previously ran because it declared nothing will now be denied unless the profile grants network:http / network:private. That is the fix — the task always did the network access, it just never said so — but it is a behaviour change for any embedder running enforceEntitlements: true with a policy that did not grant the fetch. It belongs under a Breaking Changes CHANGELOG heading for @workglow/tasks, alongside 0.3.48's "contain the server filesystem tasks by default".

The server build's effective declaration is unchanged, and nothing changes for an embedder not running with an enforcer.

Test

New packages/test/src/test/task/FileLoaderEntitlements.test.ts, modelled on FileGrepEntitlements.test.ts. @workglow/tasks resolves to the node build under vitest, so the cross-platform class is imported from its own module (../../../../tasks/src/task/FileLoaderTask) — with the precedent of HFT_CheckpointSessions.test.ts. registerSafeFetch is stubbed on that same source module, since the source FetchUrlTask holds a different safeFetch slot from the built package's; the enforcer is what is under test, not the network.

Five pins: the metadata-endpoint denial, a public load that still resolves, the http declaration (network:http and no filesystem:read), the fail-closed unscoped network:private for an unknown url, and — for the super.entitlements() consolidation — that the server build still carries both network:http and filesystem:read.

Three of the five fail on main (verified by stashing the source change), including the enforcement pin:

 × a profile without network:private denies a metadata-endpoint load
 × declares the fetch entitlements for an http url
 × declares fail-closed network:private when the url is unknown

AssertionError: promise resolved "[ { id: 'loader-node', …(2) } ]" instead of rejecting
AssertionError: expected [] to include 'network:http'
AssertionError: expected [] to include 'network:private'

 Test Files  1 failed (1)
      Tests  3 failed | 2 passed (5)

With the fix, bun scripts/test.ts task vitest:

Running all tests in sections [task] — 75 file(s)

 Test Files  75 passed (75)
      Tests  1229 passed | 24 skipped (1253)
   Duration  193.95s

🤖 Generated with Claude Code

https://claude.ai/code/session_01LowBJQsCghLDiHwPN6FgUT


Generated by Claude Code

…Task

The cross-platform FileLoaderTask owns a FetchUrlTask created inside
execute(), so it is absent from the graph-start snapshot
computeGraphEntitlements takes over graph.getTasks(). With no static or
instance entitlements() the task declared nothing, and under
enforceEntitlements a graph containing the browser-build class reached a
private destination without the enforcer ever seeing it.

Declare the fetch's entitlements on the base class, mirroring FileGrepTask,
and consolidate the server build's static declaration onto
super.entitlements().

BREAKING CHANGE: under enforceEntitlements, a graph containing a
browser-build FileLoaderTask that previously ran because it declared
nothing is now denied unless the profile grants network:http /
network:private.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LowBJQsCghLDiHwPN6FgUT
@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 58.25% 33518 / 57537
🔵 Statements 57.88% 35044 / 60539
🔵 Functions 59.56% 6492 / 10899
🔵 Branches 46.58% 16966 / 36418
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/tasks/src/task/FileLoaderTask.ts 19.82% 10.05% 36.36% 21.59% 16-27, 129, 151, 159, 170, 182, 204-358, 379, 395-454, 464-486, 493-501, 513, 516-561, 593
Generated in workflow #3248 for commit 5e2467a by the Vitest Coverage Report Action

@sroussey
sroussey merged commit 0afc1dd into main Aug 20, 2026
15 checks passed
@sroussey
sroussey deleted the claude/zealous-allen-ata6g5-fileloader-entitlements branch August 20, 2026 15:39
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