hanki

sys

stdlib/core/sys.hk: the native OS-capability seam.

sys is the single home for the raw @intrinsic primitives behind opt-in OS capabilities. The pure-Hanki extra faces (io, fs, process) delegate here, and the […] effect bubbles up through the delegating call automatically. Keep every primitive minimal and raw, the irreducible syscall form alone, and push policy and ergonomics (path normalization, atomic writes and the like) up into the faces, and this locked core surface commits to as little as possible.

Return types are specific sums and no weak booleans or options: a failure reports which failure (FsError), and a finished subprocess reports how it finished (ProcessOutcome), and no exit_code = -1 magic value. The lone Option here, stdin_read_line!, is a genuine present/absent (None = EOF), which the io face names Line/Eof.

FsError

type FsError
  NotFound
  PermissionDenied
  NotUtf8
  Other(string)
  AlreadyExists
  NotADirectory
  CrossDevice
end

A filesystem failure, specific enough for a caller to branch on. Other reports the OS-level message for the long tail. Variant order matters: the runtime builds these by tag.

FsFailure

struct FsFailure
  path: string
  kind: FsError
end

A filesystem failure and the path it happened to. FsError alone says what went wrong and never to what, and a caller that reads twenty files and gets back NotFound cannot say which one, and every caller worked around that by concatenating back in the path it already had. A walk! is the case with no workaround available: the directory it failed to descend into is one the caller never named.

path is the path the failing call named. It is empty for a call that names none, the File handle operations, where the descriptor no longer records what it was opened from, and Display then renders the kind alone in place of an empty pair of quotes.

impl Display<FsError>

to_string

def to_string(self) -> string

The OS-level reason, lowercase and without a trailing stop, which lets it read inside a larger sentence as well as alone. Other is the exception it cannot make: it forwards the platform's own message verbatim (Directory not empty (os error 39)), capital and errno tail included, because rewriting an OS string is how detail gets lost.

Other's case is shown through [FsFailure] and never bare: Other is a variant of both FsError and TermError, and only a typed position says which one is meant.

NotFound.to_string() => "not found"
FsFailure(path="x", kind=Other("disk full")).to_string() => "x: disk full"

impl Eq<FsError>

eq?

def eq?(self, other: Self) -> bool

Structural equality; Other compares its message.

Other is shown through [FsFailure] for the reason [Display] gives: bare, the name does not say which sum it belongs to.

NotFound == NotFound => true
FsFailure(path="p", kind=Other("a")) == FsFailure(path="p", kind=Other("b")) => false

impl Display<FsFailure>

to_string

def to_string(self) -> string

Renders as <path>: <kind>, or the kind alone when there is no path.

FsFailure(path="a.txt", kind=NotFound).to_string() => "a.txt: not found"
FsFailure(path="", kind=NotFound).to_string()      => "not found"

impl Eq<FsFailure>

eq?

def eq?(self, other: Self) -> bool

Two failures are equal when both the path and the kind match.

FsFailure(path="a", kind=NotFound) == FsFailure(path="a", kind=NotFound) => true

BudgetExceeded

type BudgetExceeded
  Exceeded
end

A with_budget(bytes, steps) scope (HANKI.md §23) exhausted its nested fuel or byte sub-quota. The scope yields Err(Exceeded) in place of letting the over-budget child fault the whole run, and the host recovers by matching the result. v1 reports that a limit tripped and never which one.

DirEntry

struct DirEntry
  name: string
  is_dir: bool
  size: int
  modified: int
  mode: int
end

One entry from dir_list!: the entry's name within the listed directory and whether it is itself a directory (a symlink is not reported as one). Field order matters: the runtime builds these by slot.

Metadata

struct Metadata
  size: int
  modified: int
  mode: int
  is_dir: bool
end

One path's stat. Field order matters: the runtime builds this by slot.

modified is epoch nanoseconds and no datetime.Instant: sys is core and would otherwise pull a calendar in for one field. Nanoseconds and not seconds because it is lossless and converts in one call (datetime.Instant.from_epoch_nanos), and because a second-resolution mtime cannot see a file replaced twice within the same second.

mode is the raw POSIX bits, 0 on a host that has none; the extra tier's fs.Metadata is where they become a Permissions.

NetError

type NetError
  ConnectionRefused
  ConnectionReset
  AddrInUse
  TimedOut
  BrokenPipe
  Other(string)
end

A network failure, specific enough for a caller to branch on: a client distinguishes ConnectionRefused, a server AddrInUse. Other reports the OS-level message for the long tail. Variant order matters: the runtime builds these by tag.

impl Display<NetError>

to_string

def to_string(self) -> string

The OS-level reason, lowercase and without a trailing stop, which lets it read inside a larger sentence as well as alone. Other forwards the platform's own message verbatim, for the reason [FsError]'s does: rewriting an OS string is how detail gets lost.

Other is not shown here for the reason [FsError]'s is not shown bare either: the name belongs to several sums at once, and only a typed position says which is meant.

ConnectionRefused.to_string() => "connection refused"
AddrInUse.to_string()         => "address already in use"

TlsError

type TlsError
  TlsHandshake(string)
  TlsCertificate(string)
  TlsTransport(string)
end

Why a TLS operation failed. Three cases a caller branches on: the handshake could not be agreed, the peer's identity did not check out, or the socket underneath failed. Variant order matters: the runtime builds these by tag.

TlsCertificate is its own case and no handshake detail: it is the one a caller can act on differently (a wrong host, an expired chain, a private CA nobody installed), and folding it into a generic failure is how "check the certificate" advice gets lost.

impl Display<TlsError>

to_string

def to_string(self) -> string

The reason, lowercase and without a trailing stop, which lets it read inside a larger sentence as well as alone. Each gives the library's own detail verbatim after a colon, for the reason [FsError]'s Other does: rewriting the underlying message is how detail gets lost.

TlsHandshake("no shared cipher").to_string() => "tls handshake failed: no shared cipher"
TlsCertificate("expired").to_string()        => "certificate rejected: expired"

TlsFailure

struct TlsFailure
  host: string
  kind: TlsError
end

A TLS failure and the host it happened against.

The host is here and not inside each TlsError variant, for the reason [FsFailure] pairs a path with an [FsError]: a caller reading "certificate rejected" needs to know whose certificate, and a sum whose every variant repeated the host would say it three times and still let one forget. Field order matters: the runtime builds these by slot.

impl Display<TlsFailure>

to_string

def to_string(self) -> string

host: reason, the form [FsFailure] uses for a path.

TlsFailure(host="example.com", kind=TlsCertificate("expired")).to_string() => "example.com: certificate rejected: expired"

TlsVersion

type TlsVersion
  V12
  V13
end

The TLS protocol version a connection settled on, as tls_version! reports it. Two variants: the client speaks two versions (see tls_connect!). Variant order matters twice over: the runtime builds these by tag, and the order is ascending so that Ord makes a policy floor an ordinary comparison, and "must not travel over 1.2" is version >= V13, which a string rendering could only offer as an equality test a respelling would break with nothing to say so.

impl Display<TlsVersion>

to_string

def to_string(self) -> string

The version's compact tag, which is also what #{…} interpolation of a TlsVersion yields.

V13.to_string() => "TLS1.3"
V12.to_string() => "TLS1.2"

