PUBLIC MODULE · lib/env.js

Env

lib/env.js — Env: the sole global environment object.

The environment loads and manages everything a session uses: merged settings, endpoint-keyed authentication, protocol classes, endpoints, tools, context-window sizing, and the titled folder surface. Env is the shared registry referenced by IO and Agent. Usage accounting is kept by IO and Agent: IO reports usage per turn, and Agent sums it in memory; Env does not track or persist usage accounting.

Settings scan in LAYERS (lib/env/paths.js): the package folder's top-level JSON files, then the namespace user settings folder (created when missing and the target of every DYNAMIC settings write), then the project's namespaced settings and auth files ONLY. Files merge into one tree — objects deep-merge, arrays concatenate, scalar collisions follow incidental read order (file ordering is NOT a precedence mechanism). A package or project settings-file parse failure crashes the load (fail fast); any other file's parse failure is ignored. Explicit constructor arguments override merged settings.

Authentication: authSet(endpoint, data) creates/updates auth-${endpoint}.json holding { [endpoint]: data } (tokens + cached model list). Endpoint configuration lives at settings.providers[endpoint].

Protocols: providers/*.js and configured settings.providerPaths roots (package/settings scope only — never the project folder) are scanned as default-exported classes and completed with OpenAI HTTP defaults. This module exports the provider contract, error taxonomy, and HTTP defaults for provider authors; lib/io.js re-exports them for IO consumers.

Environment surface (env.environment): the folders that matter to a session, each a TITLED {title, path} pair — the project folder (cwd), the harness source, the tool roots (added by loadTools); consumers may push more (same shape). The TUI lists them when a fresh session starts.

class Env class

The sole global environment object: settings, auth, providers, tools, and the titled folder surface.

static create(options, initOptions = {…}) method

Create a ready-to-use environment. Construction loads synchronous settings first; this factory then loads the asynchronous provider and tool registries, so callers never receive a half-initialized Env.

constructor({ dir = PACKAGE_DIR, settingsDir, cwd = process.cwd(), settings } = {…}) constructor

Build the environment: scan and merge the layered settings (the package folder, user settings folder, and namespaced project files — see lib/env/load.js), seed the titled folder surface, register the built-in tools. Providers and tools load afterwards (loadProviders/loadTools).

get settings() getter

the merged settings tree (live reference)

get toolTimeout() getter

Default Agent-enforced duration of one tool call. The merged toolTimeout setting accepts milliseconds or a unit string; absent means 120 seconds.

get toolTimeoutLimit() getter

Hard ceiling for a tool's schema-declared, model-requested timeout argument. The merged toolTimeoutLimit setting accepts milliseconds or a unit string; absent means twenty minutes.

get contextGuardCap() getter

The Agent tool loop's runaway guard: the OVERALL context-usage ceiling, a (0,1] fraction of the model's context window (settings value > 1 reads as a percentage). Crossing it refuses to continue — user oversight and /compact are required first (lib/agent/run.js owns enforcement). The merged contextGuardCap setting; absent means 90% — always leaving room for /compact itself to run.

get contextGuardTurnCap() getter

The Agent tool loop's runaway guard: the PER-TURN context-growth ceiling, a (0,1] fraction of the model's context window — even starting near-empty, one agent turn alone cannot consume more than this before it's stopped. The merged contextGuardTurnCap setting; absent means 40%.

get maxAttempts() getter

Provider-request attempts per IO turn (settings.maxAttempts, default 3): the first write plus its retries — only failure classes that can heal with time retry (Env.RETRYABLE_KINDS); the interval grows from retryBase, doubling per attempt, capped at retryMax (lib/env/reliability.js). The Agent's run loop owns enforcement.

retryDelay(attempt) method

One retry's delay (lib/env/reliability.js): retryBase doubling per attempt, capped at retryMax, jittered against lockstep.

contextWindow(endpoint, model) method

The model's context window in tokens, WHEN KNOWN: an explicit provider-settings override (<provider>.contextWindow) wins over the cached model descriptor's contextWindow (the models() snapshot persisted in the provider's auth namespace). Returns null when unknown — consumers hide the window readout then rather than show a guessed number.

contextConsumption(context, lastUsage) method

Current context consumption in tokens: the provider-reported input count of the last request when available (the exact number the provider processed), else the word-count estimate of the live context (the token-per-word likelihood ratio).

resolveSystemPrompt() method

The system-prompt text(s) for a FRESH session — read fresh from disk on EVERY call, never cached (lib/env/system-prompt.js).

endpointSettings(endpoint) method

Live endpoint configuration plus endpoint-keyed auth/model cache (lib/env/endpoints.js).

authSet(endpoint, data, { scope } = {…}) method

Persist endpoint-keyed auth/model data: creates/updates auth-<endpoint>.json holding { [endpoint]: data } (tokens + cached model list) in the endpoint's scope — the project folder ("local") or the effective user-settings folder ("package", the default; the package folder itself is used only when settingsDir is null). Merges into the live settings tree under the endpoint key.

saveEndpoint(name, endpoint, { scope = "package" } = {…}) method

Add/update one configured endpoint and persist it to the scope's settings file (the user settings folder or namespaced project file).

removeEndpoint(name) method

Remove an endpoint — the /logout contract: its configuration drops out of the scope's settings file, its auth file is deleted, and every in-memory trace is removed. Dynamic (environment-detected) endpoints clear in-memory only.

batch(fn) method

Run fn inside a WRITE BATCH: every settings-file write it triggers (authSet, saveEndpoint — a login performs several) is held in memory and each file lands ONCE, atomically, when the outermost batch ends. Cloud sync clients never see a partial or repeated update (see lib/env/persist.js). Batches nest.

flushSettings() method

Flush any batched settings writes now (a no-op outside a batch).

saveTheme(name) method

Select and persist a named TUI theme in the effective settings file.

registerAgent(agent) method

Register an active Agent. Agent construction owns this call; registration retains the user-facing session until Agent.close() expressly removes it. Registration order is preserved.

agents() method

Snapshot the active sessions. Closing an Agent removes it synchronously; inactivity and garbage collection never change this lifecycle registry. The TUI and all agent-list consumers must use this canonical list.

removeAgent(agent) method

Remove an active Agent. Agent.close() owns normal lifecycle use; this method exists for Env-owned cleanup only.

onEvent(event, callback) method

Subscribe to a generic Env lifecycle event (distinct from Agent.onEvent's numeric turn events — these are Symbols). The possible event values (the Env.ENV_EVENT constants):

offEvent(handle) method

Remove a generic Env lifecycle listener by its opaque handle.

agentsEndpointLimit(endpoint, model) method

Effective active-Agent cap for an endpoint/model scope.

agentsAt(endpoint, model) method

Count registered active Agents at an endpoint, optionally one model.

agentEndpointAvailable(endpoint, model) method

Current unreserved active-Agent capacity; never reserves a slot.

agentsEndpointLimitSet(options) method

Persist an endpoint/model active-Agent cap.

registerProvider(name, ProviderClass) method

Register and internally complete a basename-keyed protocol class.

provider(name) method

a communication protocol class by basename

providerNames() method

registered communication protocol basenames

knownEndpoints() method

Known endpoint presets offered by the login wizards (lib/env/endpoints.js).

endpointScope(name) method

The scope an endpoint's settings/auth live in: "local" when its configuration came from the namespaced PROJECT settings file, "package" otherwise (user settings or package files).

endpoint(name) method

one named endpoint configuration

endpointLocal(name) method

Is an endpoint LOCAL (lib/env/endpoints.js endpointLocal): project- scope configuration or a record publishing local: true / remote: false — the shared predicate the model-list access policy classifies with.

isDynamic(name) method

Is the endpoint ENVIRONMENT-DEFINED (auto-detected from the process environment)? Dynamic endpoints are never persisted.

endpointNames({ includeSecret = false, access = "all" } = {…}) method

Endpoint names; secret entries are hidden unless explicitly requested.

get local() getter

Are LOCAL endpoints exposed to linked-agent spawning? (settings.local ??= true)

get remote() getter

Are REMOTE (public) endpoints exposed to linked-agent spawning? (settings.remote ??= true)

registerTool(name, fn, schema, { builtin = false, file } = {…}) method

Register a tool into the flattened callable lookup (lib/env/tool-registry.js — the safe/interactive schema metadata contract lives there).

updateToolStatus(name, info) method

Merge a live-status object into a tool's registry entry; /status prints it, the TUI renders it below the status bar.

toolStatus() method

tools with a live status object

toolNames() method

all flattened tool names

get safe() getter

The SAFE VIEW of this environment: one cached facade (a Proxy) whose TOOL surface is limited to read-only (safe: true) tools (lib/env/tool-registry.js). A VIEW, not a mode: one Env serves any number of consumers in either mode.

safeToolNames() method

The READ-ONLY tools: schemas published with safe: true.

toolSchemas(names, options) method

The publishable tool catalog. Omitted/["*"] = all current tools; [] = none; an explicit list exposes only recognized names. A secret: true tool is hidden unless includeSecret is set.

defaultsSchema() method

The DEFAULTS SCHEMA: every top-level settings key Env (or a loaded tool — see the tools.js module contract's settingsSchema()) understands, its default and a one-line description. Discovery only (ai init, API.md) — an unknown settings key is never rejected either way.

defaultProviderRoots() method

Protocol roots: installed/package providers, an optional custom package root, and configured paths (never the project folder).

loadProviders({ dirs, detect = true } = {…}) method

Load default-exported provider classes, keyed by each file basename.

endpointModels(name, { refresh = false, url, signal } = {…}) method

One endpoint's model MAP. With refresh, the endpoint is queried live (a failed query falls back to the cached + static list).

refreshModels({ timeout = 2000 } = {…}) method

Query EVERY configured endpoint for its available models (parallel, each bounded by timeout): the startup cache renewal.

detectEndpoints({ timeout = 300 } = {…}) method

Run provider-owned endpoint probes and fill only absent settings entries. Discoveries marked dynamic: true are ENVIRONMENT-DEFINED: never persisted — auth merges in memory only.

hasTool(name) method

@returns {boolean}

toolEntry(name) method

The registry entry for a tool ({fn, schema, builtin?, file?, safe?, interactive?, sandbox?, onTimeout?, status?}), or undefined.

defaultToolRoots() method

Tool-folder roots: the installed package and user settings folders, administrator-controlled system roots, and explicitly configured settings.tools roots.

defaultSkillRoots() method

Skill roots, accumulated from the package, settings, configured, environment, and project layers.

defaultPromptRoots() method

Prompt roots, ACCUMULATED the same way as skill roots.

skillCatalog({ debug = false, roots } = {…}) method

The merged skill catalog as # Skill Catalog text. Read fresh from disk on every call.

skillBodies(names, { roots } = {…}) method

The full bodies of the named skills, each wrapped in <skill name="..."> tags. Unknown names are skipped, not fatal.

promptCatalog({ debug = false, roots } = {…}) method

The merged prompt catalog, same shape as skillCatalog().

promptNames({ roots } = {…}) method

The merged prompt NAMES (sorted) — completion candidates. Read fresh from disk on every call.

promptNamesAsync({ roots } = {…}) method

Async prompt names, with the same roots and override semantics.

promptBody(name, { roots } = {…}) method

One prompt's body, verbatim (no interpolation). Read fresh from disk on every call.

loadTools({ dirs } = {…}) method

Tool scan-and-load: import each root's TOP-LEVEL JS modules (the scan is NOT recursive) and publish described-and-exported callables.

refreshToolAvailability() method

Recheck dynamic tool eligibility before a model request; does not rescan modules.

refreshTools() method

Rescan the tool roots and rebuild the tool/schema/callable maps. Invoke ONLY between model requests.

static toolTimestamp() method

The shared tool-refresh revision: bumped on every refreshTools() scan. Tool WRAPPERS stamp their private helper imports with it (./read/read.js?now=<revision>) so a helper edit applies on refresh even when the wrapper file itself is unchanged. Tool modules read it WITHOUT loading this library — toolRevision() in lib/env/mcp.js (the tiny runtime, published by the scan); this static remains for library consumers only.

static osSandboxKind() method

The OS write-sandbox mechanism in effect: "seatbelt", "bwrap", "delegated" (an OUTER jail already confines this process — the wrap is a passthrough), or null (no enforcement — the Agent forces safe mode then). Shared by every tool that opts in with the sandbox: true schema metadata — the Agent runs such a tool's forked worker under this kernel write-deny jail. Available to tool modules as the global Env.

static osSandboxAvailable() method

Is OS write-sandbox ENFORCEMENT in effect for this process — our own mechanism (seatbelt on macOS, bwrap on Linux) or an OUTER jail we detected (a nested seatbelt confines us already)? A single probe, cached — it does not repeat. There is NO opt-out: when false the Agent FORCES safe mode — mutation tools run only under active write enforcement, never unjailed by configuration. Available to tool modules as the global Env.

static osSandboxWrap(file, args, cwd) method

Wrap a program invocation in the OS write sandbox: the [file, argv] to spawn (the input unchanged when no mechanism applies). Available to tool modules as the global Env.

callTool(name, args, context) method

Exact flattened lookup + invoke. Missing names are ordinary errors (Agent surfaces them as tool-result errors), never a crash.

deepMerge(a, b) function

Objects merge recursively; arrays concatenate; scalar collisions take the later value (incidental read order — not a precedence mechanism).

a *
b *

Returns * — the merged value

Defined in lib/env/settings.js

parseDuration(value) function

Parse a duration: a positive millisecond numeral or a unit string ("500ms", "20s", "5m", "1.5h").

value number|string|undefined|null

Returns number|undefined — milliseconds (undefined passes through)

Defined in lib/env/duration.js

tryDuration(value) function

parseDuration that never throws — invalid/empty input is undefined.

Defined in lib/env/duration.js

DEFAULT_TOOL_TIMEOUT = 120_000 constant

Default Agent-enforced cap for one tool call: two minutes.

Defined in lib/env/tool-timeout.js

DEFAULT_TOOL_TIMEOUT_LIMIT = 20 * 60_000 constant

Maximum model-requested tool duration: twenty minutes.

Defined in lib/env/tool-timeout.js

TOOL_ON_TIMEOUT_LIMIT = 60_000 constant

Maximum Agent-facing onTimeout cleanup/final-response grace.

Defined in lib/env/tool-timeout.js

DEFAULT_CONTEXT_GUARD_CAP = 0.9 constant

The overall context-usage ceiling: 90% (always leaves /compact room).

Defined in lib/env/context-guard.js

DEFAULT_CONTEXT_GUARD_TURN_CAP = 0.4 constant

The per-turn context-growth ceiling: 40%.

Defined in lib/env/context-guard.js

retryDelay(settings, attempt) function

One attempt's delay: retryBase doubling per retry (attempt 0 is the first RETRY — the write before it already happened), bounded by retryMax, spread by up to a quarter of the base so simultaneous retries do not land in lockstep.

settings object
attempt number
0 for the first retry

Returns number — milliseconds

Defined in lib/env/reliability.js

RETRYABLE_KINDS = Object.freeze(["network", "provider", "auth"]) constant

The classified error kinds an Agent retries after a growing interval (lib/agent/run.js). "network" is transport flakiness; "provider" and "auth" cover provider-side statuses (a token-budget depletion included — its refill is time, so a later attempt may succeed). "malformed" is deterministic and "cancelled" is the user's own word — neither retries.

Defined in lib/env/reliability.js

awaitTimeout(ms, wait) function

awaitTimeout — race a completion event against a deadline (the Promise.race pattern), resolving true for completion and false for the deadline. wait receives an AbortSignal which is aborted on EITHER outcome, so an event listener registered by the caller with that signal cannot outlive the race. A deadline is a fallback, never a polling/sleep mechanism.

ms number
the deadline in milliseconds
wait (signal: AbortSignal) => Promise<unknown>
creates the completion wait

Returns Promise<boolean> — true = completion; false = deadline

Defined in lib/env/reliability.js

THINKING_LEVELS = ["default", "off", "low", "medium", "high", "xhigh"] constant

Selectable thinking levels, "default" first, then weakest → strongest.

Defined in lib/env/thinking.js

DEFAULT_THINKING = "high" constant

The effort used when the model advertises no default of its own.

Defined in lib/env/thinking.js

resolveEffort(think, { levels, defaultLevel } = {…}) function

Translate a think option to one native effort symbol.

think boolean|string|undefined
undefined/true: the model's default; false/"off": no reasoning; otherwise a level word
[model] object
[model.levels] string[]
native symbols the model accepts; unknown/empty: the translated word is trusted as-is
[model.defaultLevel] string
the model's advertised default

Returns string — the native effort symbol to send

Defined in lib/env/thinking.js

sortEfforts(levels) function

Order native effort symbols weakest → strongest, dropping duplicates (unranked symbols keep their relative order at the end).

levels string[]

Returns string[]

Defined in lib/env/thinking.js

registryEffortLevels(entry) function

The effort symbols a models.dev registry entry declares (reasoning_options: [{type: "effort", values}]), or undefined.

entry object
one registry model

Returns string[]|undefined

Defined in lib/env/thinking.js

supportedValues(message) function

The values an API error lists as supported ("... Supported values are: 'none', 'low', and 'high'."), or undefined.

message string

Returns string[]|undefined

Defined in lib/env/thinking.js

defineProvider(Protocol, { name } = {…}) function

Complete a standalone provider class with OpenAI-compatible defaults. The returned class is the sole runtime contract; function-module plugins are intentionally unsupported in this pre-release core.

Protocol Function
plugin class

Returns Function — completed provider class

Defined in lib/env/provider.js

class ProviderError extends Error class

Provider/transport error with a stable failure class.

Defined in lib/env/provider-error.js

constructor(kind, message, detail = {…}) constructor

Build a stable classified provider error.

classifyError(err, providerName) function

Classify a raw error into the stable provider taxonomy.

Defined in lib/env/provider.js

depletionError(classified) function

The shared TOKEN-DEPLETION predicate: exact signals only. 429 (Too Many Requests) and 402 (Payment Required) are the HTTP rate/quota statuses; otherwise the provider's own error body/code must NAME a budget — rate limits, quotas, billing (OpenAI's insufficient_quota, Anthropic's rate_limit_error). A 404 (a wrong model name), a 401/403 (a credential verdict), a transient 5xx, a mid-stream stall: none of these is a budget, and none of them marks the endpoint depleted. The message/body rides on the classified ProviderError (HttpStatusError bodies included — the first 200 chars travel in the message).

classified object
a classified error/event ({kind, message, status?})

Returns boolean

Defined in lib/env/provider.js

writeJsonAtomic(file, value) function

Atomically write one JSON value (pretty-printed, trailing newline): temp file in the same folder + rename.

file string
value *

Defined in lib/env/persist.js

ENV_EVENT = Object.freeze({…}) constant

Generic Env lifecycle events. Symbols avoid collisions with consumer values.

Defined in lib/env/events.js

defaultSettingsDir() function

The namespace user settings folder, resolved in compatibility order: environment overrides, an existing legacy .ai-settings home, then the namespace home (created when missing). Dynamic settings land here.

Returns string — the resolved absolute path

Defined in lib/env/paths.js

defaultSessionsDir() function

The namespace sessions folder under settings (created when missing).

Defined in lib/env/paths.js

isToolModuleFile(name) function

Tool-module filename filter: files named like benches, tests, or demos are NEVER imported by the tool scan. Importing a module runs its top level — a bench/test script executes its suite on import, printing results AND retaining its datasets in the module cache for the process's lifetime (observed: ai at ~30x baseline memory from benchmark modules under a host tools dir). Tokens split on any non-alphanumeric, so "contest.js" or "latest.js" are unaffected.

name string
file name

Returns boolean

Defined in lib/env/tools.js

async scanToolRoots(roots, env, { trustedRoots = [] } = {…}) function

Scan tool roots into a flattened name -> { fn, schema, file, safe? } map, plus every module's contributed settings-schema entries.

roots string[]
[env] object
the owning Env (handed to toolDescription(env) so a module can contribute settings-dependent entries; omitted for callers that have none, e.g. before construction completes)

Returns Promise<{tools: Map<string, {fn: Function, schema: object, file: string, safe?: true, trusted?: true}>, settingsSchema: Object}>

Defined in lib/env/tools.js

osSandboxAvailable() function

Is OS write-sandbox ENFORCEMENT in effect for this process — our own mechanism (seatbelt/bwrap) or an OUTER jail we detected (a nested seatbelt confines us already)? A single probe, cached. There is NO opt-out: when false the Agent FORCES safe mode (read-only tools only) — mutation tools run only under active write enforcement.

Returns boolean

Defined in lib/env/os-sandbox.js

osSandboxKind() function

The sandbox mechanism in effect: "seatbelt", "bwrap", "delegated" (an outer jail enforces writes — our wrap is a passthrough), or null (no enforcement — the Agent forces safe mode then).

Returns "seatbelt"|"bwrap"|"delegated"|null

Defined in lib/env/os-sandbox.js

osSandboxWrap(file, args = [], cwd = process.cwd(), workingDirectory = cwd) function

Wrap a program invocation in the OS sandbox: the [file, argv] to spawn — the wrapper and its arguments followed by the original program — or the input unchanged when no mechanism applies. The wrap is built per call with the CURRENT working folder (a session resume may have moved it; the cached probe only remembers the mechanism).

file string
the program to run (e.g. process.execPath)
args string[]
its arguments
[cwd] string
the project folder writes are limited to
[workingDirectory] string
process working directory within cwd

Returns [string, string[]]

Defined in lib/env/os-sandbox.js

class HttpStatusError extends Error class

HTTP response carrying a non-2xx status.

Defined in lib/env/http.js

constructor(status, statusText, body) constructor

Build the error from a non-2xx response (the body's first 200 characters ride in the message).

defaultConnect(url, aiio) function

stateless HTTP connection

Returns {url: string, aiio: object} — stateless HTTP connection

Defined in lib/env/http.js

async defaultSend(connection, msg) function

Default send: msg = [headers, body].

Defined in lib/env/http.js

async defaultSendHeaders(connection, headers) function

@param {object} headers plain object

connection object
@param {object} headers plain object

Defined in lib/env/http.js

async defaultSendBody(connection, body) function

Completes the request: POSTs the JSON body (nil body -> no payload) and stores the Response for defaultRead.

Defined in lib/env/http.js

async defaultRead(connection) function

Blocking-await line reader: resolves the next whole line-delimited JSON message, nil at end-of-stream.

Returns Promise<object|null>

Defined in lib/env/http.js

async defaultClose(connection) function

Teardown: cancel the body stream; idempotent.

Defined in lib/env/http.js

singleShot(init = {…}) function

Fetch init for ONE-SHOT catalog/registry calls (the /models listing, the models.dev registry): connection: close so the platform's keep-alive agent does NOT park the socket ESTABLISHED for reuse. A parked socket is a ref'd handle that holds the event loop open after the run resolves — the startup model refresh (cli-run.js's refreshModels) fires one fetch per configured endpoint and would otherwise hang every clean exit. The streaming request path (defaultSendBody) deliberately keeps its default: a conversation reuses its connection.

[init] object
caller's fetch init (headers, signal, …)

Returns object — init with a `connection: close` header merged in

Defined in lib/env/http.js