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):
Env.ENV_EVENT.AGENT_ADDED— an Agent registered with this EnvEnv.ENV_EVENT.AGENT_REMOVED— an Agent left this EnvEnv.ENV_EVENT.AGENT_START— an Agent began a runEnv.ENV_EVENT.AGENT_DONE— an Agent's run finished (done or error)Env.ENV_EVENT.IO_START— a provider request began ({io})Env.ENV_EVENT.IO_DONE— a provider request finished ({io, terminal})
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").
valuenumber|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.
settingsobjectattemptnumber- 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.
msnumber- 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.
thinkboolean|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).
levelsstring[]
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.
entryobject- 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.
messagestring
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.
ProtocolFunction- 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).
classifiedobject- 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.
filestringvalue*
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.
namestring- 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.
rootsstring[][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).
filestring- the program to run (e.g. process.execPath)
argsstring[]- 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
connectionobject- @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