impl Eq<TlsVersion>

eq?

def eq?(self, other: Self) -> bool

Two versions are equal when they are the same protocol version.

V13.eq?(V13) => true
(V12 == V13) => false

impl Ord<TlsVersion>

cmp

def cmp(self, other: Self) -> Ordering

Ordered by protocol age, ascending, and a version floor is therefore an ordinary comparison.

V12.cmp(V13) => Less
(V13 >= V13) => true

tlsversion_rank

def _tls_version_rank(v: TlsVersion) -> u8

Declaration order as a number, backing Eq and Ord above.

LocalAddress

struct LocalAddress
  host: string
  port: u16
end

The address a listening socket is bound to: host is the textual IP the OS reports (127.0.0.1, ::1, 0.0.0.0 for a wildcard bind), never a name. Field order matters: the runtime builds these by slot.

ProcessOutcome

type ProcessOutcome
  Exited(i32)
  SpawnFailed(string)
end

How a finished subprocess ended: it ran and returned a code, or it never started. Replaces the old exit_code = -1 spawn-failure magic.

ProcessResult

struct ProcessResult
  stdout: string
  stderr: string
  outcome: ProcessOutcome
end

Captured result of a subprocess. outcome distinguishes "exited with code N" from "failed to spawn"; stdout/stderr are empty on a spawn failure.

ConstraintKind

type ConstraintKind
  Unique
  PrimaryKey
  ForeignKey
  NotNull
  Check
  OtherConstraint
end

The kind of a SQLite constraint violation, read from the extended result code. OtherConstraint buckets the remaining codes (ROWID, TRIGGER, VTAB, and the rest). Variant order matters: the runtime builds these by tag, in lockstep with rtcore's db_error_parts.

DbError

type DbError
  Busy
  Locked
  ReadOnly
  CantOpen
  Corrupt
  Full
  NotADb
  Misuse
  Constraint(ConstraintKind, string)
  BindCount(int, int)
  IntOutOfRange(int)
  NotUtf8
  MultiStatement
  Interrupted
  Other(int, string)
  BindName(string)
end

A database failure, specific enough for a caller to branch on. Constraint reports which constraint failed and SQLite's message; BindCount the expected vs supplied parameter counts (positional binds count the params list; named binds compare the parallel names/values lists); IntOutOfRange the 0-based index into the bound values of an int too large for the underlying i64; Other an extended result code and its message; BindName a name mismatch in a named bind, a statement parameter missing from the supplied names, a supplied name the statement never uses, a duplicated name, or a positional ? slot a named call cannot cover. Interrupted is SQLite's statement-abort code; today its only producer is the shutdown abort (HANKI.md §15), which the runtime turns into the actor's ExplicitShutdown death before any Err is built, and user code does not currently observe it. The variant remains for a future user-facing interrupt surface. Variant order is matters: the runtime builds these by tag, in lockstep with rtcore's db_error_parts; new variants append at the end, after BindName, and never mid-sum.

DbValue

type DbValue
  Null
  Int(int)
  Real(f64)
  Text(string)
  Blob(bytes)
end

One SQLite cell value: the five storage classes. Variant order matters: the runtime builds and decodes these by tag.

DbRows

struct DbRows
  columns: List<string>
  rows: List<List<DbValue>>
end

A fully-materialized query result: the column names, then each row as its cells in column order. Field order matters: the runtime builds these by slot.

stdout_write!

def stdout_write!(s: string) -> () [io]

Write s to standard output verbatim. No trailing newline is added.

Nothing reading the output any more ends the program with exit 0 (HANKI.md §6); any other write failure is a fault. stdout_write_checked! below is the same write with both reported as values instead. @no-doctest: writes to stdout; side-effecting, no return value to assert

stderr_write!

def stderr_write!(s: string) -> () [io]

Write s to standard error verbatim. No trailing newline is added. @no-doctest: writes to stderr; side-effecting, no return value to assert

WriteFailure

type WriteFailure
  ConsumerGone
  Failed(string)
end

Why a write to a standard stream failed.

stdoutwritechecked!

def stdout_write_checked!(s: string) -> Result<(), WriteFailure> [io]

stdout_write! with both failures as values: the write that a program which needs to outlive its consumer reaches for: a daemon that must notice its output going away in place of exiting with it.

The plain stdout_write! remains unit-returning and ergonomic, and this is the escape hatch and no toll every caller pays. @no-doctest: writes to stdout; the failure paths need a closed consumer, which no doctest can arrange

stderrwritechecked!

def stderr_write_checked!(s: string) -> Result<(), WriteFailure> [io]

The stderr_write! counterpart of stdout_write_checked!. @no-doctest: writes to stderr; the failure paths need a closed consumer, which no doctest can arrange

logminlevel!

def log_min_level!() -> u8 [runtime_state]

The run's minimum log rank, which extra/log drops records below. The rank is extra/log.Level's: Debug 0, Info 1, Warn 2, Error 3. The number and not the level itself, Level being in extra where core cannot name it. The runtime pins the same mapping, since it is what parses HANKI_LOG.

The first call settles the run's threshold, seeding it from HANKI_LOG (debug/info/warn/error, case-insensitive) or from Debug if that names no level, in which case one line of complaint goes to stderr and the run continues: a misspelled diagnostics knob must not be able to kill it. Prefer extra/log.min_level!, which answers in Levels.

The row is [runtime_state] and no capability: this observes state the program does not own, process-global and runtime-managed, and the atom is viral (a caller declares it) but never deniable, and @encapsulated refuses it. It is not [env] - the program performs no environment read here, the runtime consults its own diagnostic knob, the way net.connect! reaches /etc/resolv.conf under [net] without granting fs authority. @no-doctest: reads process-wide state seeded by the environment; host-dependent

logsetmin_level!

def log_set_min_level!(rank: u8) -> () [runtime_state]

Set the run's minimum log rank, in the log_min_level! numbering. Values outside 0..=3 clamp into it. Process-wide and immediate: a set from any actor is what every actor's next record is measured against, which is what "the log level" means everywhere else. An explicit set wins over HANKI_LOG whichever order the two happen in. Prefer extra/log.set_min_level!, which takes a Level. @no-doctest: changes process-wide state; no return value to assert

log_emit!

def log_emit!(rank: u8, message: string) -> () [io]

Write one log record to standard error, unless rank (in the log_min_level! numbering, values outside 0..=3 clamped into it) is below the run's threshold. The drop happens on this side of the seam, before message is even read, which is what leaves this [io] alone and a dropped record cheap: a caller that merely logs never observes the threshold, the run's operator alone does, and nothing beyond the write itself shows in its row. A written record is rendered runtime-side to extra/log.format's bytes, tag, newline-escaping and all, and has stderr_write!'s failure behaviour. @no-doctest: writes to standard error; no return value to assert

stdinreadline!

def stdin_read_line!() -> Option<string> [io]

Read one line from standard input. Some(line) on a read (trailing \n / \r\n stripped; an empty line is Some("")), None at EOF. @no-doctest: reads live stdin; result depends on input, cannot assert in a doctest

StdinRead

type StdinRead
  Bytes(bytes)
  TimedOut
  Eof
  Failed(string)
end

One timed raw stdin read's outcome. Variant order matters: the runtime builds these by tag, in declaration order; append at the end.

