Skip to content
Merged
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,18 @@ Pass an allow-list with `-allow-caps`, a comma-separated list of capability name
go run howlframe.go -run-bc -allow-caps network,filesystem examples/cli_hello.howl.bc.bin
```

For native bytecode and `-run`, use repeatable path grants such as
`-allow-caps filesystem:read=/data,filesystem:write=/out`. Read grants permit
reads only; write grants permit writes and `mkdir` only. `filesystem` remains
an unrestricted alias. Roots are cleaned and made absolute relative to the
runner's cwd when parsed. Targets must stay within a matching root, including
after symlink resolution. A `file://` store also needs `database`: open/load,
get, and keys need read coverage; put/delete need both read and write coverage.
Empty roots, unknown filesystem sub-keys, and scoped forms of other
capabilities are rejected. Checks precede I/O, but concurrent symlink changes
remain a TOCTOU risk. Generated Go and JavaScript still accept coarse grants
through `HOWLFRAME_ALLOW_CAPS`; they do not enforce these path scopes.

An unrecognized capability name in `-allow-caps` is rejected outright rather than silently granting nothing. See `docs/reference/bytecode_reference.md` for the full opcode-to-capability mapping.

Generated Go and JavaScript mediate `(env "KEY")`, `(exec cmd args...)`, `(read_file path)`, `(fetch url method)`, `(write_file path data)`, and `(mkdir path)` the same way. The runner grant is the `HOWLFRAME_ALLOW_CAPS` environment variable, a comma-separated list of the same names. An empty or unset value denies the effect with `CAPABILITY_DENIED` before the variable is fetched, a process is spawned, a file is read or written, a directory is created, or an HTTP request is sent. `environment` returns the value. `process` runs the command. `filesystem` reads a file, writes a file, or creates a directory. `network` performs the request. Other generated host effects are not on this gate yet. An optional `(fetch)` body is still sent by the interpreter, Go, and JavaScript when that call runs. Both bytecode compilers leave it off `OpFetch`. The design for a later change on those two compilers together is [fetch-body bytecode design](docs/reference/fetch_body_bytecode_design.md). The flip checklist is [production flip criteria](docs/reference/lowered_hfir_prod_flip_criteria.md).
Expand Down
1 change: 1 addition & 0 deletions change_log.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

