feat(compiler): SYN007 — warn on fetch() calls that bypass the net capability model (?bs 0.7+)#158
Merged
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new compiler diagnostic (SYN007) to warn on fetch(url) / fetch?.(url) calls inside function bodies at ?bs 0.7+, since fetch performs network I/O that bypasses CAP001’s declared http.* surface and therefore the uses { net } capability model.
Changes:
- Add SYN007 registry metadata (rule/idiom/rewrite/example) and MCP
explaincoverage. - Implement SYN007 detection in the consolidated
passSynChecksingle-dispatch token loop. - Add a dedicated SYN007 test suite and update “known codes” allowlists/docs.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Adds SYN007 to the MCP explain tool’s documented code list. |
| packages/mcp/tests/server.test.ts | Adds SYN007 to the MCP server’s known-codes test list. |
| packages/mcp/src/explanations.ts | Adds long-form MCP explanation content for SYN007. |
| packages/compiler/tests/syn007-check.test.ts | New test suite validating core SYN007 detection/exclusions. |
| packages/compiler/tests/error-codes.test.ts | Adds SYN007 to the compiler error-code registry allowlist test. |
| packages/compiler/src/passes/syn-check.ts | Implements SYN007 detection in the syn-check dispatch loop. |
| packages/compiler/src/error-codes.ts | Adds the SYN007 error-code entry (rule/idiom/rewrite/example). |
| AGENTS.md | Documents SYN007 in the diagnostics table for agents/tooling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+539
to
+547
| if (callTok7.matchedAt !== undefined) { | ||
| const afterCloseIdx7 = nextSignificant(tokens, callTok7.matchedAt + 1); | ||
| const afterClose7 = tokens[afterCloseIdx7]; | ||
| if (afterClose7 && ( | ||
| (afterClose7.kind === "open" && afterClose7.text === "{") || | ||
| afterClose7.kind === "fatArrow" || | ||
| (afterClose7.kind === "punct" && afterClose7.text === ":") | ||
| )) continue; | ||
| } |
Comment on lines
+43
to
+44
| * Excluded: member calls (`obj.fetch`), function/fn declarations named | ||
| * `fetch`, and object/class method shorthands. |
| const result = compile(src); | ||
| expect(result.warnings.filter((w) => w.code === "SYN007").length).toBe(2); | ||
| }); | ||
| }); |
|
|
||
| // Exclude method shorthands and TS method signatures: { fetch(url) { } } / { fetch(url): T; } | ||
| // Guard the `:` check against ternary consequents: `cond ? fetch(url) : other` | ||
| const isTernaryConsequent7 = prev7 !== undefined && prev7 !== null && prev7.kind === "question"; |
…pability model (?bs 0.7+)
`fetch()` makes HTTP requests at runtime but is invisible to CAP001,
which only checks `http.*` member calls. A fn that calls fetch has an
undeclared network dependency — no `uses { net }` covers it, and no
audit tool can observe it from the fn header.
Detection: `fetch` not preceded by `.`/`?.`, followed by `(` or `?.(`.
Member calls, method shorthands, TS method signatures, fn/function
declarations named `fetch`, and bare references are excluded. Suppressed
in `unsafe {}` blocks and `unsafe "reason" fn` bodies.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tection Copilot flagged three issues: 1. Method-signature exclusion used `:` check without guarding against ternary consequents (`cond ? fetch(url) : other` would silently suppress SYN007). Fixed by checking `prev7.kind === "question"` before treating `:` as a method signature marker, mirroring the SYN004 Function/eval guard. 2. Header comment omitted the TS method signature exclusion. Updated. 3. Added regression test: `fetch()` in both arms of a ternary expression must fire SYN007 twice (was incorrectly suppressed before the fix). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ternary-consequent guard for SYN007 only looked one token back for `?`. When the call is `await fetch(url)`, the token immediately before `fetch` is `await` (kind "ident", not "keyword"), so the guard missed the `?` one position further back — causing the ternary `:` to silently suppress the warning as a false TS method-signature match. Fix: when prev is `await` (kind "ident"), peek one more token back; if that is `?` the call is still a ternary consequent and SYN007 should fire. Adds regression test: `fires on await fetch() inside a ternary expression`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
f75f37c to
9974e29
Compare
Comment on lines
+682
to
+686
| rule: | ||
| "`fetch(url)` and `fetch?.(url)` make HTTP requests at runtime but are invisible to " + | ||
| "botscript's capability model: CAP001 checks for `http.*` member calls, not the `fetch` " + | ||
| "global. A fn that calls `fetch` has an undeclared network dependency — no `uses { net }` " + | ||
| "declaration in the header covers it, and no audit tool can observe it from the fn header.", |
Comment on lines
+679
to
+681
| SYN007: { | ||
| code: "SYN007", | ||
| title: "fetch() call bypasses the net capability model", |
| | SYN004 | (0.7+, warning) A fn body calls `eval(...)` / `eval?.(...)` (global eval not preceded by `.`/`?.`) or calls `Function(...)` / `Function?.(...)` / `new Function(...)` (Function constructor not preceded by `.`/`?.`). All forms execute strings as code at runtime — every static capability check (CAP001/CAP002), resource declaration (reads/writes), and safety check (SYN002/SYN003) can be bypassed by routing any unsafe pattern through eval or the Function constructor. Suppressed inside `unsafe {}` blocks and `unsafe fn` bodies. `.eval(...)` (method call on a local) and `Function.*` member accesses are excluded. | Refactor the eval-based pattern to use explicit code paths. If eval is genuinely required (e.g. sandboxed interpreter), wrap in `unsafe "<reason>" { eval(...) }`. | | ||
| | SYN005 | (0.7+, warning) A fn body accesses `process.env`. `process.env` is a global deployment-environment namespace — access is invisible to callers and to static analysis; no capability or resource declaration covers it, so the fn has an undeclared dependency on deployment configuration. Detection: `process` not preceded by `.`/`?.`, followed by `.`/`?.` then `env`. `obj.process.env` (member access on a local), `unsafe {}` blocks, and `unsafe "reason" fn` bodies are excluded. | Pass config and secrets as explicit fn parameters so the dependency is visible in the call signature; if env access is required at the load site, wrap in `unsafe "reads deployment env" { }`. | | ||
| | SYN006 | (0.7+, warning) A fn body calls `process.exit()`, `process?.exit()`, or `process.exit?.()`. All forms terminate the entire host process — not just the fn, not just the bot. They produce no return value, bypass `Result` propagation, `throws {}`, `match`, and any caller recovery path. No capability declaration covers them. Detection: `process` not preceded by `.`/`?.`, followed by `.`/`?.` then `exit` then `(` or `?.(`. `obj.process.exit(...)`, `process.exit` without `(`, and `process.exitCode` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Return `err(...)` and let the caller decide whether to terminate. If `process.exit` is genuinely required at a bootstrap entry point, wrap in `unsafe "exits on invalid config" { process.exit(1) }`. | | ||
| | SYN007 | (0.7+, warning) A fn body calls `fetch(url)` or `fetch?.(url)`. `fetch` makes HTTP requests at runtime but is invisible to CAP001, which only checks `http.*` member calls. A fn that calls `fetch` has an undeclared `net` dependency — no `uses {}` declaration covers it. Detection: `fetch` not preceded by `.`/`?.`, followed by `(` or `?.(`. Member calls (`obj.fetch(...)`), object method shorthands (`{ fetch(url) {} }`), TypeScript method signatures, fn/function declarations named `fetch`, and bare `fetch` references are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Replace `fetch(url)` with `http.get(url)` / `http.post(url, { body })` and add `uses { net }` to the fn header. If the native fetch API is required, wrap in `unsafe "calls fetch directly" { fetch(url) }`. | |
- Reorder SYN007 to sit between SYN006 and SYN010 (restore numeric order)
- Fix rule text: CAP001 cannot infer/require uses{net} from fetch calls;
a fn can already declare uses{net} — the issue is CAP001 won't detect
a missing declaration from fetch usage
- Update AGENTS.md description to match corrected framing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
fetch(url)orfetch?.(url)at?bs 0.7+fetchmakes HTTP requests at runtime but is invisible to CAP001, which only checkshttp.*member calls. A fn that callsfetchhas an undeclarednetdependency — nouses { net }covers itWebSocket(SYN008) andXMLHttpRequest(SYN009): real network effects invisible to the declared capability surfaceChanges
packages/compiler/src/error-codes.tspackages/compiler/src/passes/syn-check.tscase "fetch":in dispatch loop with member call/method shorthand/TS method signature/unsafe exclusionspackages/compiler/tests/syn007-check.test.tspackages/compiler/tests/error-codes.test.tspackages/mcp/src/explanations.tspackages/mcp/tests/server.test.tsAGENTS.mdREADME.mdexplaintool code listTest plan
pnpm -r build && pnpm test— 1250 tests pass (43 test files)fetch(url)await fetch(url)fetch?.(url)?bs 0.7unsafe {}blocks orunsafe fnbodiesobj.fetch(...)(member call on a local)fetchreference (not called)fetchfetchfn fetch(...)botscript declarationsSupersedes PR #148.
🤖 Generated with Claude Code