You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add --net-listen-allow CLI flag; add RFC 868 time-server example
Closes the gap flagged right after net.listen shipped (#215): dial's
"no CLI policy flag" limitation doesn't matter in practice since dial
defaults to allow, but listen defaults to deny, so the same gap left
net.listen completely unusable from the CLI, not just unrestricted.
--net-listen-allow pattern[:port] (repeatable) adds an allow rule to
net.listen's bind policy directly from the CLI, mapping onto the same
net_state.addListenPolicyRule the embedding API already uses. Accepts
the same pattern shapes as the embedding API (*, exact IPv4/IPv6, CIDR,
hostname wildcard) plus an optional :port suffix, bracket-safe for IPv6
literals ("[::1]:8080").
examples/time-server/ is a minimal end-to-end demonstration: RFC 868's
Time Protocol is about as simple as a network server gets (connect,
receive 4 bytes, done - no request to parse). Verified live: server and
client both run under the plain CLI, no host embedding needed, and the
decoded time matches actual UTC. Building it surfaced two real script
bugs worth noting for future examples: std.io.println does not insert
separators between arguments (concatenates directly - "listening on" +
address ran together with no space), and net.dial returns a single
Conn-or-error value, not a [value, error] pair the way net.listen and
http.get/post/fetch do - an easy mix-up given how close the two now
look in the same codebase.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+3-2Lines changed: 3 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,12 +9,13 @@ This changelog tracks notable language/runtime changes by implementation date.
9
9
Adds `net.listen(network, address) [Listener, error]` and `Listener.accept()/close()/local_addr()/set_accept_deadline(ms)`, so a script can bind a port and accept inbound TCP connections — an MQTT listener, an HTTP server, a TCP echo service — purely in Gengo, reusing the exact same `Conn` type and read/write/close/deadline API `net.dial` already produces for the data path.
10
10
11
11
-**New CLI/embedding mechanism, not just a new function**: `net` is now gated by scope — `--cap net=dial`, `--cap net=listen`, or `--cap net=dial,listen`. Bare `--cap net` (no `=`) still means exactly what it always has (dial-only); upgrading an existing deployment never silently grants listen. The `--cap name=scope1,scope2` syntax is a general CLI mechanism (split on `=` then `,`), not `net`-specific, so any future capability that grows scopes gets the same syntax for free. `cap:net`'s import gate succeeds as long as *some* net scope is granted; each function is refused individually at call time if its own scope wasn't (`ctx.vs.net_scopes`, computed from `Runtime.enabled_capabilities` in `Runtime.activate()`) — the same shape as an `fs` operation against a path with no matching mount.
12
-
-**Listen policy defaults to deny-all**, deliberately the opposite of dial's existing default-allow: a host must affirmatively add at least one allow rule (`engine_net_listen_policy_add`, mirroring `engine_net_policy_add`'s shape and LIFO evaluation exactly) before any `net.listen(...)` call succeeds. Separate rule list from dial's — configuring one has no effect on the other. No CLI flag exists to configure it (matching how dial policy has always been host-API-only from the CLI); `--cap net=listen` alone makes `net.listen` compile and import but refuse every call until a host adds a rule.
12
+
-**Listen policy defaults to deny-all**, deliberately the opposite of dial's existing default-allow: a host must affirmatively add at least one allow rule before any `net.listen(...)` call succeeds. Separate rule list from dial's — configuring one has no effect on the other. Two ways to add a rule: the embedding API (`engine_net_listen_policy_add`, mirroring `engine_net_policy_add`'s shape and LIFO evaluation exactly), or the CLI's new repeatable `--net-listen-allow pattern[:port]` flag (added same day, after initially shipping this host-API-only to match dial's precedent — dial's identical "no CLI flag" gap doesn't matter in practice since dial defaults to allow, so the asymmetry meant listen's version of that gap left it completely unusable from the CLI, not just unrestricted; see #215).
13
13
-**`net.listen`/`Listener.accept` return `[value, error]` pairs**, unlike `dial`'s single "Conn or error" value — matching `http.get`/`post`/`fetch`'s existing convention (`l, err := net.listen(...)`) rather than dial's, since that's the calling shape the language surface actually needs here.
14
14
- Native POSIX implementation reuses existing `net_state.zig` machinery rather than inventing new plumbing: `socket()`+`bind()`+`listen()` (fixed backlog, 128) into a new `g_listeners` table (independent ceiling from `MaxConns`, default 8 — a script needing many listening ports is a different shape of program than one needing many connections); accept deadline reuses `posixSetSockOptTimeval`/`SO_RCVTIMEO` applied to the *listening* socket instead of a connection, so `EAGAIN` maps to `error.DeadlineExceeded` exactly like `netRead`'s existing arm; a successful `accept()` produces a plain connected socket wrapped in the same `NetConn` struct `dial` produces, pushed into the existing `g_conns`/`MaxConns` pool — accepted and dialed connections share one ceiling and one accounting path, not a separate parallel budget.
15
15
- Host-mediated path (WASI/browser/embedders without POSIX sockets) extends `gengo_net_handlers_t` with optional `listen`/`accept`/`listener_close`/`listener_local_addr`/`set_accept_deadline` callbacks — `null` on a host that supports dial but not listen, reported as `CapabilityNotAvailable` rather than a crash. WASI itself gets the same treatment `dial` already has (`error.CapabilityNotAvailable`): confirmed directly against Zig's std that WASI has no `bind`/`listen`/`accept` syscall wrappers at all (only Linux's raw syscall interface does), so there was a real platform gap to gate, not a guess.
16
16
- Tests: `compiler_test.zig` gains scope-gating and default-deny-policy tests, plus a genuine POSIX `bind`+`accept`+`read`+`write` roundtrip test using a `std.Thread`-spawned raw-socket client (kept off `net_state`'s own globals entirely to avoid a real cross-thread data race, since `net_state` has no locking) against the real listener implementation; `engine.zig` gains `engine_net_listen_policy_add`/`_clear` C API tests mirroring the existing dial-policy ones; a new `tests/spec/cap/fail/006_net_listen_scope_not_granted.gengo` conformance case.
17
-
- Docs: `capabilities.md` (scope table, `Listener` API, default-deny policy, server-loop shape), `capability-matrix.md`, `security.md` (listen vs. dial policy defaults, why they differ, existing cross-runtime `net_state` isolation gap now also covers listeners), `cli.md` (`--cap net=scope1,scope2` syntax).
17
+
- Docs: `capabilities.md` (scope table, `Listener` API, default-deny policy, server-loop shape), `capability-matrix.md`, `security.md` (listen vs. dial policy defaults, why they differ, existing cross-runtime `net_state` isolation gap now also covers listeners), `cli.md` (`--cap net=scope1,scope2` and `--net-listen-allow` syntax).
18
+
- New example: `examples/time-server/` — an RFC 868 Time Protocol server and client, about as minimal a demonstration of `net.listen`/`Listener.accept` as exists (connect, receive 4 bytes, done). Runs directly under the CLI, no host embedding needed.
18
19
-**Known, explicitly deferred, not fixed here**: `net_state.zig`'s connection/listener tables and both policy lists remain process-wide module state, not yet part of the per-instance activation set #190 tracks for `chunk`/`globals`/`heap`/`vm` — a pre-existing latent gap for dial, now also covering listeners. Not a concern for a single-`Runtime`-per-process embedding (the CLI); a host running multiple independently-untrusted scripts with `listen` enabled in one process should treat this as a real isolation gap until #190 lands, not assume it's already handled. See `dev-docs/design/net-listen-design.md` for the full design rationale.
Copy file name to clipboardExpand all lines: docs/cli.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -38,7 +38,8 @@ script from standard input instead.
38
38
|`--test`| Run top-level `test` blocks rather than ordinary script execution. A failed test exits unsuccessfully. |
39
39
|`--profile`| With `--test`, print each block's instruction count and peak heap bytes/stack depth/live object count, plus a final peak-across-all-blocks summary line. Does not affect pass/fail behavior or the exit code. Forces per-instruction instruction counting on for the run, which costs real speed — a diagnostic aid, not something to leave on by default. |
40
40
|`--cap name`| Enable one named capability. Repeat for several capabilities. See `capabilities.md`; no capability is enabled merely by importing it. |
41
-
|`--cap net=scope1,scope2`| Scope the `net` capability instead of granting it unscoped. Scopes are `dial` and `listen`, comma-separated (`--cap net=dial`, `--cap net=listen`, `--cap net=dial,listen`). Bare `--cap net` (no `=`) still means dial-only, unchanged from before scopes existed — upgrading never silently grants listen. `net.listen`'s policy defaults to deny-all regardless of scope; there is no CLI flag to add a listen-policy rule, so `--cap net=listen` alone makes `net.listen(...)` compile and import but refuse every call until a host adds one via the embedding API (`engine_net_listen_policy_add`). This general `name=scope1,scope2` syntax is available for any capability that defines scopes, not just `net`. |
41
+
|`--cap net=scope1,scope2`| Scope the `net` capability instead of granting it unscoped. Scopes are `dial` and `listen`, comma-separated (`--cap net=dial`, `--cap net=listen`, `--cap net=dial,listen`). Bare `--cap net` (no `=`) still means dial-only, unchanged from before scopes existed — upgrading never silently grants listen. This general `name=scope1,scope2` syntax is available for any capability that defines scopes, not just `net`. |
42
+
|`--net-listen-allow pattern[:port]`| Add an allow rule to `net.listen`'s bind policy, which defaults to deny-all (the opposite of `net.dial`'s default-allow — see `security.md`). Repeatable. `pattern` accepts the same shapes as the embedding API's policy rules (`"*"`, exact IPv4/IPv6, CIDR, hostname wildcard); an optional `:port` suffix restricts to one port (bracket the pattern for a literal IPv6 address with a port, e.g. `"[::1]:8080"`). With no rules, `--cap net=listen` alone still makes `net.listen(...)` compile and import but refuse every call. |
42
43
|`--modules path`| Permit source imports from one additional directory. Repeatable, up to eight paths. The script directory remains the default source root. |
43
44
|`--max-ops n`| Limit VM instruction execution to `n`. `0` means unlimited. This limit does not account for work inside host callbacks. |
44
45
|`--heap size`| Set the GC heap size. A size may be bytes or end in `k`, `m`, or `g`; the default is `1m`. |
0 commit comments