stdin_read!

def stdin_read!(max: int, timeout_ms: i32) -> StdinRead [io]

Read up to max bytes from stdin, waiting at most timeout_ms milliseconds for input: the primitive behind a terminal event loop ("give me input bytes or time out"). timeout_ms < 0 blocks indefinitely, 0 polls immediately; max is clamped to at least 1; the result is whatever ONE OS read yields (no accumulation loop). Reads the raw stdin descriptor directly, with no buffering layer, and interleaving with stdin_read_line! cannot swallow bytes. A parked read is cancellable by actor.shutdown!, like stdin_read_line!. The timeout is real wall time, outside the --deterministic virtual clock (terminal input is inherently non-replayable, like the read itself). @no-doctest: reads live stdin with a timeout; environment-dependent

TermError

type TermError
  NotATty
  Other(string)
end

A terminal-control failure. Variant order matters: the runtime builds these by tag, in declaration order; append new variants at the end.

TermSize

struct TermSize
  columns: int
  rows: int
end

A terminal's dimensions in character cells. Field order matters: the runtime builds these by slot. int per the index-surface convention.

stdinistty!

def stdin_is_tty!() -> bool [io]

Whether stdin is a terminal (false when piped or redirected). @no-doctest: answers for the live process's stdin; environment-dependent

stdoutistty!

def stdout_is_tty!() -> bool [io]

Whether stdout is a terminal (false when piped or redirected). @no-doctest: answers for the live process's stdout; environment-dependent

term_size!

def term_size!() -> Result<TermSize, TermError> [io]

The terminal size of stdout. NotATty when stdout is not a terminal. @no-doctest: queries the live terminal; environment-dependent

termsetraw!

def term_set_raw!(enabled: bool) -> Result<(), TermError> [io]

Enable or disable raw mode on stdin (character-at-a-time, no echo). The The first enable saves the original terminal state in a process-global slot; disable restores it. It is idempotent whichever way it is called, and the runtime guarantees the saved state is restored on process exit, on every exit path, fatal ones included, and a crashed program therefore never strands the terminal. @no-doctest: mutates the live terminal; needs a real tty

termrestorewrite!

def term_restore_write!(s: string) -> () [io]

Register bytes the runtime writes to stdout at process exit (single slot, overwrite semantics; the empty string clears). The terminal face registers alt-screen-leave and cursor-show through this, and a crashed full-screen program still leaves the user's screen usable. @no-doctest: mutates process-global exit state; side-effecting

fileread!

def _file_read!(path: string) -> Result<string, FsError> [fs_read]

The raw seam: the OS result with no path attached. The public file_read! below is what callers use.

file_read!

def file_read!(path: string) -> Result<string, FsFailure> [fs_read]

Read the entire file at path as a UTF-8 string. Ok(content) on success, Err(FsFailure) otherwise (the raw error is not collapsed). @no-doctest: reads the filesystem; needs a real file, cannot assert in a doctest

filecanonicalize!

def _file_canonicalize!(path: string) -> Result<string, FsError> [fs_read]

The raw seam: the OS result with no path attached. The public file_canonicalize! below is what callers use.

file_canonicalize!

def file_canonicalize!(path: string) -> Result<string, FsFailure> [fs_read]

Resolve path to its absolute, symlink-free form (the OS realpath). Ok(resolved) when path exists, Err(FsFailure) otherwise. @no-doctest: resolves against the real filesystem; needs a real path, cannot assert in a doctest

filewrite!

def _file_write!(path: string, content: string) -> Result<(), FsError> [fs_write]

The raw seam: the OS result with no path attached. The public file_write! below is what callers use.

file_write!

def file_write!(path: string, content: string) -> Result<(), FsFailure> [fs_write]

Write content to the file at path, creating or replacing it. Ok(()) on success, Err(FsFailure) otherwise. @no-doctest: writes the filesystem; side-effecting, cannot assert in a doctest

filewrite_bytes!

def _file_write_bytes!(path: string, content: bytes) -> Result<(), FsError> [fs_write]

The raw seam: the OS result with no path attached. The public file_write_bytes! below is what callers use.

filewritebytes!

def file_write_bytes!(path: string, content: bytes) -> Result<(), FsFailure> [fs_write]

Write raw content to the file at path, creating or replacing it: write counterpart of file_read_all!, for content that is not UTF-8 text. Ok(()) on success, Err(FsFailure) otherwise. @no-doctest: writes the filesystem; side-effecting, cannot assert in a doctest

filewrite_mode!

def _file_write_mode!(path: string, content: string, mode: i32) -> Result<(), FsError> [fs_write]

The raw seam for a mode-carrying write. fs.write_with_permissions! is the typed face; mode is the POSIX bit pattern its Permissions value computes, and nothing above this line spells a number.

filewritemode!

def file_write_mode!(path: string, content: string, mode: i32) -> Result<(), FsFailure> [fs_write]

Write content to path and leave the file at that mode, creating or replacing it. fs.write_with_permissions! is the typed face; see it for what that precision costs and gains. @no-doctest: writes the filesystem; side-effecting, cannot assert in a doctest

filewritebytesmode!

def _file_write_bytes_mode!(path: string, content: bytes, mode: i32) -> Result<(), FsError> [fs_write]

The raw seam for a mode-carrying byte write. fs.write_bytes_with_permissions! is the typed face.

filewritebytes_mode!

def file_write_bytes_mode!(path: string, content: bytes, mode: i32) -> Result<(), FsFailure> [fs_write]

Write raw content to path and leave the file at that mode, creating or replacing it. fs.write_bytes_with_permissions! is the typed face; see it for what that precision costs and gains. @no-doctest: writes the filesystem; side-effecting, cannot assert in a doctest

setpermissions!

def _set_permissions!(path: string, mode: i32) -> Result<(), FsError> [fs_write]

The raw seam for chmod(2). fs.set_permissions! is the typed face.

set_permissions!

def set_permissions!(path: string, mode: i32) -> Result<(), FsFailure> [fs_write]

Set the mode of the existing file or directory at path to mode. fs.set_permissions! is the typed face. @no-doctest: writes the filesystem; side-effecting, cannot assert in a doctest

createtemp_dir!

def _create_temp_dir!(parent: string, prefix: string, mode: i32) -> Result<string, FsError> [fs_write]

The raw seam for a race-free temporary directory. fs.create_temp_dir! is the typed face.

createtempdir!

def create_temp_dir!(parent: string, prefix: string, mode: i32) -> Result<string, FsFailure> [fs_write]

Create a fresh directory under parent at that mode, returning its path. fs.create_temp_dir! is the typed face; see it for the guarantee. @no-doctest: creates a directory; side-effecting, cannot assert in a doctest

File

File, or fs.File, is a runtime-managed native resource handle (HANKI.md §4): live native state the per-actor memory manager owns, released when the last handle drops. It is single-owner - never copied, moved across a send - and has no fields of its own, so its methods are its whole surface. A handle is minted by an API that opens one; it is never constructed.

fileopen!

def _file_open!(path: string) -> Result<File, FsError> [fs_read]

The raw seam: the OS result with no path attached. The public file_open! below is what callers use.

file_open!

def file_open!(path: string) -> Result<File, FsFailure> [fs_read]