### Fixed
* S8/P2.1 filesystem portion: native VM and interpreter enforce repeatable `filesystem:read=<root>` and `filesystem:write=<root>` grants, with symlink containment checks. File stores require read coverage and write coverage for mutations. Coarse `filesystem` remains unrestricted; process/network scoping and generated backends remain open. Journal: `docs/journals/2026-10-06_path_scoped_filesystem.md`.
* Review C3/C7: executable VM effect-gate conformance coverage; demo decisions use escaped JSON; executor capability checks precede writes. Effects remain ordered, not atomic. Journal: `docs/journals/2026-10-05_effect_gate_conformance_demo_honesty.md`.
* Checker enforces `docs/reference/NUMERIC_CONTRACT.md`: mixed int/float `+`, `-`, `*` are valid and typed float, `/` always types float, and int/float comparisons are valid (HFREC-063). Void expressions in value position (`let`, `set`, call/print/list arguments, `append`/`map_set` values) and statically provable builtin argument mismatches (`str_split`, `str_join`, `regex_match`, `list_len`) are rejected before any backend runs (HFREC-009, HFREC-010). `parse_json` bodies must be variable names outside `web_app` (HFREC-004). Generated JavaScript keeps typed parameter names (HFREC-049). Go and JavaScript `to_int` truncate toward zero like the VM; `web_app` rejects integer literals and JSON integers outside +/-(2^53-1) instead of rounding. Wasm integer `/` fails closed. Artifact format version 2 (sorted function table, deterministic bytes) still reads version 1 artifacts; version 1 readers cannot read version 2 artifacts (see `docs/reference/artifact_compatibility.md`). New corpus: `tests/parity_stabilization/`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ their own code.
| S5 | **Unbounded recursion crashes the process.** At review time, `MaxCallDepth` (128) guarded only `SPAWN_AGENT`; ordinary bytecode `CALL` recursion could overflow the Go stack. C4a now bounds bytecode CALL with structured `LIMIT_EXCEEDED`, default 1000 shared with SPAWN_AGENT nesting, and a runner flag. AST interpreter recursion remains unbounded by this limit. | Medium | PARTIAL (C4a, 2026-10-06): bytecode CALL bounded; AST interpreter remains open. | `/tmp` probe with `--max-instructions 2000000000` |
| S6 | **At review, wall-clock was unbounded.** `sleep`, `fetch`, `exec`, `read_line`, and model calls block outside the instruction count. | Medium | PARTIAL (C4b, 2026-10-06): optional deadline cancels bytecode sleep/fetch/exec/model requests and checks the instruction loop; blocking read_line and HTTP serving remain open, as does AST interpreter. | C4b deadline regression tests. Historical Codex B: a 5 s `sleep` under a 3-instruction ceiling ran until killed. Code: `vm.go:2122`, `:2170`. |
| S7 | **At review, response and output sizes were unbounded.** `fetch` read the whole body and `exec` used `CombinedOutput`. `print` remains unbounded. | Medium | PARTIAL (C4b, 2026-10-06): bytecode fetch body and combined exec output capped at 10 MiB by default; print remains uncapped; interpreter unchanged. | C4b byte-cap regression tests; historical code reading (A, B) |
| S8 | **`process` grant = arbitrary host authority.** `(exec "/bin/sh" "-c" "...")` with only `process` writes files. Grants are five classes with no resource scoping. | High (policy design) | OPEN (P1/P2: scoped grants, action allow-list) | Codex B ran it in `/tmp` on both VM and interpreter |
| S8 | **`process` grant = arbitrary host authority.** `(exec "/bin/sh" "-c" "...")` with only `process` writes files. Native filesystem grants now support read/write roots; process still permits arbitrary host authority. | High (policy design) | PARTIAL-closed: filesystem roots enforced in VM/interpreter; process/network still OPEN (P2.1) | Codex B ran it in `/tmp` on both VM and interpreter; filesystem evidence: [2026-10-06 journal](../journals/2026-10-06_path_scoped_filesystem.md) |
| S9 | **Child agents inherit the full grant.** `SPAWN_AGENT` and legacy `SPAWN` children get `AllowedCaps` unchanged (`vm.go:2233`, `:3030`). Legacy `SPAWN` also resets instruction accounting. | Medium | OPEN (P2: attenuation) | Code reading (B) |
| S10 | **Semantic divergence changes effects.** `(and (= 1 2) (= (env "X") "x"))`: the VM and interpreter evaluate eagerly and hit `CAPABILITY_DENIED`. Generated Go short-circuits and prints `false`. | Medium | OPEN (P1: decide `and`/`or` semantics, add differential test) | Found by Codex A. Reproduced in this review. |
| S11 | **Generated Go/JS gate only 6 effects.** Model calls, SQL, listeners, goroutines, and JS `spawn` run ungated. A generated-Go panic writes `crash.json` unconditionally. | Medium (documented) | OPEN. Keep Go/JS out of the governed profile. | README states it. Codex B confirmed. |
Expand Down
2 changes: 1 addition & 1 deletion docs/ai-native-language-review/08-roadmap-p0-p4.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ the artifact format.

| # | Item | Size |
| --- | --- | --- |
| P2.1 | Resource-scoped grants: `filesystem:read=/data`, `filesystem:write=/out`, `network=host[:port]`, `process=<allow-listed action id>`, `environment=VAR`. Keep the coarse names as aliases. | L |
| P2.1 | Resource-scoped grants: `filesystem:read=/data`, `filesystem:write=/out`, `network=host[:port]`, `process=<allow-listed action id>`, `environment=VAR`. Keep the coarse names as aliases. Filesystem portion implemented in native VM/interpreter (read and write independent); remaining scopes OPEN. See [journal](../journals/2026-10-06_path_scoped_filesystem.md). | L |
| P2.2 | Grant attenuation for `SPAWN_AGENT` children. Fold legacy `SPAWN` accounting into the parent budget. | M |
| P2.3 | Full artifact validation (stack-height, operand types, embedded body lengths) for artifacts from outside | L |
| P2.4 | Rooted / hash-pinned `include` and `use` for the governed profile | M |
Expand Down
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Executes a compiled HowlFrame bytecode artifact.
**Important**: Capabilities are denied by default. You must explicitly grant capabilities to the runtime.

