Oska design notes

This is a working draft. Decisions that are hard to reverse are recorded in adr/, and terms are defined in GLOSSARY.md. The background research is in research/2026-11-ai-first-secure-language.md.

Principles

  1. Secure by default, unsafe only on explicit request. The safe path is the shortest path. Anything dangerous has to be opted into, is named, and can be found with grep.
  2. No ambient authority. I/O, network, filesystem, environment, clock, randomness, scheduling, and process spawning are capabilities: values passed as arguments. A function without a capability-bearing parameter cannot perform that effect.
  3. Legible to humans and models. A function signature tells you everything the function can do. There is one obvious way to write things, and no syntax depends on context. Everything that runs is visible on the page: there are no user macros (ADR 0003), no glob imports, and no code that runs when a module loads.
  4. Machine-first tooling. Every diagnostic has a stable code and a precise span. Where possible it also carries a fix that tools can apply automatically. Diagnostics are emitted as JSON with --format=json.

Safety baseline

Types and modules

Syntax

The syntax is close to Rust's, and departs from it only where Rust's rules depend on context (principle 3). The AST export is described in ast.md.

use std.sql.{Db, query}

pub struct Account { pub id: Int, balance: Int }

impl Account {
    pub fn deposit(var self, amount: Int)
        requires amount > 0
    {
        self.balance = self.balance + amount
    }
}

fn describe(s: Shape) -> String {
    match s {
        Shape.Circle(radius: r) if r > 100 => "big circle"
        Shape.Circle(radius: _) => "circle"
        Shape.Square(side: n) => "square"
    }
}

test "deposit adds to the balance" {
    var a = Account(id: 1, balance: 0)
    a.deposit(amount: 5)
    t.assert_eq(a.balance, 5)
}

Statements

Names, types, and calls

Control flow and patterns

Closures, contracts, and other forms

Type checking

The checker is oskac check, built as its own crate, oska-check, on top of the parser. Its diagnostic codes are E02xx. Its report format and codes are described in checker.md.

Scope and reporting

Inference

Method calls

The prelude

Capability rules

Patterns and values

Capabilities

fn read_config(dir: DirRead, path: Path) -> Result<Config, ConfigError> {
    let text = dir.read_to_string(path)?  // Untrusted<String>
    Config.parse(text)                     // validation: Untrusted<String> -> Config
}

Capabilities are the effect system. Oska has no separate effect annotations (ADR 0002).

Root of authority

fn main(fs: Fs, net: Net, args: Args) -> Result<(), AppError> {
    let config_fs = fs.restrict_to("/etc/myapp").read_only()
    let config = read_config(config_fs, Path.new("config.toml"))?
    serve(net.connect_only(), config)
}

Built-in capabilities:

CapabilityGrants
Stdin, Stdout, StderrStandard streams
FsFilesystem, only to attenuate: it reads and writes nothing itself. Attenuates to FsRead, FsWrite, a directory capability (Dir, DirRead, DirWrite), or a file capability (FileRead, FileWrite).
NetNetwork. Attenuates to NetConnect or NetListen.
EnvEnvironment variables. These can hold secrets.
Clock, RandomNondeterministic inputs
SchedChannels and select
SpawnStarting OS processes

Rules

Capability-bearing types

There are two deliberate exceptions to the rules above:

Package authority

Untrusted data and sinks

Trust is a flag that the checker tracks on text types (ADR 0006). Untrusted<String> is how a signature spells "this text may be untrusted". It is not a wrapper type.

pub fn parse(s: Untrusted<String>) -> Result<Email, ParseError> {
    if !s.contains("@") { return Err(ParseError.Invalid) }
    Ok(Email(raw: s.assume_trusted()))
}
SinkData position (untrusted OK)Structural position (trusted only)Not provided
SQL (std.sql)Parameters: db.query(sql"SELECT * FROM users WHERE id = ?", [id])Query text, which must be an sql"..." literalConcatenation or interpolation
Process (std.proc)Arguments: run("git", ["log", branch])The program nameA shell string. Running a shell is a separate, explicitly named capability.
HTML (std.html)Text, which is escaped for its contextTrustedHtmlWriting untyped strings into markup
PathsPath.from (see below)The root passed to restrict_to or restrict_to_file on Fs, FsRead, or FsWrite.. traversal outside a directory capability
NetworkRequest bodiesThe host passed to NetConnect
OutputStdout.println and Stderr

Concurrency

Contracts

fn withdraw(acct: Account, amount: Int) -> Result<Account, Error>
    requires amount > 0
    ensures |r| r.is_ok() implies r.unwrap().balance >= 0
{
    if amount > acct.balance {
        return Err(Error.InsufficientFunds)
    }
    Ok(Account(id: acct.id, balance: acct.balance - amount))
}

Testing

Runtime

The interpreter is oskac run file.osk -- args..., built as the crate oska-run.

Pipeline

Values

Capabilities

Failures

CodePanic
P0001integer overflow
P0002index out of bounds
P0003todo() reached
P0004division by zero

Strings

Concurrency in v0

Unsafe and FFI

Escape hatches are where memory-safety and capability guarantees break, so they have to be rare, visible, and contained.

Once escape hatches exist, the following policy applies:

Toolchain

Evaluation

The research does not show that AI-first design choices measurably improve the code agents write. Oska tests this itself instead of assuming it.

Open questions