Open the file at path for reading, returning a runtime-managed File handle. File is a native resource the runtime closes when its last handle drops; see HANKI.md §4. Ok(File) on success, Err(FsFailure) otherwise. @no-doctest: opens a real descriptor; needs a file, cannot assert in a doctest

fileexists!

def _file_exists!(path: string) -> Result<bool, FsError> [fs_read]

The raw seam: the OS result with no path attached. The public file_exists! below is what callers use.

file_exists!

def file_exists!(path: string) -> Result<bool, FsFailure> [fs_read]

Whether path exists (following symlinks). Ok(true) / Ok(false); a missing path is Ok(false), only a real stat failure is Err(FsFailure). @no-doctest: stats the filesystem; needs a real path, cannot assert in a doctest

fileis_dir!

def _file_is_dir!(path: string) -> Result<bool, FsError> [fs_read]

The raw seam: the OS result with no path attached. The public file_is_dir! below is what callers use.

fileisdir!

def file_is_dir!(path: string) -> Result<bool, FsFailure> [fs_read]

Whether path exists and is a directory. A missing path or a non-directory is Ok(false); a real stat failure is Err(FsFailure). @no-doctest: stats the filesystem; needs a real path, cannot assert in a doctest

filemetadata!

def _file_metadata!(path: string) -> Result<Metadata, FsError> [fs_read]

The raw seam: the OS result with no path attached. The public file_metadata! below is what callers use.

file_metadata!

def file_metadata!(path: string) -> Result<Metadata, FsFailure> [fs_read]

Stat path, following symlinks. A missing path is Err(NotFound) rather than an empty success: this retrieves something, and a retrieval that could not be performed is a failure. The predicates above - file_exists!, file_is_dir! - are the other arm of that rule, where absence IS the answer. @no-doctest: stats the filesystem; needs a real path, cannot assert in a doctest

dirlist!

def _dir_list!(path: string) -> Result<List<DirEntry>, FsError> [fs_read]

The raw seam: the OS result with no path attached. The public dir_list! below is what callers use.

dir_list!

def dir_list!(path: string) -> Result<List<DirEntry>, FsFailure> [fs_read]

List the entries of the directory at path as DirEntry values, sorted by name. A missing path or a non-directory is Err(FsFailure). @no-doctest: reads the filesystem; needs a real directory, cannot assert in a doctest

_mkdir!

def _mkdir!(path: string, parents: bool) -> Result<(), FsError> [fs_write]

The raw seam: the OS result with no path attached. The public mkdir! below is what callers use.

mkdir!

def mkdir!(path: string, parents: bool) -> Result<(), FsFailure> [fs_write]

Create the directory at path. With parents, also create missing parent directories (mkdir -p) and treat an already-present directory as success; otherwise a missing parent or an existing path is Err(FsFailure). @no-doctest: mutates the filesystem; side-effecting, cannot assert in a doctest

_delete!

def _delete!(path: string, recursive: bool) -> Result<(), FsError> [fs_write]

The raw seam: the OS result with no path attached. The public delete! below is what callers use.

delete!

def delete!(path: string, recursive: bool) -> Result<(), FsFailure> [fs_write]

Remove the file or directory at path (a symlink is removed and never its target). A non-empty directory needs recursive; a missing path is Err. @no-doctest: mutates the filesystem; side-effecting, cannot assert in a doctest

_copy!

def _copy!(src: string, dst: string, overwrite: bool) -> Result<(), FsError> [fs]

The raw seam: the OS result with no path attached. The public copy! below is what callers use.

_rename!

def _rename!(src: string, dst: string, overwrite: bool) -> Result<(), FsError> [fs_write]

The raw seam: the OS result with no path attached. The public rename! below is what callers use.

rename!

def rename!(src: string, dst: string, overwrite: bool) -> Result<(), FsFailure> [fs_write]

Move src to dst through the OS's own rename: the destination goes from old to new with nothing in between, and no window where it is absent or half-written. That atomicity is the reason to reach for this over a copy then a delete, which has both.

overwrite decides an existing dst: false is Err(AlreadyExists), true replaces it in one step. Neither value is the default: replacing a file is not something to do because nobody said otherwise, and the write-to-temp-then-swap idiom needs the replacing form, and both therefore have to be reachable by name.

Atomic-or-fail: endpoints on different filesystems are Err(CrossDevice), not a silent copy-and-delete that would not be atomic. [fs_write] rather than [fs]: a rename moves a directory entry and never reads the file's contents, and a program granted only writes can still move files.

@no-doctest: mutates the filesystem; side-effecting, cannot assert in a doctest

copy!

def copy!(src: string, dst: string, overwrite: bool) -> Result<(), FsFailure> [fs]

Copy the file at src onto dst. Only regular files are copied; a directory src is Err(FsFailure). overwrite decides an existing dst, as on rename!.

The failure reports no path, this being the one call here that names two. The OS does not say which endpoint it tripped on: a NotFound is as often dst's missing parent as it is src - so naming src would state something the call has not established, and a wrong path is worse than none. @no-doctest: mutates the filesystem; side-effecting, cannot assert in a doctest

fileread_all!

def _file_read_all!(f: File) -> Result<bytes, FsError> [fs_read]

The raw seam: the OS result with no path attached. The public file_read_all! below is what callers use.

filereadall!

def file_read_all!(f: File) -> Result<bytes, FsFailure> [fs_read]

Read the rest of an open File as raw bytes. Ok(bytes) on success, Err(FsFailure) on an I/O error or a file already closed. @no-doctest: reads a real descriptor; needs a file, cannot assert in a doctest

file_close!

def file_close!(f: File) -> () [fs_read]

Close an open File, releasing its descriptor. Idempotent; dropping the last handle also closes it, and this is therefore an eager release. @no-doctest: side-effecting close; no return value to assert

TcpStream

TcpStream, or net.TcpStream, is a runtime-managed native resource handle (HANKI.md §4): live native state the per-actor memory manager owns, released when the last handle drops. It is single-owner - never copied, moved across a send - and has no fields of its own, so its methods are its whole surface. A handle is minted by an API that opens one; it is never constructed.

tcp_connect!

def tcp_connect!(host: string, port: u16) -> Result<TcpStream, NetError> [net]

Open a blocking TCP connection to host:port, returning a runtime-managed TcpStream resource. host is an IP literal or a name the OS resolves. Ok(TcpStream) on success, Err(NetError) otherwise. @no-doctest: opens a real socket; environment-dependent, cannot assert in a doctest

TcpListener

TcpListener, or net.TcpListener, is a runtime-managed native resource handle (HANKI.md §4): live native state the per-actor memory manager owns, released when the last handle drops. It is single-owner - never copied, moved across a send - and has no fields of its own, so its methods are its whole surface. A handle is minted by an API that opens one; it is never constructed.

tcp_listen!

def tcp_listen!(host: string, port: u16) -> Result<TcpListener, NetError> [net]

Bind a blocking listening socket to host:port and start accepting, returning a runtime-managed TcpListener resource. Ok(TcpListener) on success, Err(NetError) (e.g. AddrInUse) otherwise. @no-doctest: binds a real port; environment-dependent, cannot assert in a doctest

tcp_accept!

def tcp_accept!(listener: TcpListener) -> Result<TcpStream, NetError> [net]

