Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ parse the resulting `{ ok: false, diagnostics: [...] }` envelope.
| SYN019 | (0.7+, warning) A fn body calls `crypto.getRandomValues(buf)` or `crypto.randomUUID()` (including optional-chain forms `crypto?.getRandomValues(buf)` and optional-call forms `crypto.getRandomValues?.(buf)`). These calls generate cryptographic randomness at runtime but are invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib calls (`random.next()`, `random.int()`), not the `crypto` global. A fn that calls these methods has an undeclared randomness dependency — tests cannot control the output and callers cannot observe the dependency from the fn header. Detection: `crypto` ident not preceded by `.`/`?.`, followed by `.` or `?.`, followed by `getRandomValues` or `randomUUID`, followed by `(` or `?.(`. Member calls (`obj.crypto.getRandomValues(...)`), non-randomness members (e.g. `crypto.subtle.digest(...)`), bare references, and `fn`/`function` declarations named `crypto` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Use `random.next()` or `random.int(min, max)` from the `random` stdlib with `uses { random }` when general randomness is sufficient. If cryptographic randomness or UUIDs are genuinely required, wrap in `unsafe "uses crypto for <reason>" { crypto.getRandomValues(buf) }`. |
| SYN022 | (0.7+, warning) A fn body accesses `process.argv`, `process.cwd()`, `process.platform`, `process.arch`, `process.pid`, `process.ppid`, `process.version`, `process.versions`, `process.hrtime()`, `process.uptime()`, `process.memoryUsage()`, `process.cpuUsage()`, or `process.resourceUsage()`. These read ambient Node.js process state at runtime — OS identity, process tree, memory, CPU — invisible to botscript's capability model. Unlike `process.env` (SYN005) and `process.exit` (SYN006), which cover configuration and termination respectively, SYN022 targets the remaining ambient introspection surface. Detection: `process` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member in the ambient-state set, optionally followed by `(` or `?.(`. Bare `process.*` references without a trailing `(` still fire. `obj.process.*` member calls, `unsafe {}` blocks, and `unsafe "reason" fn` bodies are excluded. `process.env` and `process.exit` are handled by SYN005/SYN006 and excluded here. | Pass the required ambient value as an explicit fn parameter so the dependency is visible in the call signature and tests can inject a mock. If direct `process.*` access is required at a bootstrap entry point, wrap in `unsafe "reads process state for <reason>" { process.argv }`. |
| SYN023 | (0.7+, warning) A fn body accesses a high-concern `navigator.*` member: `geolocation`, `clipboard`, `mediaDevices`, `serviceWorker`, `permissions`, `onLine`, `userAgent`, `language`, `languages`, `platform`, `hardwareConcurrency`, `deviceMemory`, `connection`, or `wakeLock`. These expose ambient browser capability state — location, clipboard, media devices, background service workers, network connectivity, browser identity, and hardware specs — invisible to botscript's capability model. Detection: `navigator` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member in the high-concern set. `obj.navigator.*` member calls, `fn`/`function`/`function*` declarations named `navigator`, members not in the listed set, `unsafe {}` blocks, and `unsafe "reason" fn` bodies are excluded. | Pass the required value as an explicit fn parameter so callers can see the dependency and tests can inject a mock. If direct `navigator.*` access is required, wrap in `unsafe "accesses navigator.<member> for <reason>" { navigator.<member> }`. |
| SYN028 | (0.7+, warning) A fn body uses a browser navigation API: `window.open()`, `globalThis.open()`, `self.open()`, `location.href = url`, `location.assign()`, `location.replace()`, or `location.reload()`. All cause real network activity at runtime but are invisible to botscript's capability model — none are covered by `uses { net }`, `reads {}`, or `writes {}`. In bot/agent contexts, prompt-injected content can redirect users to phishing pages or exfiltrate data as URL parameters via these APIs. Detection: `location` ident (not preceded by `.`/`?.`) followed by `.`/`?.` and `href =`, `assign(`, `replace(`, or `reload(`; OR `open` ident preceded by `.`/`?.` from an ambient receiver (`window`, `globalThis`, `self` — not itself a member access). `obj.location.*`, `obj.window.open`, fn/function/function* declarations named `location` or `open`, method shorthands, `unsafe {}` blocks, and `unsafe "reason" fn` bodies are excluded. | Pass a navigation callback as an explicit parameter so callers can see the dependency, or wrap in `unsafe "navigates for <reason>" { location.href = url }` when direct navigation is required at an entry point. |
| INT002 | (0.7+) A fn declares `intent: "pure"` but its body directly references a stdlib capability (e.g. `http.get`, `fs.read`). Pure intent is enforced at the body level as well as the header. | Remove the stdlib call from the body, or change the intent. |
| INT003 | (0.7+) A fn declares `intent: "idempotent"` but also has `uses { random }` or `uses { time }`. Both capabilities produce different values on each call, making the function non-idempotent. Only `random` and `time` are flagged; other capabilities are not structurally flagged by this check (INT003 is a narrow heuristic, not a proof of idempotence). | Remove `random`/`time` from `uses {}`, or change the intent. |
| INT004 | (0.7+) A fn declares `intent: "idempotent"` but its body directly references `random` or `time` without declaring them. Under-declaration variant of INT003 — fires when INT003 does not. | Remove the non-idempotent call from the body, or declare the capability and remove the idempotent intent. |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ claude mcp add botscript -- npx -y @mbfarias/botscript-mcp
| ----------- | -------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `primer` | (no args) | The canonical language primer (same text the `?primer` directive emits). |
| `transform` | `{ source: string, filename?: string }` | `{ ok: true, code, forms, version, warnings: [...] }` on success, or `{ ok: false, diagnostics: [...] }` on failure. `warnings` is an array of non-blocking diagnostics (e.g. CAP003). |
| `explain` | `{ code: string }` | Long-form explanation for any stable diagnostic code (`ALI001`, `ALI002`, `ALI003`, `BS001`, `BS002`, `CAP001`–`CAP003`, `DEP001`–`DEP004`, `EFF002`–`EFF004`, `FMT001`, `INT001`–`INT005`, `MAT001`–`MAT004`, `RES001`, `RES002`, `SYN001`, `SYN002`, `SYN003`, `SYN004`, `SYN005`, `SYN006`, `SYN007`, `SYN008`, `SYN010`, `SYN011`, `SYN012`, `SYN013`, `SYN014`, `SYN016`, `SYN017`, `SYN018`, `SYN019`, `SYN022`, `SYN023`, `THR001`–`THR004`, `UNS001`–`UNS005`, `VER001`–`VER003`) plus a fails/passes example pair. |
| `explain` | `{ code: string }` | Long-form explanation for any stable diagnostic code (`ALI001`, `ALI002`, `ALI003`, `BS001`, `BS002`, `CAP001`–`CAP003`, `DEP001`–`DEP004`, `EFF002`–`EFF004`, `FMT001`, `INT001`–`INT005`, `MAT001`–`MAT004`, `RES001`, `RES002`, `SYN001`, `SYN002`, `SYN003`, `SYN004`, `SYN005`, `SYN006`, `SYN007`, `SYN008`, `SYN010`, `SYN011`, `SYN012`, `SYN013`, `SYN014`, `SYN016`, `SYN017`, `SYN018`, `SYN019`, `SYN022`, `SYN023`, `SYN028`, `THR001`–`THR004`, `UNS001`–`UNS005`, `VER001`–`VER003`) plus a fails/passes example pair. |

