fix: publish the endpoint guard through aiToolkit's public barrel - #5625
Conversation
|
Thanks for the well-specified issue — the reuse search and the exact call-site list made this a clean, mechanical fix to verify end-to-end. Happy to adjust anything or take on more of this module-hygiene series if useful. |
atomantic
left a comment
There was a problem hiding this comment.
Reviewed by /do:review (security-focused pass, external contributor) — 0 critical, 2 improvements. No merge blockers in the code; one design question for the maintainer, and a required rebase.
Rebase required before this can land. The branch is 188 commits behind main and currently CONFLICTING. In this repo a conflicted PR gets no CI at all ("no checks reported"), so nothing here has been verified by the pipeline. Both conflicts are mechanical import-line collisions (aiProvider.js, askService.js) where main added neighboring imports — resolution is obvious, no semantic overlap.
Verified locally instead (isolated worktree, rebased onto current main, conflicts resolved mechanically):
- Full
cd server && npm test→ 1809 files / 37284 tests pass, exit 0. - Every acceptance criterion in #5619 is met: barrel exports both functions; no
server/servicesproduction file importsinternal/endpointGuard.js; the guard contract test runs through the public export; toolkit-internalproviders.js/runner.jskeep their relative imports; no second implementation introduced;server/lib/README.mddocuments the owner and boundary. - Security scan: pure ASCII apart from em-dashes, no network calls, no
process.envreads, noeval/exec/child_processinvocation, no dependency or CI changes, no policy change to the guard itself — allow/block decisions, the evaluate-vs-assert split, keyless local-LLM behavior and theallowCustomEndpointopt-in are all untouched. Single commit, single author.
The one substantive question is on aiProvider.js — the barrel is the toolkit's composition root, so routing six hot services through it pulls 24 modules including express and child_process where the current deep import pulls 1 and nothing, three lines below a comment in that same file explaining why it avoids exactly this. A public sibling module (aiToolkit/endpointGuard.js, peer of errorDetection.js — which this file already imports directly) satisfies all four ACs at zero graph cost. #5619 named the barrel specifically, so this is a maintainer call on shape, not an error on your part; details in the inline comment.
Minor: the description's claim that it fixed "a stale test mock" in visionTest.frameGuard.test.js isn't accurate — that mock is correct on main and only goes stale because of this change. Worth rewording.
Coherence check: the rest of the description matches the diff, and the test plan holds up — I re-ran it. Note the README.md row is now a very long single table cell; consider trimming it to the sentence that matters ("public guard for provider endpoint URLs; import from the toolkit's public surface, not internal/").
Thanks for the clean, well-scoped change — the grep for remaining references and the barrel-import contract test were both the right instincts.
Generated by /do:review
Addresses the maintainer's /do:review design question on atomantic#5625: routing the six server/services/* consumers through aiToolkit/index.js (the toolkit's composition root) pulled 24 modules including express and child_process, where the previous direct import to internal/ pulled 1 module and nothing else - three lines below a comment in aiProvider.js explaining why it deliberately avoids exactly that. Moves evaluateSecretEndpoint/assertSecretEndpoint from aiToolkit/internal/endpointGuard.js to aiToolkit/endpointGuard.js - a peer of aiToolkit/errorDetection.js, which already follows this exact "pure toolkit-root module, importable directly AND re-exported via the barrel" shape. index.js now does `export * from './endpointGuard.js'` instead of a named re-export, matching errorDetection.js's own pattern. All 8 consumers repointed at the new path: the 6 services from the original PR (aiProvider.js, askService.js, insightsService.js, localLlmPlayground.js, visionTest.js, voice/llm.js, plus visionTest.frameGuard.test.js's mock target), and 2 toolkit-internal callers the original PR's scope didn't touch (providers.js, runner.js) - both were still importing the pre-move internal/ path directly and would have broken had they been left pointed at a path that no longer exists. Also updated 3 doc-comment references to the old path (providers.js, aiToolkit/validation.js, lib/validation.js) for accuracy; left one reference alone (scripts/migrations/ 195-cerebras-provider.js) since it's a historical, already-applied migration's own comment, not live code. Verified: the 12 directly relevant test files pass (583 tests, plus 5 pre-existing intentional skips); a repo-wide grep confirms zero remaining references to the old path outside that one historical migration comment; lib/aiToolkit/runner.test.js's own EBUSY failures reproduce identically with or without this change (pre-existing Windows temp-file-lock flakiness, already confirmed unrelated on sibling PRs atomantic#5626/atomantic#5627 via git stash). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
c965d81 to
7c16d3c
Compare
|
Went with the sibling-module approach from the review rather than the barrel — the reasoning was airtight (24 modules including `express`/`child_process` vs. 1, three lines below a comment in `aiProvider.js` explaining why it avoids exactly that), and `errorDetection.js` already established the exact pattern (`export * from './endpointGuard.js'`, same as its own line 31) so this isn't a new shape for the toolkit, just applying the existing one consistently. Also found and fixed 2 consumers outside the original PR's scope while doing the move: `aiToolkit/providers.js` and `aiToolkit/runner.js` were both still importing the old `internal/endpointGuard.js` path directly — moving the file without updating them would have broken both. PR description rewritten to match (it previously described the barrel approach). Also fixed the inaccurate "stale test mock" wording flagged in review. Verified: full `cd server && npm test` - same 16 files / 58 tests fail with or without this change (`git stash`-confirmed), all pre-existing Windows-only noise unrelated to anything here. 🤖 Generated with Claude Code |
Addresses the maintainer's /do:review design question on atomantic#5625: routing the six server/services/* consumers through aiToolkit/index.js (the toolkit's composition root) pulled 24 modules including express and child_process, where the previous direct import to internal/ pulled 1 module and nothing else - three lines below a comment in aiProvider.js explaining why it deliberately avoids exactly that. Moves evaluateSecretEndpoint/assertSecretEndpoint from aiToolkit/internal/endpointGuard.js to aiToolkit/endpointGuard.js - a peer of aiToolkit/errorDetection.js, which already follows this exact "pure toolkit-root module, importable directly AND re-exported via the barrel" shape. index.js now does `export * from './endpointGuard.js'` instead of a named re-export, matching errorDetection.js's own pattern. All 8 consumers repointed at the new path: the 6 services from the original PR (aiProvider.js, askService.js, insightsService.js, localLlmPlayground.js, visionTest.js, voice/llm.js, plus visionTest.frameGuard.test.js's mock target), and 2 toolkit-internal callers the original PR's scope didn't touch (providers.js, runner.js) - both were still importing the pre-move internal/ path directly and would have broken had they been left pointed at a path that no longer exists. Also updated 3 doc-comment references to the old path (providers.js, aiToolkit/validation.js, lib/validation.js) for accuracy; left one reference alone (scripts/migrations/ 195-cerebras-provider.js) since it's a historical, already-applied migration's own comment, not live code. Verified: the 12 directly relevant test files pass (583 tests, plus 5 pre-existing intentional skips); a repo-wide grep confirms zero remaining references to the old path outside that one historical migration comment; lib/aiToolkit/runner.test.js's own EBUSY failures reproduce identically with or without this change (pre-existing Windows temp-file-lock flakiness, already confirmed unrelated on sibling PRs atomantic#5626/atomantic#5627 via git stash). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
460aac5 to
8bf8744
Compare
|
Rebased onto current Ran the full |
8bf8744 to
c5a3f4e
Compare
atomantic
left a comment
There was a problem hiding this comment.
Approving. Second security-focused pass on the revised branch — 0 critical, 0 blocking. Both prior threads are resolved, and the sibling shape you landed is the right one.
I rebased the branch for you (it was CONFLICTING against current main) and force-pushed the result to your fork — your authorship is preserved on the single squashed commit c5a3f4e. Two things changed in that rebase:
insightsService.jsdropped out of the diff entirely.maincommit7bc373f("dedupe insightsService's transport onto the shared aiProvider one") removed that service's guard call altogether, so its import line no longer exists to repoint. That was the whole conflict. The consumer count is now five services, not six — nothing wrong on your end, justmainmoving underneath you.- Squashed the two commits into one. The second commit reversed the first's approach, so
git log --onelineread as "publish through the barrel" immediately followed by "…actually don't." This repo derives release notes from commit subjects, so the intermediate state would have shipped as a note describing a design the tree doesn't have.
Security review (untrusted-contributor pass)
The thing that matters here is that this touches the policy deciding whether a paid API key gets attached to a provider endpoint. It does not:
git diff main:internal/endpointGuard.jsvsendpointGuard.jsis the doc comment and nothing else — every allow/block decision, the metadata block, theevaluatevsassertsplit, keyless local-LLM behavior, and theallowCustomEndpointopt-in are byte-identical.endpointGuard.test.jsis a 100% pure rename — same 140 lines, same contract, and it still resolves./endpointGuard.jsrelatively at the new location.- The diff is import lines only. No call site was moved, reordered, or dropped; all five services still call the guard at the same points.
- No
package.json/ lockfile / CI / workflow changes. No network calls, noprocess.envreads, noeval/exec/child_process. - Scanned every added line for zero-width, bidi-override, and homoglyph characters — clean; the only non-ASCII are em-dashes in prose.
- Zero remaining references to the old path outside
scripts/migrations/195-cerebras-provider.js:8, which you correctly left alone as an already-applied migration's own historical comment.
Ticket check (#5619)
Every acceptance criterion is met, and the shape is better than the one the ticket prescribed:
| AC | Status |
|---|---|
| Barrel publicly exports both functions; README names the owner and boundary | ✔ export * from './endpointGuard.js' |
No server/services production file imports internal/endpointGuard.js |
✔ |
| Guard test runs through the public surface, full coverage retained | ✔ (the file itself is now that surface) |
| Service consumers pass their existing tests | ✔ 177/177 locally on the rebased tree; server CI green |
| No second implementation introduced | ✔ moved, not duplicated |
The ticket's Fix paragraph said to keep the file under internal/ and route services through index.js. You did the opposite and that is the correct call — the barrel is the composition root (24 modules, express + child_process), and errorDetection.js is the standing precedent for "pure toolkit-root module, direct-importable and barrel re-exported." Ticket text, not your implementation, was wrong.
Non-blocking — I'll take these in a follow-up PR, no action needed from you
scripts/migrations/195-cerebras-provider.js:8still points ataiToolkit/internal/endpointGuard.js, a path that no longer exists. Historical or not, the comment is now a dangling reference.- The
server/lib/README.mdcell is a four-clause sentence inside a table row. The rationale is good but belongs in the module's own header comment, where it already is. - Nothing asserts the
index.jsre-export actually resolves — onlybootstrap.jsimports the barrel, so a rename would fail at server boot rather than in a test. One import through../index.jsin the guard test would pin AC 1 mechanically.
Heads up on the red client check
Client tests and build fails on api.deadExports.test.js (getPreferredSelfRestartOrigin, pullAndUpdateApp). Not yours — those wrappers are orphaned on main itself and this PR touches zero client files. Filed as #5848; merging around it.
Thanks — clean, well-scoped, and you made the design call instead of bouncing it back.
…top deep-importing internal/ `evaluateSecretEndpoint`/`assertSecretEndpoint` — the SSRF/key-exfiltration guard that decides whether a paid API key may be attached to a provider `endpoint` — lived at `aiToolkit/internal/endpointGuard.js`, yet PortOS service modules imported it from that private path. Any toolkit resync or internal reshuffle would have required a manual multi-service migration, with a runtime import failure as the failure mode. Move it to `aiToolkit/endpointGuard.js`, a public peer of `errorDetection.js` — the module that already establishes the "pure toolkit root module, direct-importable and barrel re-exported" shape. `index.js` adds the matching `export *`, so the barrel advertises the surface without any consumer paying for the composition root's route/provider/runner graph to reach one pure URL check. Guard logic and its test are byte-identical to before: same allow/block decisions, same evaluate-vs-assert split, same keyless local-LLM behavior, same `allowCustomEndpoint` opt-in. Fixes atomantic#5619
c5a3f4e to
2180ad2
Compare
…ferences to its old internal/ path Follow-ups from the #5625 review, none of which belonged in an external contributor's PR: - `index.js`'s `export * from './endpointGuard.js'` was asserted by nothing. `bootstrap.js` is the only barrel importer, so dropping that line would have failed a live server start rather than a test. The new case loads the barrel lazily and checks both functions resolve to the same identities as the direct import — verified by deleting the re-export and watching it fail. - `scripts/migrations/195-cerebras-provider.js` still pointed at `aiToolkit/internal/endpointGuard.js`, a path that no longer exists. - Trimmed the `server/lib/README.md` row back to the one sentence a reader needs; the graph-cost rationale already lives in the module's own header.
Summary
evaluateSecretEndpoint/assertSecretEndpoint(the SSRF/key-exfiltration guard for providerendpointURLs) lived only at the toolkit's privateinternal/endpointGuard.jspath, yet sixserver/services/*modules imported it from there directly.aiToolkit/index.js(the toolkit's composition root — 24 modules includingexpress/child_process, vs. the 1-module direct import this replaces), the guard moves toaiToolkit/endpointGuard.js— a public sibling ofaiToolkit/errorDetection.js, which already follows this exact "pure toolkit-root module, direct-importable AND barrel re-exported" shape.index.jsnow doesexport * from './endpointGuard.js', matchingerrorDetection.js's own re-export line.aiProvider.js,askService.js,insightsService.js,localLlmPlayground.js,visionTest.js,voice/llm.js),visionTest.frameGuard.test.js's mock target, plus 2 toolkit-internal callers (providers.js,runner.js) that were still pointed at the pre-moveinternal/path and would have broken otherwise.server/lib/README.md; updated 3 doc-comment references to the old path for accuracy. Left one reference alone (scripts/migrations/195-cerebras-provider.js) since it's a historical, already-applied migration's own comment, not live code.visionTest.frameGuard.test.js) pinned to the old internal path, found while grepping the whole repo for any remaining reference — that mock was correct onmainand only went stale as a result of this change, not before it.Test plan
npx vitest runacross all 12 directly affected files — 583 tests pass (plus 5 pre-existing intentional skips)aiToolkit/internal/endpointGuard.jsoutside the one historical migration comment noted abovecd server && npm test: same 16 files / 58 tests fail with or without this change (confirmed viagit stash) — all pre-existing Windows-only environment noise (symlink creation needs Developer Mode privileges,EBUSYtemp-file races, one missing local Python module), none touching anything this PR changesFixes #5619