Block until the next inbound connection arrives, returning its TcpStream. The peer address is not reported in v0. @no-doctest: blocks on a real accept; environment-dependent, cannot assert in a doctest

socket_read!

def socket_read!(s: TcpStream, max: u32) -> Result<bytes, NetError> [net]

Read once from s, returning up to max bytes: whatever a single blocking read yields. An empty bytes means the peer closed the connection (EOF). Err(NetError) on an I/O error or a closed socket. @no-doctest: reads a real socket; needs a peer, cannot assert in a doctest

socket_write!

def socket_write!(s: TcpStream, data: bytes) -> Result<i64, NetError> [net]

Write once to s from data, returning the number of bytes written (a single blocking write may accept fewer than data.length; loop with net.write_all! to write the whole buffer). Err(NetError) on an I/O error or a closed socket. @no-doctest: writes a real socket; needs a peer, cannot assert in a doctest

socket_close!

def socket_close!(s: TcpStream) -> () [net]

Close an open TcpStream, releasing its descriptor. Idempotent; dropping the last handle also closes it, and this is therefore an eager release. @no-doctest: side-effecting close; no return value to assert

listener_address!

def listener_address!(listener: TcpListener) -> Result<LocalAddress, NetError> [net]

The address listener is bound to. An ephemeral bind (port 0) reports the port the OS chose, which no other call recovers. Err(NetError) once the listener is closed. @no-doctest: needs a bound port; environment-dependent, cannot assert in a doctest

TlsStream

TlsStream, or tls.TlsStream, is a runtime-managed native resource handle (HANKI.md §4): live native state the per-actor memory manager owns, released when the last handle drops. It is single-owner - never copied, moved across a send - and has no fields of its own, so its methods are its whole surface. A handle is minted by an API that opens one; it is never constructed.

tls_connect!

def tls_connect!(host: string, port: u16) -> Result<TlsStream, TlsFailure> [net]

Open a TCP connection to host:port and complete the TLS client handshake over it, returning a runtime-managed TlsStream resource.

The socket is minted inside this call and the plaintext handle never surfaces, and that is the design: an ordinary call does not consume its argument, and a wrap!(TcpStream) form would leave the caller holding a live handle to the same descriptor, able to write past the encryption.

The peer is verified against the system trust store, and there is no insecure escape hatch: an opt-out is a posture decision nobody has asked for. TLS 1.3 is preferred and 1.2 accepted, every compiled-in 1.2 suite being ECDHE with an AEAD cipher, and forward secrecy and authenticated encryption hold whichever version the peer settles on. @no-doctest: opens a real connection and handshakes; needs a peer

tls_read!

def tls_read!(s: TlsStream, max: u32) -> Result<bytes, TlsFailure> [net]

Read once from s, returning up to max decrypted bytes. An empty bytes means the peer closed the connection, an EOF, as socket_read! spells it. Err(TlsFailure) on an I/O or protocol error or a closed stream. @no-doctest: reads a real connection; needs a peer

tls_write!

def tls_write!(s: TlsStream, data: bytes) -> Result<i64, TlsFailure> [net]

Write once to s from data, returning the number of bytes accepted. Loop with tls.write_all! to write a whole buffer, on the same contract socket_write! has: the seam promises a single write and no complete one. @no-doctest: writes a real connection; needs a peer

tls_version!

def tls_version!(s: TlsStream) -> Result<TlsVersion, TlsFailure> [net]

The protocol version the completed handshake settled on. Available for the life of the stream; Err(TlsFailure) once it is closed.

This is the observability half of the 1.2 fallback (tls_connect!): the security guarantee is held by the compiled-in suite list on either version, and auditing what a fleet negotiates, or holding a "must not travel over 1.2" floor, needs the answer, and no other call can give it. @no-doctest: reads a real connection; needs a peer

tls_close!

def tls_close!(s: TlsStream) -> () [net]

Close an open TlsStream, sending close_notify best-effort so the peer can tell a clean shutdown from a truncation, then releasing the descriptor. Idempotent; dropping the last handle also closes it, and this is an eager release. @no-doctest: side-effecting close; no return value to assert

TlsListener

TlsListener, or tls.TlsListener, is a runtime-managed native resource handle (HANKI.md §4): live native state the per-actor memory manager owns, released when the last handle drops. It is single-owner - never copied, moved across a send - and has no fields of its own, so its methods are its whole surface. A handle is minted by an API that opens one; it is never constructed.

tls_listen!

def tls_listen!(host: string, port: u16, chain: bytes, key: bytes) -> Result<TlsListener, TlsFailure> [net]

Bind host:port and compile chain and key into the configuration the returned TlsListener hands every connection it accepts.

Both are PEM: chain the server certificate followed by any intermediates, key its private key. They arrive as raw bytes and not as a config value, core not being able to name an extra type; tls.listen! is the face that takes tls.ServerConfig and reveals its Secret key here, once.

The compile happens once, at bind time, and never per accepted connection: parsing a chain and a key and standing up the crypto provider is not work to repeat on a hot path. It also means a key that does not match its certificate therefore fails here, at startup, and not under traffic.

This call touches no filesystem, whoever read the material having charged [fs_read] for it, and the row is therefore [net], as tcp_listen!'s is. Err(TlsFailure) names the bind address and not a peer. @no-doctest: binds a real port and needs real key material

TlsAccept

type TlsAccept
  Accepted(TlsStream)
  Rejected(TlsFailure)
end

What one tls_accept! produced, given the listener survived it.

The split is why it exists: a peer that resets mid-handshake and a listener whose accept failed are both TlsTransport, and the variant alone cannot say whether to keep serving. Only the seam knows which half failed, and it says so here in place of leaving an accept loop to guess: and guessing wrong ends a server on the first client that walks away. Variant order matters: the runtime builds these by tag.

tls_accept!

def tls_accept!(listener: TlsListener) -> Result<TlsAccept, TlsFailure> [net]

Accept the next inbound connection on listener and complete the TLS server handshake over it.

Ok(Accepted(s)) is a connection, reading and writing as a client-side one does. Ok(Rejected(f)) is this peer failing: a reset, a scan, or a client offering nothing we speak, with the listener unaffected and the next tls_accept! still serving; f is there to be logged or counted and never acted on. Err(TlsFailure) is the listener itself finished, and is the only outcome that ends an accept loop.

Both halves park interruptibly, and actor.shutdown! reaches an actor waiting on a quiet port as readily as one mid-handshake. @no-doctest: blocks on a real accept; environment-dependent

tlslisteneraddress!

def tls_listener_address!(listener: TlsListener) -> Result<LocalAddress, TlsFailure> [net]

The address listener is bound to. An ephemeral bind (port 0) reports the port the OS chose, which no other call recovers; the listener_address! contract, for the TLS listener. @no-doctest: needs a bound port; environment-dependent, cannot assert in a doctest

tlslistenerclose!

def tls_listener_close!(listener: TlsListener) -> () [net]

Close an open TlsListener, releasing its bound port and dropping the compiled configuration with it. Idempotent; dropping the last handle also closes it, and this is an eager release. @no-doctest: side-effecting close; no return value to assert

listener_close!

def listener_close!(listener: TcpListener) -> () [net]

