Skip to content

feat(compiler): SYN007 — warn on fetch() calls that bypass the net capability model (?bs 0.7+)#158

Merged
marcelofarias merged 4 commits into
mainfrom
botkowski/syn007-v2
Jun 13, 2026
Merged

feat(compiler): SYN007 — warn on fetch() calls that bypass the net capability model (?bs 0.7+)#158
marcelofarias merged 4 commits into
mainfrom
botkowski/syn007-v2

Conversation

@marcelofarias

Copy link
Copy Markdown
Owner

Summary

  • Adds SYN007: warns when a fn body calls fetch(url) or fetch?.(url) at ?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 net dependency — no uses { net } covers it
  • Same bypass class as WebSocket (SYN008) and XMLHttpRequest (SYN009): real network effects invisible to the declared capability surface
  • Implemented in the single dispatch loop (from PR perf(syn-check): consolidate 6 per-check token scans into a single dispatch loop #155), matching the SYN008-v2 pattern

Changes

File Change
packages/compiler/src/error-codes.ts Add SYN007 entry (rule, idiom, rewrite, example)
packages/compiler/src/passes/syn-check.ts Add case "fetch": in dispatch loop with member call/method shorthand/TS method signature/unsafe exclusions
packages/compiler/tests/syn007-check.test.ts 13 tests: call forms, unsafe suppression, member call exclusion, method shorthand exclusion, TS method signature exclusion, fn declaration exclusion, bare reference, version gate, severity, multi-call count
packages/compiler/tests/error-codes.test.ts Add SYN007 to exhaustive allowlist
packages/mcp/src/explanations.ts Add SYN007 long-form explanation
packages/mcp/tests/server.test.ts Add SYN007 to KNOWN_CODES list
AGENTS.md Add SYN007 row to diagnostic table
README.md Add SYN007 to explain tool code list

Test plan

  • pnpm -r build && pnpm test — 1250 tests pass (43 test files)
  • SYN007 fires on fetch(url)
  • SYN007 fires on await fetch(url)
  • SYN007 fires on optional-call fetch?.(url)
  • SYN007 does NOT fire below ?bs 0.7
  • SYN007 does NOT fire inside unsafe {} blocks or unsafe fn bodies
  • SYN007 does NOT fire on obj.fetch(...) (member call on a local)
  • SYN007 does NOT fire on bare fetch reference (not called)
  • SYN007 does NOT fire on object method shorthands named fetch
  • SYN007 does NOT fire on TypeScript method signatures named fetch
  • SYN007 does NOT fire on fn fetch(...) botscript declarations

Supersedes PR #148.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 explain coverage.
  • Implement SYN007 detection in the consolidated passSynCheck single-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);
});
});

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.


// 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";
Marcelo Farias and others added 3 commits June 12, 2026 23:50
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment thread packages/compiler/src/error-codes.ts Outdated
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 thread packages/compiler/src/error-codes.ts Outdated
Comment on lines +679 to +681
SYN007: {
code: "SYN007",
title: "fetch() call bypasses the net capability model",
Comment thread AGENTS.md Outdated
| 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@marcelofarias marcelofarias merged commit db44d44 into main Jun 13, 2026
4 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