A bot's loop becomes deterministic: `transform` → if `ok=false`, read
`diagnostics[0].code` → `explain(code)` → apply `rewrite` → `transform` again.
Expand Down
30 changes: 30 additions & 0 deletions packages/compiler/src/error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,36 @@ const E: Record<string, ErrorCodeEntry> = {
" return userAgent\n" +
"}",
},
SYN028: {
code: "SYN028",
title: "fn uses a navigation API that bypasses the net capability model (?bs 0.7+)",
rule:
"window.open(), globalThis.open(), self.open(), location.href=, location.assign(), " +
"location.replace(), and location.reload() all cause real network activity at runtime; " +
"none are covered by uses { net }, reads {}, or writes {} — callers cannot see the dependency",
idiom:
"pass a navigation callback as an explicit parameter so the dependency is visible in the fn header, " +
"or wrap in unsafe \"navigates for <reason>\" { ... } when direct navigation is required at an entry point",
rewrite: "fn name(navigate: (url: string) -> void, ...) -> ...",
example:
"// SYN028: location.href = url bypasses the capability model\n" +
"?bs 0.7\n" +
"fn redirectTo(url: string) -> void {\n" +
" location.href = url // SYN028\n" +
"}\n\n" +
"// SYN028: window.open() opens a new context without net declaration\n" +
"fn openTab(url: string) -> void {\n" +
" window.open(url, '_blank') // SYN028\n" +
"}\n\n" +
"// fix: pass navigation as a parameter\n" +
"fn redirectTo(navigate: (url: string) -> void, url: string) -> void {\n" +
" navigate(url)\n" +
"}\n\n" +
"// or use unsafe when navigation is genuinely required at an entry point\n" +
"fn redirectTo(url: string) -> void {\n" +
" unsafe \"redirects to login page\" { location.href = url }\n" +
"}",
},
DEP001: {
code: "DEP001",
title: "fn transitively reads a resource category not declared in its header",
Expand Down
Loading
Loading