Close an open TcpListener, releasing its bound port. Idempotent; dropping the last handle also closes it, and this is an eager release. @no-doctest: side-effecting close; no return value to assert

process_run!

def process_run!(cmd: string, args: List<string>, stdin_input: string) -> ProcessResult [process]

Spawn cmd with args, feed stdin_input to the child's stdin, wait for it to exit, and return its captured output and outcome. args are passed verbatim: no shell, no globbing. @no-doctest: spawns a subprocess; environment-dependent, cannot assert in a doctest

processrunattached!

def process_run_attached!(cmd: string, args: List<string>) -> ProcessOutcome [io, process]

process_run!'s attached counterpart: the child inherits this process's stdin, stdout and stderr in place of pipes, and an interactive program, an editor or a pager, therefore takes over the terminal while the caller waits for it. Nothing is captured, and the return is therefore the bare ProcessOutcome and no ProcessResult whose two string fields could only ever be empty. It takes [io] alongside [process]: handing the terminal TO a subprocess is the same capability as writing to it, so --deny io reaches it, as --deny env reaches process_run_opts!.

While the child runs, the parent ignores the terminal's interrupt and quit signals, and Ctrl-C therefore reaches the child in place of killing the program waiting on it; and the parent's own raw mode, if it had any, is dropped for the child and restored on return. Refused under --deterministic: a child's terminal interaction cannot be replayed, and a run that claimed to reproduce would be lying. @no-doctest: hands the terminal to a subprocess; cannot assert in a doctest

processrunopts!

def process_run_opts!(cmd: string, args: List<string>, stdin_input: string, cwd: string, env_pairs: List<string>, env_remove: List<string>, env_clean: bool) -> ProcessResult [env, process]

process_run! with every spawn attribute at once: process.SpawnOptions flattened onto the seam, the way sqlite.open_with! flattens OpenOptions. This is the one capturing spawn that takes any of them: process.run_with! and process.run_in! are Hanki faces over it, since a seam per subset of the same options was four near-identical wrappers over one OS call. cwd empty inherits this process's directory, like process_run!. env_pairs is a flattened alternating [key, value, ...] overlay laid on top of the inherited environment (overlay wins on collision); an odd-length list is an intrinsic error, never a guessed pairing. The two an overlay alone cannot express: env_remove unsets names in the child (env -u NAME, which overlaying "" does not do: that sets the variable to empty), and env_clean inherits nothing at all, so the child's environment is env_pairs and no more. They apply in the order clean, remove, overlay, and an overlaid name therefore survives also appearing in env_remove. It takes [env] alongside [process]: handing a value TO a subprocess is the same capability as reading one out, so --deny env reaches it. That row is why process_run! is still its own seam and no face over this one: it passes no environment, and delegating here would charge it [env] it does not use. @no-doctest: spawns a subprocess; environment-dependent, cannot assert in a doctest

processrunattached_opts!

def process_run_attached_opts!(cmd: string, args: List<string>, cwd: string, env_pairs: List<string>, env_remove: List<string>, env_clean: bool) -> ProcessOutcome [env, io, process]

process_run_attached! with the same spawn attributes as process_run_opts!, the combination the named faces do not cover, since attaching the terminal previously composed with nothing. There is no stdin_input: an attached child reads the terminal, and there is no pipe to feed it. Refused under --deterministic for the reason process_run_attached! is; laying an environment on the child changes what it inherits and never who owns the terminal. @no-doctest: hands the terminal to a subprocess; cannot assert in a doctest

env_get!

def env_get!(name: string) -> Option<string> [env]

Read the environment variable name. Some(value) when set, None where unset. A value that is not valid UTF-8 also answers None in v0 (Hanki strings are UTF-8; a lossy or bytes-typed read is a future surface). Effect-gated [env]: environment reads are the classic exfiltration target (cloud credentials, CI tokens), so they must be visible in signatures and deniable with --deny env. @no-doctest: reads the live environment; result is host-dependent

program_path!

def program_path!() -> Option<string> [env]

The path of the program this process is running: for hanki run, the entry .hk source file (canonical and absolute, the $0 a shell script had); for an AOT-compiled binary, the executable itself, resolved, and a symlink or PATH spelling does not change the answer. None where no single program owns the process, in the REPL or a hanki test run, or where the host cannot say. The value argv[0] never was: main!(args) still gives what trails the program path alone, and this is that path through a door of its own, for finding files relative to the program and not to wherever the user stands. @no-doctest: reads process identity; result is host-dependent

env_vars!

def env_vars!() -> List<string> [env]

Every environment variable as a flattened alternating [name, value, ...] list, the same wire form process_run_opts! takes, and what is read out here can be handed straight back. extra/env.vars! builds the Map<string, string> face over it. An entry whose name or value is not valid UTF-8 is skipped, matching env_get!'s answer for the same case. Effect-gated [env] for the reason env_get! is: reading the whole environment at once is the same exfiltration target, only broader. @no-doctest: reads the live environment; result is host-dependent

cwd!

def cwd!() -> Result<string, FsError> [fs_read]

The process's current working directory. Err(NotUtf8) when the path is not valid UTF-8, Err(Other) when the OS refuses the read (a deleted or unreadable directory). It takes [fs_read] and not [env]: the answer comes from the filesystem and names a location on it, which is what --deny fs is about. There is no chdir! companion; see extra/env. @no-doctest: reads the live working directory; result is host-dependent

sleep!

def sleep!(ms: i32) -> () [time]

Block the calling actor for ms milliseconds; other actors keep running (one OS thread per actor). Negative ms is treated as 0. Under --deterministic the sleep is virtual: the actor parks on the deterministic gate's clock, which advances only when no actor can run, so a deterministic run sleeps in zero real time and replays the same schedule for the same seed. @no-doctest: blocks for real time; nothing to assert

now_ms!

def now_ms!() -> i64 [time]

Wall-clock time as milliseconds since the Unix epoch. Effect-gated [time]: a signature that reads the clock declares it, and a run can refuse it with --deny time. Under --deterministic this reads the gate's virtual clock in place of the OS clock, and a time-printing program still replays the same bytes for the same seed. @no-doctest: reads the live clock; result is host-dependent

monotonic_ms!

def monotonic_ms!() -> i64 [time]

Monotonic milliseconds since an unspecified fixed origin. Never goes backward, and it is therefore the clock for measuring elapsed durations; the absolute value is meaningless. Same [time] gate and --deterministic virtualization as now_ms!. @no-doctest: reads the live clock; result is host-dependent

monotonic_ns!

def monotonic_ns!() -> i64 [time]

monotonic_ms! at the clock's full tick: monotonic nanoseconds since the same origin, and the two reads therefore describe the same instant at two resolutions. i64 nanoseconds hold ~292 years of process uptime. Same [time] gate; under --deterministic the virtual clock ticks in nanoseconds too, and sleep_ns! advances this read by its own duration. @no-doctest: reads the live clock; result is host-dependent

sleep_ns!

def sleep_ns!(ns: i64) -> () [time]

sleep! at the clock's full tick: block the calling actor for ns nanoseconds, and a sub-millisecond pace (a 16.667ms frame) is expressible. Negative ns is treated as 0. Under --deterministic the sleep is virtual, as sleep!'s is. @no-doctest: blocks for real time; nothing to assert