### Options
* `--allow-caps` : Comma-separated capabilities to allow (e.g., `network,filesystem,process,environment,database`). Instructions requiring an unlisted capability are denied and will cause the VM to panic.
* `--allow-caps` : Comma-separated capabilities to allow (e.g., `network,filesystem,process,environment,database`). Repeatable `filesystem:read=<root>` grants permit reads; `filesystem:write=<root>` grants permit writes and `mkdir`, without granting reads. `filesystem` remains unrestricted. Roots are cleaned and anchored to the runner cwd at parse time. Targets must remain within a matching root after lexical normalization and symlink resolution. `file://` stores require `database` plus read coverage for open/get/keys and both read/write coverage for put/delete. Empty roots, unknown sub-keys, and scoped forms of other capabilities are unknown-capability errors. Denials occur before I/O with `CAPABILITY_DENIED`. Concurrent symlink changes remain a TOCTOU caveat. Generated Go/JavaScript remain coarse.
* `--max-instructions` : A finite instruction ceiling (default `100000`). Once the ceiling is reached, execution halts to prevent infinite loops and runaway resource consumption.
* `--max-call-depth` : Positive CALL recursion and SPAWN_AGENT nesting ceiling (default `1000`).
* `--max-memory-bytes` : Positive cumulative allocation charge ceiling (default `67108864`, 64 MiB). Charges approximate storage; they are not a measured live heap limit and are never reclaimed.
Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ <h2><span class="sec-id">SECTION // 04</span> Capability Boundary Matrix</h2>
<td><code>filesystem</code></td>
<td><code>file_read</code>, <code>file_write</code></td>
<td><span class="chip chip-deny">DENY (Blocked)</span></td>
<td>Runner flag: <code>-allow-caps filesystem</code></td>
<td>Runner flag: <code>-allow-caps filesystem:read=/data,filesystem:write=/out</code> (native VM; <code>filesystem</code> remains unrestricted)</td>
</tr>
<tr>
<td><code>system</code></td>
Expand Down
72 changes: 72 additions & 0 deletions docs/journals/2026-10-06_path_scoped_filesystem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 2026-10-06: Path-scoped filesystem grants (S8 / P2.1)

## Findings and change

- S8 is PARTIAL-closed: the native filesystem portion is implemented. The
original process authority finding remains open. P2.1 retains its other scopes.
- `-allow-caps filesystem:read=/data,filesystem:write=/out` accepts repeatable
roots. Read grants authorize reads only. Write grants authorize write_file
and mkdir only. The existing `filesystem` alias remains unrestricted.
- `internal/capability/grants.go` exposes ParseGrant and Grants. ParseGrant
returns Capability values compatible with existing []capability.Capability
APIs, including RunBytecode and Interpreter.AllowedCaps. Roots are Abs/Clean
at parse time, relative to runner cwd. Direct callers should use ParseGrant.
Scoped grants count as filesystem for coarse required-capability gates;
path checks remain mandatory before native I/O. RequiredCapabilities still
reports the coarse effect class, not evidence of path authorization.
- Invalid/empty roots, unknown filesystem sub-keys, and colon scopes on other
capabilities produce the existing CLI unknown-capability error.
- `howlframe.go` parses CLI grants; `internal/vm/vm.go` enforces read_file,
write_file, mkdir in both execution engines. Native file stores check the
path before open/load and at storeHandle for every get/keys/put/delete.
Database is still required. Open/get/keys need read; put/delete need both
read and write. Memory stores are unchanged. No interpreter file-store
implementation exists to extend.
- Containment uses filepath.Rel, rejecting parent escapes and prefix siblings.
EvalSymlinks resolves each root and the target's longest existing ancestor;
resolution errors and dangling symlinks fail closed. Scoped I/O uses the
checked absolute cleaned path, preventing raw symlink/.. traversal from
differing from the permission check. Roots themselves may be symlinks.
- Scoped receipt authorization is recorded only after all required path checks
pass; denied paths record denied rather than a provisional coarse allowed.
- Denial precedes file I/O with the existing CAPABILITY_DENIED format in the
VM and existing capability denial in the interpreter. Denials omit paths.

## Verification

- TestParseGrant: valid forms, invalid forms, cwd anchoring.
- TestFilesystemGrantContainment: independent read/write scopes, containment,
prefix siblings, parent escape, unrestricted alias, symlink and dangling-link
escape denial with nonexistent write targets.
- TestFilesystemScopesVMAndInterpreter: reads/writes/mkdir inside/outside,
parent escape, independent scopes, coarse alias, symlink escapes, and
unchanged files/directories after denied mutations.
- TestFileStoreFilesystemScopes: allowed put/delete, denied put/delete outside
write coverage, read-only get/keys, denied open outside read coverage, and
write-only open denial; denied mutations leave persisted bytes unchanged.
- TestFilesystemScopeReceiptDecision: scoped denial and success decisions,
including read-only store mutation denial after successful open.
- TestCLIPathScopedFilesystemGrants: relative/repeated roots, coarse alias,
write-only read denial, invalid roots/sub-keys/other-capability scopes.
- `gofmt -l .`: empty output; `go vet ./...`: passed; `go build -v ./...`:
passed. Targeted capability/VM/CLI tests passed.
- `python3 -m unittest test_harness.py` in benchmarks/v2/harness: 8 tests
passed. Its deliberate failing fixture attempts are expected harness probes;
the command exits zero. `python3 scripts/test_seo.py`: all checks passed.
- Initial `go test ./...` collided with the concurrently running Python
harness HTTP fixture on port 8080 (TestHTTPServerServeHFBC). The full Go
suite passed after the harness finished. A subsequent `go test ./...` on
the final code (including receipt decisions) also passed, all packages.

## Remaining work