localoffsetseconds_at!

def local_offset_seconds_at!(epoch_second: i64) -> Option<i32> [time]

Seconds east of UTC that the host's local time runs at epoch_second giving 7200 on a machine set to +02:00. It is per-instant and no constant because a zone observing daylight saving answers differently either side of a transition. None when the host cannot place that second on its local calendar; a host with no timezone configured reads as UTC, which is an answer and no failure.

Reads the machine's zone and no database: this is the host's own offset, never an arbitrary named zone (datetime's scope is fixed offsets, and IANA zones stay a community library). Same [time] gate as the clock reads, and it follows them under --deterministic, where it answers UTC: a run with a virtual clock and a host-derived offset would not replay the same bytes on another machine. @no-doctest: reads the host's timezone; result is host-dependent

random_u64!

def random_u64!() -> u64 [random]

A uniform 64-bit random value. Effect-gated [random]: minting randomness is an authority a signature must declare and a run can refuse with --deny random, and pure code must never mint, unannounced, a nonce. Under --deterministic the stream is seeded from the run seed (reproducible and not cryptographic); otherwise it draws from the OS CSPRNG. @no-doctest: draws fresh entropy; result is non-deterministic

random_bytes!

def random_bytes!(n: i32) -> bytes [random]

n random bytes (negative n yields an empty buffer). Same [random] gate and --deterministic seeding as random_u64!. @no-doctest: draws fresh entropy; result is non-deterministic

aead_encrypt

def aead_encrypt(key: bytes, nonce: bytes, plaintext: bytes, aad: bytes) -> bytes

XChaCha20-Poly1305: seal plaintext under a 32-byte key and a 24-byte nonce, authenticating aad alongside it. Returns the ciphertext with a 16-byte tag appended.

The two entries below are the seam's only pure members, and the exception is principled and no shortcut: encryption is a deterministic function of its inputs, there is no authority to gate and no […] to charge, and the nondeterminism in the flow is minting the key and nonce, which is random_bytes! above and already gated. They are here and not in extra/aead because the native seam is core-only (§17, The boundary, locked), and native they must be: pure-Hanki bulk crypto measures ~30 KB/s on the bytecode VM, which is not a speed anything can use.

extra/aead is the face, and is what to reach for: its opaque Key and Nonce check the two lengths once, at construction, and the wrong length stops being expressible. Called directly, as any raw seam primitive can be, a key that is not 32 bytes or a nonce that is not 24 faults the run on both tiers: there is no correct ciphertext to answer with, and returning something anyway would be the one outcome worse than stopping. @no-doctest: a concise EXPR => VALUE example needs a 32-byte key and a 24-byte nonce built first; extra/aead has the worked examples

aead_decrypt

def aead_decrypt(key: bytes, nonce: bytes, ciphertext: bytes, aad: bytes) -> Option<bytes>

The inverse: check ciphertext's tag against key, nonce and aad, and decrypt it where the tag verifies. None is authentication failing: a wrong key, a wrong nonce, mismatched aad, and altered ciphertext are one indistinguishable outcome of the same check, which the extra/aead face names AuthFailed. @no-doctest: needs a sealed ciphertext to open, which needs a key and nonce built first; extra/aead has the worked examples

SqliteConn

SqliteConn, or sqlite.SqliteConn, is a runtime-managed native resource handle (HANKI.md §4): live native state the per-actor memory manager owns, released when the last handle drops. It is single-owner - never copied, moved across a send - and has no fields of its own, so its methods are its whole surface. A handle is minted by an API that opens one; it is never constructed.

sqlite_open!

def sqlite_open!(path: string, read_only: bool, create: bool) -> Result<SqliteConn, DbError> [db]

Open the SQLite database at path, returning a runtime-managed SqliteConn resource. The security lockdown is always on, with no URI filenames, no ATTACH and no extension loading, and [db] therefore grants a database and never arbitrary file access. read_only opens without write access; create (write mode only) creates the file when absent. :memory: is an anonymous in-memory database. Only the lockdown is applied here; the extra/sqlite library owns the secure-modern-strict tuning, applied on top via sqlite_batch! pragmas and the two sqlite_set_*! config intrinsics. @no-doctest: opens a real database handle; mints a native resource, cannot assert in a doctest

sqlite_close!

def sqlite_close!(conn: SqliteConn) -> Result<(), DbError> [db]

Close an open SqliteConn, releasing its handle. Idempotent, and always Ok(()); dropping the last handle also closes it, and this is an eager release. Any later use of a closed connection is Err(Misuse). @no-doctest: side-effecting close; mints/uses a native resource

sqlite_batch!

def sqlite_batch!(conn: SqliteConn, sql: string) -> Result<(), DbError> [db]

Run a multi-statement SQL script (pragmas, DDL, migrations) with no parameters. This is how the extra/sqlite library applies its secure-modern-strict pragma defaults after the native secure-open. @no-doctest: runs SQL against a native handle, cannot assert in a doctest

sqlite_execute!

def sqlite_execute!(conn: SqliteConn, sql: string, params: List<DbValue>) -> Result<int, DbError> [db]

Run one non-query statement (INSERT, UPDATE, DELETE, DDL) with positional params, returning the number of rows changed. Multi-statement SQL is Err(MultiStatement); use sqlite_batch! for scripts. A parameter count that does not match the statement is Err(BindCount); a bound int beyond i64 is Err(IntOutOfRange). @no-doctest: runs SQL against a native handle, cannot assert in a doctest

sqlite_query!

def sqlite_query!(conn: SqliteConn, sql: string, params: List<DbValue>) -> Result<DbRows, DbError> [db]

Run a query with positional params and return every row fully materialized as DbRows. One native crossing per query bounds the row hot-path native. Same binding errors as sqlite_execute!. @no-doctest: runs SQL against a native handle, cannot assert in a doctest

sqliteexecutenamed!

def sqlite_execute_named!(conn: SqliteConn, sql: string, names: List<string>, values: List<DbValue>) -> Result<int, DbError> [db]

sqlite_execute! with named parameters as parallel lists: names.get(i) (bare, no :/@/$ prefix) binds values.get(i) to every statement parameter with that bare name, whichever prefix form the SQL spells it with. Coverage is checked each way: a statement name missing from names, a names entry the statement never uses, or a positional ? slot is Err(BindName). The extra/sqlite face owns the Map<string, DbValue> surface over this raw pair. @no-doctest: runs SQL against a native handle, cannot assert in a doctest

sqlitequerynamed!

def sqlite_query_named!(conn: SqliteConn, sql: string, names: List<string>, values: List<DbValue>) -> Result<DbRows, DbError> [db]

sqlite_query! with named parameters (see sqlite_execute_named! for the binding contract). @no-doctest: runs SQL against a native handle, cannot assert in a doctest

sqlitestreamstart!

def sqlite_stream_start!(conn: SqliteConn, sql: string, params: List<DbValue>) -> Result<(), DbError> [db]

Begin a streaming query on conn: prepare sql, bind positional params, and hold the statement natively for row-at-a-time reads with bounded heap (sqlite_stream_next!). One stream per connection: starting a second while one is open is Err(Misuse); ordinary sqlite_query! / sqlite_execute! calls interleave freely with an open stream. Same binding errors as sqlite_execute!. @no-doctest: runs SQL against a native handle, cannot assert in a doctest

sqlitestreamcolumns!

def sqlite_stream_columns!(conn: SqliteConn) -> Result<List<string>, DbError> [db]

The open stream's column names. Err(Misuse) with no stream open. @no-doctest: reads native connection state, cannot assert in a doctest

sqlitestreamnext!

def sqlite_stream_next!(conn: SqliteConn) -> Result<Option<List<DbValue>>, DbError> [db]

Step the open stream once: Ok(Some(row)) on a row, Ok(None) once exhausted (the stream closes itself). Any error also closes the stream: a failed stream is restarted and never resumed. Err(Misuse) with no stream open (including after exhaustion or sqlite_close!). @no-doctest: runs SQL against a native handle, cannot assert in a doctest

sqlitestreamclose!

def sqlite_stream_close!(conn: SqliteConn) -> Result<(), DbError> [db]

Close the open stream early, releasing its statement. Idempotent while the connection is open. @no-doctest: mutates native connection state, cannot assert in a doctest

sqliteallowtables!

def sqlite_allow_tables!(conn: SqliteConn, tables: List<string>) -> Result<(), DbError> [db]

Confine every later statement on conn to the named tables: the static untrusted-SQL policy. Reads and writes touch only the listed tables (matched without case, like SQLite table names); SELECT, scalar functions, CTE recursion, and transaction/savepoint control stay allowed; all DDL, pragmas, schema reads, and anything else are denied at prepare time, surfacing as SQLite's authorization error (Other, code 23). It composes with the always-on lockdown and can never weaken it, and it is one-way: nothing removes an installed policy. Confinement bounds what SQL may touch inside a legitimately opened database; the lockdown handles file reach, and sqlite_limit! bounds how much the engine will do. @no-doctest: mutates native connection config, cannot assert in a doctest

sqlitecreatefunction!

def sqlite_create_function!(conn: SqliteConn, name: string, f: (List<DbValue>) -> Result<DbValue, string>) -> Result<(), DbError> [db]

Register name as a scalar SQL function backed by the pure Hanki function f. The empty effect row on the parameter type is the purity gate, and an effectful closure is rejected at the call site. The function receives each SQL call's arguments as one List<DbValue> (any arity) and returns the result value, or Err(message) to fail the calling statement with that SQL error (surfacing to the statement's caller as a DbError). Registered SQLITE_DETERMINISTIC (usable in indexes and generated columns) and DIRECTONLY (not callable from untrusted-schema triggers/views). The connection owns the function for its own lifetime; re-registering a name replaces the SQL binding. A fault inside the function (a step-budget exhaustion, a malformed result) also fails the statement, and no fault ever unwinds through the SQLite frames. Under deterministic replay the enclosing statement remains one OS-seam crossing; the function runs as pure compute inside it. @no-doctest: registers against a native database handle

sqlitebackupto!

def sqlite_backup_to!(conn: SqliteConn, dest_path: string) -> Result<int, DbError> [db, fs_write]

Online-backup the database behind conn into the file at dest_path (created or replaced), returning the page count copied. Runs in bounded steps with a shutdown check between them, and actor.shutdown! therefore aborts a long backup; a busy source retries after a short interruptible sleep. Effect row [db, fs_write]: this is the one sqlite primitive that writes a caller-chosen filesystem path, and it charges the file-write capability on top of [db]: --allow db --deny fs_write reaches the database and cannot export it, keeping "[db] grants a database, never arbitrary file access" literally true. The destination path is taken verbatim (no URI interpretation), and no SQL ever runs on the destination. @no-doctest: writes a real file from a native handle, cannot assert in a doctest

sqlite_limit!

def sqlite_limit!(conn: SqliteConn, id: int, value: int) -> Result<int, DbError> [db]

Read or set one of SQLite's per-connection engine limits (sqlite3_limit). id is the stable SQLITE_LIMIT_* category number: 0 LENGTH, 1 SQL_LENGTH, 2 COLUMN, 3 EXPR_DEPTH, 4 COMPOUND_SELECT, 5 VDBE_OP, 6 FUNCTION_ARG, 7 ATTACHED, 8 LIKE_PATTERN_LENGTH, 9 VARIABLE_NUMBER, 10 TRIGGERDEPTH, 11 WORKERTHREADS; an unknown id is Err(Misuse). value >= 0 sets the limit (SQLite clamps to its compile-time ceiling) and returns the prior value; value < 0 reads without changing. The extra/sqlite hardened profile is built on this; the limits bound what the engine will accept, a hostile-SQL defense, a different axis from the run-level --max-steps/--max-bytes budgets that bound the Hanki side. @no-doctest: mutates native connection state, cannot assert in a doctest

sqlitelastinsert_rowid!

def sqlite_last_insert_rowid!(conn: SqliteConn) -> Result<int, DbError> [db]

The rowid of the most recent successful INSERT on this connection. @no-doctest: reads native connection state, cannot assert in a doctest

sqlite_changes!

def sqlite_changes!(conn: SqliteConn) -> Result<int, DbError> [db]

The number of rows changed by the most recent statement on this connection. @no-doctest: reads native connection state, cannot assert in a doctest

sqlite_autocommit!

def sqlite_autocommit!(conn: SqliteConn) -> Result<bool, DbError> [db]

Whether the connection is in autocommit mode (i.e. not inside an explicit transaction). It is not readable through SQL, and it is therefore a native accessor. @no-doctest: reads native connection state, cannot assert in a doctest

sqlitesetdefensive!

def sqlite_set_defensive!(conn: SqliteConn, enabled: bool) -> Result<(), DbError> [db]

Toggle SQLITE_DBCONFIG_DEFENSIVE: reject the writable-schema corruption paths. A db-config and no pragma, and it cannot be set from SQL; the extra/sqlite library sets it through here. @no-doctest: mutates native connection config, cannot assert in a doctest

sqlitesetstrict_sql!

def sqlite_set_strict_sql!(conn: SqliteConn, strict: bool) -> Result<(), DbError> [db]

Toggle strict SQL: strict = true turns SQLite's double-quoted-string literals off for DDL and DML (the "typo"-matches-a-string footgun). A db-config, and not settable from SQL. @no-doctest: mutates native connection config, cannot assert in a doctest

actors!

def actors!() -> () [io]

List the live actors to stderr for debugging: one row per registered actor with its id, class, liveness, mailbox depth, and supervisor. Reaches the scheduler's actor registry, a native runtime primitive and no OS capability, and works on both tiers. Effect-tracked [io], and no dbg!-style exemption, and it cannot be called from pure code. @no-doctest: prints the live actor table to stderr; no return value to assert

get_state!

def get_state!<T>(a: ActorRef<T>) -> () [io]

Print the live state of one running actor to stderr for debugging: its current state fields rendered as a struct (the shared dbg! renderer), captured at the actor's next inter-handler safe point. The live counterpart to the crash-time state dump. An actor wedged inside a long-running handler, or a self snapshot, which can never reach its own safe point, is reported unavailable, the documented Erlang sys:get_state limit. Like actors!, an ordinary [io] action (NOT a dbg! exemption) that works on both tiers. (Named get_state!, not state!, state being a reserved keyword.) @no-doctest: prints a live actor's state to stderr; no return value to assert