- Process allow-list, network host scoping, and environment=VAR remain open.
- S9/P2.2 SPAWN_AGENT attenuation is unchanged; children inherit runner grants.
- Generated Go and JavaScript still use coarse howlFrameGrantHas("filesystem")
checks through HOWLFRAME_ALLOW_CAPS. Scoped native execution does not establish
backend parity. No production -compile-bc/HFIR default flip or #90 status change.
- Symlink checks reduce static escapes but are not atomic with I/O: concurrent
replacement of symlinks/ancestors remains a TOCTOU risk. Hard links and host
mount changes are not isolated by path scoping. Strong isolation needs
descriptor-relative OS containment or a host sandbox in a later change.
- No live LLM calls, process/network scoping, commit, or push in this change.
2 changes: 1 addition & 1 deletion docs/reference/lowered_hfir_abi_v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ The conformance cases `nested_fetch_denied` and `nested_fetch_granted` are `test

The conformance cases `nested_multi_effect_denied`, `nested_multi_effect_partial`, and `nested_multi_effect_granted` are `tests/conformance/abi_v1/21_nested_multi_effect.howl`. Their hosts are the same five. One `defun` nests `env`, `read_file`, `write_file`, `mkdir`, `exec`, and `fetch` inside `if`, `while`, and `for`, including a `for` inside a `for`. With no grant, the first reached effect is `env`. The denial is `CAPABILITY_DENIED` before any later effect, stdout is empty, and no path is created. With `environment,filesystem,process` and without `network`, the first site prints `phase1-token`, `multi-read-marker`, and `phase2b-exec-marker`, writes one file, creates one directory, and then `fetch` is `CAPABILITY_DENIED`. No request is sent. With `environment,filesystem,process,network`, the taken lines continue through `phase2d-fetch-marker`, `L a 0`, `L b 0`, `kept 2`, the kept site, `result 2`, `miss`, the miss site, and `empty 0`. The untaken branches must not print, must not create their paths, and must not send their requests. One untaken `fetch` passes a body string. `OpFetch` has no body operand, and neither bytecode compiler loads that string. This case is evidence that the experimental lowerer and the AST bytecode compiler execute that combination the same way. It does not switch `-compile-bc` to HFIR.

The interpreter and the bytecode VM consult `-allow-caps`. Generated Go and JavaScript do the same check in `howlFrameEnv`, `howlFrameExec`, `howlFrameReadFile`, `howlFrameFetch`, `howlFrameWriteFile`, and `howlFrameMkdir`. Their runner grant is `HOWLFRAME_ALLOW_CAPS`, a comma-separated list of the same names as `-allow-caps`. An empty or unset value denies. A grant that omits the required name denies. `howlFrameEnv` may read that grant variable. It does not read the requested key until `environment` is present. `howlFrameExec` does not spawn until `process` is present, and the denial text does not contain the command. `howlFrameReadFile` does not call `os.ReadFile` or `readFileSync` until `filesystem` is present, and the denial text does not contain the path. `howlFrameFetch` does not call `http.NewRequest`, `http.DefaultClient.Do`, or `fetch` until `network` is present, and the denial text does not contain the URL. `howlFrameWriteFile` does not call `os.WriteFile` or `writeFileSync` until `filesystem` is present, and the denial text does not contain the path. `howlFrameMkdir` does not call `os.MkdirAll` or `mkdirSync` until `filesystem` is present, and the denial text does not contain the path. Other generated host effects are still not mediated.
The interpreter and the bytecode VM consult `-allow-caps`. Generated Go and JavaScript do the same check in `howlFrameEnv`, `howlFrameExec`, `howlFrameReadFile`, `howlFrameFetch`, `howlFrameWriteFile`, and `howlFrameMkdir`. Their runner grant is `HOWLFRAME_ALLOW_CAPS`, a comma-separated list of coarse capability names. Native VM/interpreter `-allow-caps` additionally accepts `filesystem:read=<root>` and `filesystem:write=<root>`; generated Go/JavaScript do not enforce these scopes. An empty or unset value denies. A grant that omits the required name denies. `howlFrameEnv` may read that grant variable. It does not read the requested key until `environment` is present. `howlFrameExec` does not spawn until `process` is present, and the denial text does not contain the command. `howlFrameReadFile` does not call `os.ReadFile` or `readFileSync` until `filesystem` is present, and the denial text does not contain the path. `howlFrameFetch` does not call `http.NewRequest`, `http.DefaultClient.Do`, or `fetch` until `network` is present, and the denial text does not contain the URL. `howlFrameWriteFile` does not call `os.WriteFile` or `writeFileSync` until `filesystem` is present, and the denial text does not contain the path. `howlFrameMkdir` does not call `os.MkdirAll` or `mkdirSync` until `filesystem` is present, and the denial text does not contain the path. Other generated host effects are still not mediated.

### Feasibility

Expand Down
Loading
Loading