@oddington/sdk
v0.1.59
Published
OS routine and response-handler SDK
Readme
OS SDK
The importable client for OS apps, routines and response handlers. Node operations require Node.js 22.12.0 or later. The root import and hosted Data and Files APIs work in browsers. Installing the package grants no authority.
When developing OS alongside a customer repository, use the repository's
dev:customer helper to run Bun tests and TypeScript checks
against the built local SDK without changing the customer's released pin,
lockfile or installed package. Build the SDK and CLI together first; the same
helper can execute that built CLI from the customer checkout.
Hosted Data and shared Files reach OS directly through /api/data and /api/files.
Inbox owns workspace-hosted /api/inbox/check, /api/inbox/publish,
/api/inbox/resolve. Request-key recovery and captured
publication validation are internal behavior of these operations. Retrying the
same key and content returns the original result; changed content conflicts.
Inbox File previews and downloads use package-root files.read(fileId) with
current workspace access. Item visibility does not grant File access. The SDK
exposes no raw-credential API.
Hosted apps use inbox.questions.list({ routines }) for AnswerableQuestion[].
Derive routines from the existing app declaration with
[...new Set(Object.values(appDeclaration.routines).map(binding => binding.routine))].
These inner canonical names narrow relevance only; they grant no answer or
Record-search authority. The ordinary authenticated Inbox context needs no app
identity or declaration provisioning. The filter is required; [] returns no
Questions after authentication. The SDK validates and privately snapshots the
filter across pagination. Local apps use inbox.questions.list() instead, bound
to their trusted host's declaration; any supplied filter is invalid locally.
Both use the same answer and Record-search methods, without a filter or prior list.
InboxItem remains the complete first-party Inbox value. Record refs are placement
hints only: match them against independently authorized Data, and omit ambiguous
detail placement. Keep drafts on failed or uncertain acceptance, prevent duplicate
interaction while submitting, and dismiss a Question only after acknowledged
acceptance. Refresh after a conflict; later effects remain Inbox work. These
interaction duties belong to the consuming app's interface.
npm install @oddington/sdkAn authored routine exports an inert definition from routines/<directory>/routine.ts:
import { defineRoutine, file, schema } from '@oddington/sdk';
export default defineRoutine({
id: 'invoice-import',
input: schema.object({ document: file({ storage: 'local', path: 'Statements' }) }),
access: { files: 'read' },
steps: {
read: {
title: 'Read receipt',
async run(input: { document: import('@oddington/sdk').File }, os) {
const response = await os.files.read(input.document.id);
return { summary: { files: 1 }, data: { text: await response.text() } };
},
},
},
result: schema.object({ recordsCreated: schema.integer() }),
async run(input, os) {
await os.step('read', input);
return { recordsCreated: 0 };
},
});Importing or defining performs no business work. Input and result use bounded
schemas; step inputs, summaries and detail are inferred from executable functions.
Each step runs at most once and returns { summary, data }. Unknown or repeated
steps fail the Run even if routine code catches the error; unreached steps are
skipped. Omitted input normalizes to {}. A top-level schema can supply a validated
immutable default with .default(value), such as
schema.object({ files: schema.array(file()) }).default({ files: [] }).
Nested defaults are unsupported and rejected. Omitted steps permit no step call;
omitted result permits no returned value. file() declares a complete OS File descriptor.
Its optional storage selects local or remote placement and its optional path
selects a managed OS parent such as Statements; it is never a machine or provider
path. The trusted launcher separately verifies current File-use authority. The supplied
runtime contains run.id, data, files, connections, inbox, and step.
Accepted input and saved values are immutable snapshots. Access declarations set
maximum named capabilities and grant no authority; the trusted host checks current
authority. Response handler IDs are local to the routine and must be unique. A
declared review handler crosses the Inbox publication seam as review; Inbox
retains the canonical invoice-import routine identity separately, without
deploying or executing that handler.
Long customer computation inside a remote step can report advancing work with
await os.run.progress({ completed, total }). Report progress after meaningful
batches of work rather than after every individual item. OS capability calls and
step transitions already count as activity automatically.
Microsoft Connection configuration has a separate, optional access description.
It preserves supplied strings, including existing read and read-write labels,
and does not grant or restrict Microsoft permissions. Management results omit
this metadata when absent. Setup and safe inspection are documented in the
CLI Connections guide.
With access: { ai: true }, os.ai.generateText returns non-empty { text }.
os.ai.generateObject returns { object } validated against the requested
schema, with its inferred type. An invalid answer throws AIError
with code invalid_output, a clear bounded detail, and the original final
answer as optional text for routine-owned recovery. The text is bounded at
256 KiB and excluded from the error message and ordinary JSON serialization.
AI failures have plain-language messages and keep their structured code.
Optional detail explains a known runner limit, exit, response failure or request
validation rule. Use these facts rather than parsing messages. A code-only legacy
failure does not establish a more specific cause or justify a retry. Local Run
activity and AI inspection preserve these diagnostics even when the routine
catches the error and continues; no provider logs or private answer text are
included in the ordinary activity summary.
The routine can inspect it explicitly, apply its own recovery rules, report
an item failure and continue; it must not cast a rejected answer to the promised
type. OS does not coerce, drop fields, repair or retry automatically.
generateObject also accepts one exact outer Markdown code fence (empty or
json language tag), then applies the same strict schema checks. It does not
extract JSON from explanatory prose or repair the answer. RoutineAIError
remains a compatibility alias of AIError, so existing instanceof checks work.
For a File that needs local tool-assisted inspection, explicitly enable tools on that call:
const { object } = await os.ai.generateObject({
input: document,
instructions: 'Read this document and report only grounded facts.',
schema: documentFacts,
tools: true,
});Omitting tools or setting it to false keeps the existing tool-free behavior.
OS supplies and sandboxes the tools; routines do not choose CLI flags or shell
commands. This first version requires a File input and local macOS execution
with Claude Code 2.1.285 or newer. Unsupported hosts reject with
AIError('capability_unavailable') rather than silently ignoring the option.
The original stays read-only, temporary processing is disposable, and tool
network access is blocked. A tool-assisted answer still has to satisfy the
requested schema and the routine's own completeness checks.
Tool-enabled object generation uses the runner's structured final submission;
OS validates that value without extracting JSON from surrounding commentary.
Preserve safe AI error facts when reporting a per-document failure. Keep the
original answer (error.text) out of step data, business records and logs:
import { AIError } from '@oddington/sdk';
try {
const { object } = await os.ai.generateObject({ input, instructions, schema });
// Apply the routine's business validation before using the result.
} catch (error) {
if (!(error instanceof AIError)) throw error;
return { status: 'unavailable', error: {
code: error.code, message: error.message,
...(error.detail ? { detail: error.detail } : {}),
} };
}An AI call failure can be caught and handled without automatically failing the Run. Uncaught routine or step exceptions still fail execution, and outstanding AI calls drain before completion. The host retains authority, isolation, cancellation and byte limits. Local and remote Runs use the same API.
The bounded grammar supports closed objects with required properties, arrays with
an optional positive safe-integer maxItems business bound, booleans, strings (at most 4096 code points),
finite numbers and safe integers with optional inclusive bounds, and Files.
Omitting maxItems imposes no item-count ceiling; the complete serialized input
still shares the Run's byte bound, including every File descriptor and other field.
Definitions allow at most 64 steps, 64 total object properties across their schemas,
and eight schema levels. Run input, the result, and each step summary accept at
most 256 KiB of compact UTF-8 JSON. Each step accepts at most 1 MiB across its
summary and data together, measured as the sum of their compact UTF-8 JSON sizes
(including JSON quotes and escapes, excluding transport envelopes), without
truncation. Step inputs remain closed, immutable JSON but have no separate byte
ceiling because they are transient in-process values rather than transported or
retained Run evidence.
Across 64 steps, retained summary plus data is at most 64 MiB without a separate
aggregate output limit, plus 512 KiB for Run input and result. An oversized output
fails the step and Run once, even if routine code catches the rejection; later
steps do not execute and customer code is not automatically retried. The rejected
summary and data are not retained. The safe step_output_too_large failure keeps
only step, sizeBytes, limitBytes, summaryBytes, and dataBytes and explains
how to return fewer fields or process a smaller batch. limitBytes identifies
the violated summary or combined bound. Schema defaults share the input bound.
Titles contain at most 256 UTF-16 code units;
access dimensions and response handlers each have at most 64 entries. Functions,
accessors, sparse arrays, cycles and non-JSON values reject. The canonical routine
ID is a bounded lowercase alphanumeric/hyphen name, independent of its directory.
No entry, workspace, credentials or placement are authored in this definition.
Local Run history returns the newest 20 summaries per canonical routine, without deleting older Runs or unpublished changes. Step titles and bounded summary/detail values are saved with the accepted Run, so later source edits cannot rewrite them. Checkout-private execution JSON has a 128 MiB read envelope for up to 64 MiB of step output plus existing evidence; other local state files keep their 64 MiB envelope, including private diagnostics; app summaries never include those diagnostics. Publication reads the existing confirmed-receipt journal independently from the successful execution status. Refresh reads materialized state; watching provides connected updates without replay or interrupted-execution recovery.
The named CLI local launcher requires native Node TypeScript stripping and a
module routines/package.json whose engines.node range matches that executable.
Before inspection it pins the selected entry's reachable TypeScript/JavaScript
modules, checkout-local runtime packages, governing package metadata and static
assets referenced by new URL('./asset', import.meta.url). Literal imports,
dynamic imports with literal specifiers, and literal CommonJS require are
supported; computed loaders/assets, loader aliases, createRequire, evaluated
code, __dirname assets, custom export conditions and export fallback arrays
are refused. This first slice accepts at most 20,000 reachable files and 256 MiB;
reachable symlinks, native addons and imports outside the pinned copy are refused.
Historical runs output, unrelated routines, unused packages and files, package
manager .bin links, caches and logs are excluded. Run evidence stays with the
durable Run; artifacts are explicit File outputs. Dependency setup remains an
explicit workspace action. JSON input transport is bounded to 256 KiB.
The launcher renews its own 90-second ownership lease while queued or running;
each execution retains the existing 15-minute temporary capability/connection
bound. Lost ownership fails the Attempt without repeating customer code.
import { defineResponse, respond, resolveAsk } from '@oddington/sdk';
// List this action in the owning defineRoutine({ responses: [setStatus], access, ... }).
// The routine's single access declaration governs both its run and its actions.
const setStatus = defineResponse<{ recordRef: string; status: string }>({ id: 'set-status', async run({ input }, os) {
const changes = os.data.repository('general').changes();
changes.update(input.recordRef, { status: input.status });
await changes.publish();
return resolveAsk({ outcome: 'Status updated' });
} });
// Attach to a published Question with handler: respond(setStatus, { recordRef, status: 'complete' }).The SDK owns browser provider setup, email/passcode actions, persisted restoration, refresh and expiry. Create one opaque handle outside rendering and bind the app's presentation to its state:
import { auth, os, BrowserAuthError } from '@oddington/sdk';
const browserAuth = auth.create();
const unsubscribe = browserAuth.subscribe(state => {
// Render status, account.email/displayName, pending action and safe error reason.
});
const current = browserAuth.getState();
await browserAuth.sendCode(enteredEmail);
try {
await browserAuth.verifyCode(enteredEmail, enteredCode);
} catch (error) {
if (error instanceof BrowserAuthError) showFailure(error.reason);
}
os.init({ auth: browserAuth }); // Trusted workspace host startup only.
// Later: await browserAuth.signOut(); // Ends the active session.
// On document teardown (refresh, navigation or close):
unsubscribe();
browserAuth.dispose();In a local app started by os app dev or os app launch, the same handle
uses the CLI-backed app connection and presents the signed-in viewer:
const { status, account } = browserAuth.getState();
if ((status === "signed_in" || status === "reconnecting") && account) {
// account.email and account.displayName identify this app's current viewer.
// Keep viewer presentation while reconnecting; protected access awaits renewal.
}Use this account for viewer-aware presentation such as a Mine filter. Subscribe
to keep that presentation current; it is not an authorization decision. There
is no Inbox identity call, extra hosted lookup, browser provider session or CLI
token. The handle and os.init share one exact-origin connection. When an
established viewer's short access proof expires, the handle reports reconnecting
and retains the confirmed account for presentation while the SDK begins or joins
one shared, bounded recovery of the same session. No protected operation is
admitted before renewal succeeds. Confirmed connection and account recovery
restore signed_in; failed or unconfirmed recovery settles to unavailable.
A later lifecycle event or protected operation may recover the still-eligible
same session from unavailable; the SDK does not start an automatic retry loop
after an exhausted attempt. Local handles never report signed_out or expired.
Local sign-out immediately reports unavailable without an account, closes document
access and requests host-wide sign-out. Confirmed sign-out resolves while remaining
unavailable; failed confirmation rejects while remaining unavailable. Reopen
the app through OS after local sign-out.
Disposal preserves the connection's stored proofs for document refresh. Local
passcode and consent actions are unavailable: sign in with os login first.
In React, consume the handle as an external store so an early auth update cannot be missed between rendering and subscribing:
import { useSyncExternalStore } from "react";
const authState = useSyncExternalStore(browserAuth.subscribe, browserAuth.getState);Derive viewer-aware UI from the current authState.account; account changes do
not require refetching the app's records.
At a hosted workspace origin, auth.create() accepts no options. Trusted
same-origin OS setup selects the
provider internally through exact-origin GET /api/auth, owned by OS. The SDK
uses same-origin credentials, refuses redirects and bypasses caching. Missing or
invalid setup makes authentication unavailable without a legacy endpoint or
app-configured provider fallback; ordinary app modules do not consume this
internal bootstrap. Hosted state is initializing, signed_out, signed_in,
reconnecting, expired or unavailable; actions complete without returning a
session. Invitation-only code delivery never creates accounts or reveals account existence. Keep form
selection, entered values, copy and navigation in the app. The handle exposes no
provider, configuration, membership list or access/refresh credential. Consumers
never construct bearer headers. Multiple handles share provider coordination on
the same origin; each owns its subscription/disposal independently.
When established hosted access expires, including after a suspended tab resumes,
the handle reports reconnecting with the already confirmed account. Keep that
account for presentation only: protected SDK operations wait for one shared,
bounded provider refresh and never send expired access. Same-session success
restores signed_in without requiring sign-in UI. Retryable or unconfirmed
failure settles to unavailable; a later protected operation may retry the
still-eligible session, with no automatic retry loop or app-owned retry API.
Only confirmed loss or revocation of refresh authority permits expired or
signed_out. Explicit signOut() ends access immediately, including during
recovery.
dispose() releases that handle's subscriptions, timers, installed SDK context
and pending in-memory work. It does not sign out. A disposed handle cannot be
reused; a refreshed or reopened document creates a fresh handle and calls
os.init({ auth: browserAuth }) again. Use signOut() to end the browser session
and invalidate its installed capabilities intentionally.
The hosted runtime routes that exact capability path directly to OS while the app keeps its independently deployed interface and other paths. Operators must activate and verify the OS route before releasing a consumer that depends on it; installing this SDK does not deploy routes or migrate an app's sign-in UI.
Only trusted workspace host startup passes that exact registered handle to
os.init. Copying its public shape grants no authority. Fresh per-operation
authority fails closed on sign-out, replacement, expiry or disposal. Existing
hosts may continue supplying their shared refreshing session source:
import { os, data, files, DataOperationError, FileOperationError } from '@oddington/sdk';
os.init({ auth }); // OS-owned startup, once before app modules run.
// App code uses the installed capabilities directly.
const general = data.repository('general');
try {
const selected = await general.records.query({ invoices: { $: { limit: 20 } } });
const found = await general.records.search('acme overdue', {
collections: ['invoices'], limit: 20,
});
const statement = await files.create({
file: selectedFile, name: 'September statement.csv', path: 'HSC/2026/09',
});
const content = await files.read(statement.id);
// selected and found contain canonical Records and their opaque version guards.
// content is an unconsumed Web Response.
} catch (error) {
if (error instanceof DataOperationError || error instanceof FileOperationError) {
console.error(error.code, error.retryable); // Bounded public failure information.
}
throw error;
}This returns void and derives context from the current HTTPS workspace origin
under oddington.ai, excluding auth. Reinitialization replaces the previous
session subscription. Workspace existence and member access are checked by the
OS service. App modules use capability namespaces; routines and handlers receive
runtime context without calling os.init.
Hosted browser apps can initialize an existing named Data snapshot for local queries and shared Record Changes after host startup has installed that context:
const accounting = await data.init({ repository: 'general', snapshot: 'buchhaltung' });
const month = await accounting.records.query({
transactions: { $: { where: { date: { $gte: '2026-10-01', $lt: '2026-11-01' } } } },
});
const transaction = await accounting.records.read('transactions/example');
const changes = await accounting.changes.list();
const proposed = await accounting.transact(tx => {
tx.transactions[transactionId].update({ payment: 'paid' }).link({ invoice: invoiceRef });
});
await accounting.changes.publish(proposed, { summary: 'Invoice paid' });
// Or discard an exact current owned selection before publication:
// await accounting.changes.discard(proposed);
const unsubscribe = accounting.subscribe(({ records, changes }) => {
if (records) void reloadRecords(); // Ordinary reads now see the committed state.
if (changes) void reloadChanges();
});
// When this view leaves:
unsubscribe();Initialization confirms current access before opening local customer data. If
a complete artifact-and-Changes installation is already local, it returns
that coherent state without waiting for hosted freshness and reconciles once in
the background. A cold initialization installs the validated download and
relevant Changes together before returning. Published Changes are materialized before
querying or exact reads. Stored Changes without publication information remain
in changes.list() and never alter Records.
records.query, records.pages and records.all(query, { maxRecords }) use the existing query language and generated-schema inference.
They read locally and expose the initialized version in $meta.version. The
handle incorporates its own successful mutations. Subscribe to observe another
client's later Changes or an operator-built snapshot. A listener attached before
warm background reconciliation commits receives the same namespace notification;
after that reconciliation, a handle without listeners stays fixed except for its
own successful operations. Single-ref records.read(ref)
returns the complete structured Record or null from that same pinned state.
Initialization never refreshes the hosted artifact. Sign-out, context replacement and expiry invalidate local reads.
Snapshot initialization requires a hosted browser app; Node, routines and local
app contexts receive DataOperationError with context_unavailable.
transact captures a synchronous callback describing create, update, link,
unlink and delete operations across selected collections. Record methods chain;
create followed by edits folds into creation on the host. Generated schema
declarations refine fields and values, while the normalized runtime schemas and
host remain authoritative. Each call makes one authenticated hosted request;
transact changes only shared unpublished work. Publish and discard require exact
current owned Change IDs and versions. Publication uses the canonical writer and
applies its confirmed effects to local Records and the same Changes before
resolving, without waiting for hosted projection settlement. No operation
refreshes the artifact or adds an unpublished query overlay.
subscribe(listener) has no initial callback and returns a synchronous,
idempotent unsubscribe function. Listeners on one handle share a connection;
removing the last listener releases it. Each callback receives exactly
{ records: boolean, changes: boolean }, with at least one true, after atomic
local persistence. Callback-triggered ordinary reads see that committed state;
already-started reads remain pinned to their earlier installation. Unpublished
transact/discard affect Changes only; publication affects both namespaces. A
new artifact affects Records and also Changes when its relevant set changes.
Own successful operations notify once, including when their remote hint races
the response. Repeated hints and unchanged state do not notify.
The SDK reconciles on connection, reconnection and update hints, with bounded reconnect backoff and no healthy-connection polling. Changes-only updates reuse the unchanged artifact bytes. Subscription never builds or refreshes a snapshot. Sign-out, context replacement and expiry stop live delivery and invalidate the handle under the same local-operation rules.
After a lost transact/discard response, DataOperationError.effect is
uncertain, retryable is false, and that handle is invalid. Explicitly
initialize again to inspect authoritative work; do not replay the transaction.
A lost publication response may be retried on the same handle with the same
selection and summary; the SDK retains its canonical operation identity. If the
host accepted work but local persistence fails, the error reports applied and
invalidates the handle. Initialize again to recover coherent local state.
First-party Auth uses the handle without initializing workspace context. Its
bounded browserAuth.authorization.get(id), approve(id) and deny(id) actions
support only the OS CLI request and fixed callback. Inspection/restoration never
approves a fresh request. Results are validated consent labels, safe status or a
completed returnUrl; Auth owns navigation. Decisions require prior inspection
and remain bound to the current request and account. The reserved auth origin
cannot obtain workspace capabilities even if a caller invokes os.init there.
Existing Inbox/customer apps and local app hosting are separate migrations.
The same os.init({ auth }) call also consumes authenticated bootstrap state on
an OS-launched IPv4 loopback origin. It checks /_os/app/context on that exact
origin for every protected operation, without reading hosted access credentials.
It accepts no workspace, endpoint or token option. Missing, forged, expired or
changed local context fails closed, as do sign-out and initialization replacement.
Local Data selects from the Host's exact published index; a warm selection
neither initiates nor waits for renewal or synchronous hosted authority checks.
Local Files returns unsupported_context after checking context. Neither path
falls back to hosted services. Local app routines use the same local execution
and live capability calls as the CLI.
Apps can assess a declared routine without starting it:
import { app } from '@oddington/sdk';
const routines = await app.listRoutines(); // Declaration order, including unavailable routines.
const description = await app.routines.receipts.describe();
const run = await app.routines.receipts.run(); // Uses the definition's default input.
for await (const state of run.watch()) console.log(state.status, state.steps);
const detail = await run.step('read'); // Inferred summary and data for this new Run.
// After refresh, under newly checked authority:
const recent = await app.routines.receipts.runs.list();
const saved = await app.routines.receipts.runs.get(recent[0].id);
console.log(await saved.current());The fixed build generates types from each routine definition; no app-authored
contract map is needed. Descriptions combine safe structure with live readiness
and connection availability. Starts validate the current definition and input;
retained local reads check the established session and saved Run ownership. No installation is needed. Bindings accept no endpoint, credential,
command, path or configuration selector. A local app uses its live Local Host;
a hosted app uses its existing member session and workspace context to reach
the current remote Run owner through the same bindings. Operations reject with
AppRoutineError.reason: authority_unavailable, execution_unavailable, or
invalid_input. Starts are not automatically retried. A
disconnected watch ends and requires current(). History returns at most 20
summaries without deleting older Runs. Input, result and each step summary are
bounded to 256 KiB; each step's summary plus on-demand data is bounded to 1 MiB.
Transport envelopes allow 17 MiB per summary, 1 MiB plus 4 KiB per detail and
20 summaries per list. Local child reports allow 1 MiB plus 64 KiB of framing;
non-output report evidence retains its 10 MiB bound independently of step output. Raw process
output and private authority stay private.
The exact-origin browser credential survives document disposal, reload and tab
closure in local storage. Disposal rejects the outgoing document's pending
operations and disconnects its Run handles without cancelling the host-owned
child. Fresh initialization at the same live Local Host origin can call the
declared operation's runs.get(id) to read retained facts without launching it
again. Old handles remain invalid. Terminal facts and bounded step detail live
in the checkout's existing Run store; interrupted execution is not recovered.
Each active initialization proactively renews before expiry, normally
sixty seconds early with a shorter lead for shorter lifetimes. Only the exact
Local Host silently refreshes the secure CLI login and confirms unchanged
complete access. Successful renewal rotates only the bounded browser credential;
it exposes no CLI credential or login prompt. Other tabs and SDK instances
retain only their unchanged deadline during a temporary renewal outage and
retry proactively without starting or delaying Data reads. They
confirm the stored winner against the same Host and logical session before
silently adopting it. Rotation preserves admitted Data/results and owned runs;
old credentials cannot admit new requests and failed writes, launches and
cancellations are never replayed. Disposal, sign-out, replacement and expiry
cancel renewal, including pending response bodies. Unlike disposal, sign-out,
expiry, confirmed revocation and host stop end local access. Host expiry and
current membership remain authoritative; stored state cannot revive a stopped host. Recovery
covers only the operation's current logical session and bounded terminal grace,
without relaunching or durable history.
Supplied File inputs currently reject execution_unavailable before launch;
File-use transfer and a local browser Files adapter are separate work.
Browser bundlers select the standard browser condition at the package root.
Named imports retain the selected browser capabilities without Node built-ins,
compatibility shims or unused routine, filesystem, Inbox and connection code.
Use the same @oddington/sdk import and declarations in both environments;
there is no browser subpath or required alias, polyfill or consumer build define.
Node imports retain the complete supported routine and managed-handler runtime
on Node.js 22.12.0 or later. Browser operations requiring a Node working root
remain unsupported.
Verify the installed tarball with bun run --filter @oddington/sdk verify:package
from the repository. This checks Node runtime exports and declarations, all public
text for leaks, and the named browser imports in a realm without Node globals or
a module loader. It rejects bundler warnings, external dependencies, Node/shim
code, unused implementations and increases beyond the reviewed SDK byte ceiling.
During diagnosis, list the independently runnable phases with
bun --no-env-file run packages/sdk/scripts/verify-package.ts --list-phases
and rerun only the failed artifact or browser phase with --phase <name>.
The newer Local Host app imports are bundled and exercised separately under the
same strict compatibility audit, so they cannot weaken or inflate the hosted
Data/Files size comparison.
Auth uses a separately pinned installed fixture and 150,000 SDK-byte ceiling.
The private @supabase/auth-js 2.116.0 build dependency is bundled into the
tarball, so installation needs no runtime provider dependency. Browser handle
creation loads its internal provider chunk lazily; unused auth/provider code
disappears from the Data/Files and Local Host fixture outputs. The audit counts
both shipped SDK inputs and every emitted helper, rejects unexpected inputs and
imports, and retains the full credential/server/customer leak scan. Node retains
the complete package-root exports; provider classes are never public.
Current auth measurement is 186,220 minified bytes, including 8,189 fixture bytes and 178,031 SDK bytes, with 50,478 total gzip bytes. Current Data/Files fixture measures 58,294 total / 7,204 fixture / 51,090 SDK bytes, with 18,751 total gzip bytes. The shipped runtime also carries the local Questions transport, transparent Local Host renewal, and bounded Data validation details and guidance. Both fit their explicitly reviewed ceilings: 64,000 SDK bytes for the pinned Data/Files fixture and 256,000 SDK bytes for the separately pinned auth fixture. The combined runtime includes validation guidance and local Data recovery without another dependency. These allowances leave room for ordinary feature growth while catching substantial size regressions. Dependency, compatibility and private-code audits remain separate checks. Those ceilings are fixed review decisions, not values regenerated from later measurements. The installed auth realm/host-handoff proof passes alongside the Node exports, declarations, tarball leak scan and browser compatibility audits.
The browser budget uses pinned esbuild 0.28.1, browser platform, bundled ESM,
es2022, minification, default tree shaking and inline legal comments, with no
source map, aliases, polyfills, external modules or consumer defines. Its fixed
named-import fixture exercises hosted Data selection/search and Files operations.
SDK bytes include all emitted JavaScript except the bundler-attributed fixture
bytes; generated helpers count toward the SDK. Gzip is diagnostic only.
| Same named-import fixture | Legacy diagnostic | Current strict proof | | --- | ---: | ---: | | Total minified JavaScript bytes | 488,539 | 58,294 | | Fixture-attributed bytes | 5,954 | 7,204 | | SDK bytes | 482,585 | 51,090 | | Total gzip bytes | 198,612 | 18,751 |
The legacy diagnostic externalized three unresolved Node built-ins and failed
browser compatibility. The current strict proof has no imports, shims or warnings
and reduces SDK bytes by 89.4%. Its literal ceiling is 64,000 SDK bytes.
The measurement includes
the Local Host bootstrap, local Questions transport, transparent renewal and
opaque browser-auth handoff that os.init must select at runtime, and bounded
Data validation errors. The verifier pins the fixture
hash and bundler version; changing either requires a reviewed measurement
comparison. Future ceiling increases require explicit review of added browser
capability and must never be automatically regenerated from an oversized output.
Create and recover a private immutable original through the same host context:
import { files } from '@oddington/sdk';
const report = await files.create({
file: selectedFile, // Web File, path-backed Blob, Uint8Array or byte stream
name: 'September statement.csv',
path: 'HSC/2026/09',
}, { requestKey: 'statement/2026/09' });
const descriptor = await files.get(report.id);
const response = await files.read(descriptor.id);
await response.text(); // Also supports blob(), arrayBuffer() and body streaming.
const reading = await files.enrich({ file: report.id, name: 'read.md', content: readingMarkdown });
await files.enrich({ file: report.id, name: 'extracted.json', content: JSON.stringify(extractedValue) });
const current = await files.get(report.id); // Discover retained enrichment descriptors.
const context = await files.read(current.id, ['read.md', 'extracted.json']);
await context['read.md'].text();
await context['extracted.json'].json(); // Each body is independently consumed.
await files.enrich({ file: report.id, name: 'read.md', content: revisedMarkdown, replace: reading.sha256 });
await files.upload(report.id); // Move a local File and every retained enrichment to remote storage.Hosted Files retains remote originals and caller-produced named enrichments.
A managed local CLI parent stores local Files beneath the signed-in active
workspace's ~/.os/<workspace>/files root; callers cannot configure another
root. Local Runs default new Files to local storage, hosted Runs to remote.
An existing local or remote File keeps its placement. Every completed File has
one workspace registry row, including local Files whose bytes stay on this
device. Request-key replay and reusable enrichments preserve the same identity
across member calls, Runs and restarts.
Explicit remote creation, upload and enrichment complete during their calls.
upload(id) verifies the original and every enrichment before changing the same
File's placement; retrying a completed upload returns that File. Later reads
and enrichments follow its pinned remote store. A Source reference leaves a
local File local. Inbox publication uploads that same File before exposing the
Ask or Digest. Completed effects survive later Run failure. A runtime without
the local bytes reports local_only (409).
Unnamed inputs require a safe name; streams also require declared size.
Blob and byte-array declarations must match their intrinsic size. Originals are
bounded to 5 GiB per original and 16 MiB per enrichment. Large originals stream
through bounded multipart transfers; local writes and reads use bounded disk
buffers. Operations share four concurrent slots and idle/request deadlines;
later calls wait in invocation order without reading caller content, and
transport retries preserve the same bytes and request key.
Omitting a key generates one for that invocation. Supply a stable key to recover
across processes or fresh routine runs. Caller inputs remain owned by the caller.
Each request resolves current authority; hosted routine Files calls use the
selected workspace's HTTPS OS host, and managed calls use the authenticated
private parent transport. FileOperationError exposes a bounded code,
status and retryability. Input validation can also include a safe detail reason
and numeric byte counts; its message explains the rejected field or expected
versus received size. Streaming rejection marks partial received counts with
receivedAtLeast: true. These facts survive local Run summaries and saved
failure evidence without raw names, keys, file content or provider text. Older
errors without detail remain valid. Messages use the known reason in plain
language; use code, status, retryable and detail for programmatic decisions
rather than parsing the message. Replacement errors explain that replace
expects the current enrichment's SHA-256 value. Reads return the successful Web
Response without consuming its body. Local transfer is bounded and buffered
before constructing that Response. Current workspace members can use every File in that workspace. A
routine's files: "read" permits get/read; files: "read-write" also permits
create/enrich/upload. References in Sources, Runs and Inbox items grant no
additional access. Files has no public delete operation.
enrich accepts a string encoded as UTF-8 or exact Uint8Array bytes, a safe
flat filename, and an optional MIME hint. OS verifies text/Markdown/JSON or
reduces the content to generic binary metadata; it performs no extraction or
model work. The first enrichment atomically promotes the storage bundle while
preserving original bytes, File identity, name, path and access. An identical
retry returns the retained descriptor. Different content conflicts unless
replace matches the current enrichment hash; repeating a completed replacement
still succeeds with its preceding hash. There is no enrichment request key.
read(id) returns the original; read(id, name) returns one enrichment.
read(id, names) validates at most 16 distinct safe names, deduplicates repeats,
and returns an object of unconsumed Responses after all reads succeed. An empty
list returns an empty object in a valid runtime context. Missing authorized names
return invalid_input; a failed keyed request cancels obtained bodies. Each name
is read independently with fresh authority; there is no cross-name revision snapshot.
Names cannot contain separators or use the reserved original basename with
or without an extension. Enrichments share their parent File's access and retention.
Compose retained Files and ordinary Data changes with the pure source helpers:
import { data, files, source } from '@oddington/sdk';
const file = await files.create({ file: statementCsv, name: 'October statement.csv',
path: 'Statements', storage: 'remote' });
const statement = source.file({ $ref: '_sources/statement_october', file,
locations: [source.location.os(file)] });
const changes = data.repository('general').changes();
changes.create({ ...statement, added: '2026-10-20' });
changes.create({ $ref: 'transactions/one',
sources: [source.csv.row(statement.$ref, parsedTransaction.recordPosition)] });
await changes.apply();The caller's CSV parser supplies recordPosition from the retained statement:
positions count CSV records from 1, including the header when present. Expanded
Source occurrences expose sourceRecord.$fragment (for example row=41),
while their $ref remains the base Record ref. Exact Source reads and unexpanded
links retain their ordinary Data shapes.
source.location.gmail({ connection, message, attachment, url? }) and
source.location.dropbox({ connection, file, revision?, path?, url? }) build flat
Locations from successful provider responses. Use
source.locations.merge(existingLocations, incomingLocations) in an ordinary
Data field update. Known objects match by OS File ID, Gmail connection/message/
attachment, or Dropbox connection/File ID; newer Locations replace old path,
revision and URL facts. Distinct objects and custom provider values remain.
Helpers need no initialization and perform no I/O, choose no ref, and add no
changes. File retention and Data validation/application remain explicit.
File Sources use the OS-defined fields filename, media_type, size_bytes,
and sha256. Keep the customer repository's Source schema aligned with this
definition when updating it. mime_type is not an alias for media_type;
do not rename a field produced by source.file() to fit an incompatible schema.
Microsoft helpers are also exported at the package root, with the corresponding
OutlookSourceLocation and SharePointSourceLocation types:
const messageLocation = source.location.outlook({
connection: 'outlook/invoices', message: message.id, url: message.webLink,
// attachment: attachment.id, // Include for attachment bytes; omit for message MIME.
});
const itemLocation = source.location.sharepoint({
connection: 'sharepoint/operations', site: siteId, drive: driveId, item: item.id,
version: version.id, path: lastKnownPath, url: item.webUrl,
});Construct these only after successful provider reads establish the coordinates
for the retained bytes, and save them alongside source.location.os(file) using
an ordinary Data apply. Request Prefer: IdType="ImmutableId" consistently
when obtaining Outlook message and attachment IDs. These IDs are case-sensitive
and survive folder moves within the same mailbox, but not archive-mailbox moves
or export/reimport. Subjects, filenames and internetMessageId are not substitutes.
Omit attachment for an exact message MIME File; supply it for an attachment File.
For SharePoint, use the saved Graph site ID, the document library's drive ID and
the file's driveItem ID. Optional version is an actual driveItemVersion ID
established for those bytes, never an eTag or a guessed version. Omit it when no
such version is established. Names, paths and URLs do not establish identity.
Optional path is last-known context; url is the ordinary HTTPS message web
link or SharePoint webUrl. Never retain temporary download, upload-session or
copy-monitor URLs, authorization or credentials in Locations.
Outlook merge identity is connection/message/attachment, with whole messages distinct from attachments. SharePoint identity is connection/site/drive/item/ version, with absent versions distinct from present ones. Matching observations replace path/link context, including removing omitted context; distinct IDs, versions and copies remain separate. Incomplete coordinates gain no inferred identity. The retained OS File and hash own exact content; external edits do not change an existing Source's bytes. These helpers perform no provider I/O.
Read a named repository through the authenticated host context:
import { data, DataOperationError } from '@oddington/sdk';
const general = data.repository('general');
const collections = await general.collections.list();
const schema = await general.collections.schema('invoices');
const first = await general.records.query({
invoices: { $: { limit: 50 }, supplier: { $: { fields: ['name'] } } },
suppliers: {},
});
const next = first.$meta.collections.invoices.next;
if (next) await general.records.query({
invoices: { $: { limit: 50, after: next }, supplier: { $: { fields: ['name'] } } },
});Binding is inert. Each operation resolves current workspace, endpoint and
short-lived authority from the trusted app host or managed routine context.
Customer calls cannot configure those values. Missing or expired context
rejects; a named handle does not expand repository access. During
os routine run <name>, the same calls read current schema-governed Markdown
in the trusted checkout, including staged, modified and untracked Records. Each
operation reads the files as it begins. Later calls see completed writes and
external edits. Current local Data acquires no hosted binding or index. A dead
local parent fails without hosted fallback.
Each graph selection sends one request and returns only selected canonical values, expanded forward links and exact opaque public versions. For selected current repositories, cursors continue a live keyset position with the same selection; each page reads current. Concurrent movement can repeat or skip a Record. Unselected repositories temporarily retain their existing exact-version cursors. Repeat options and links while omitting completed collections. There is no automatic retry or separate version fetch. Collection schemas omit Markdown storage hints.
For schema-aware authoring, sign in with the OS CLI and run
os data schema general inside the consuming TypeScript Git worktree. It writes
one os-data.generated.d.ts and integrates that shared declaration into the
worktree's TypeScript projects. Review and commit the declaration and changed
project files so a fresh clone needs no credentials or regeneration. Customer
source continues to import only from @oddington/sdk.
The declaration contains no package import: the worktree root needs no package
installation, and each consuming project resolves its own compatible SDK
dependency. Unrelated documentation symlinks are ignored without traversal;
linked or escaping integration paths are refused before authentication or writes
with a path-specific diagnostic. A successful refresh atomically upgrades exact
unedited first-release generated output and preserves every other alias; edited
or malformed generated files remain protected.
Known repository literals then constrain collection/field/link names and query
operands and infer projected query, pages and all values from normalized
schemas. Multiple aliases share the root declaration. Refresh explicitly with
the same CLI command after publishing schema changes; app builds and launches
never refresh it. Missing metadata and dynamic repository strings retain generic
types. Read/search and change payloads remain generic. Committed or stale
types grant no runtime authority and never replace authoritative Data validation.
Iterate matching Records on demand or collect them with an explicit total ceiling:
import type { RecordsQuery } from '@oddington/sdk';
const query = {
invoices: { $: { limit: 50, count: true }, supplier: { $: { fields: ['name'] } } },
suppliers: { $: { fields: ['name'], limit: 50 } },
} satisfies RecordsQuery;
for await (const page of general.records.pages(query)) {
consume(page.invoices, page.suppliers);
}
const matches = await general.records.all(query, { maxRecords: 10_000 });Both helpers preserve literal collection arrays and selected-link inference.
pages captures the query at invocation, fetches only when requested, and never
prefetches after a consumer stops. Independent collection cursors continue live positions; versions and requested
counts may change between pages. Completed collections yield empty arrays on later pages and retain
requested counts. Empty matches yield one versioned page. Helpers own continuation;
use query for a manual after cursor.
all sends one bounded collection operation. Selected hosted execution uses one
ordinary database transaction, and local execution reads one captured in-memory
view. Records, requested counts and version describe that single observation;
the operation retains no snapshot afterward.
Its required positive safe-integer maxRecords caps top-level Record occurrences
across all collections; expanded links do not count toward that ceiling. Overflow
names the queried collections and ceiling, tells the author to narrow the query
or increase maxRecords, and rejects with nonretryable capacity without a
partial or truncated result.
Invalid helper arguments use invalid_query; malformed pages, cursor cycles,
or missing/invalid requested counts use nonretryable unavailable.
Query and authority failures propagate unchanged, with no retry or restart.
Read known stable refs directly, including refs from different collections:
const invoice = await general.records.read('invoices/one'); // complete Record or null
const invoiceWithBody = await general.records.read('invoices/one', { body: true });
const batch = await general.records.read(['invoices/one', 'suppliers/acme']);
// batch.$meta.version identifies one repository snapshot.
// batch.records has one Record-or-null slot per input, in the same order.An array accepts 1–100 distinct valid refs; a one-element array keeps the batch
shape, and all-missing batches still carry their version. Both forms send one
bounded operation through the same app/routine authority and snapshot as query.
Records include $ref, $version and all canonical structured fields; links
remain refs, and omitted and null fields stay distinct. Markdown bodies and
$filing are excluded by default. An exact read may request { body: true } to
include the Markdown body as $body; query, search and pagination remain
body-free. Invalid refs, exceeded bounds, denied access and service failures
reject the entire call; only a missing Record returns null. No projection,
expansion or version argument is accepted.
Use bounded plain-text lookup to find full Records and guards:
const found = await general.records.search('acme overdue', {
collections: ['invoices'], limit: 20,
});
const searchedVersion = found.$meta.version;
for (const { collection, record } of found.matches) {
console.log(collection, record.$ref, record.$version);
}Search sends one request. The trimmed query has 1–256 Unicode code points;
case-insensitive whitespace-separated terms match stable refs and structured
scalar values, including link refs and array values. Every term must match;
field names and Markdown bodies are excluded. Omitted options search all
accessible collections with a limit of 20. Supplied collections contain 1–16
distinct accessible names; the integer limit is 1–100. Invalid queries or
unsupported options fail with invalid_query. There is no cursor, count,
projection, highlighting, embedding or historical version option.
Matches have deterministic order and complete canonical Records. Empty matches succeed with the exact searched version. Members search latest published data; hosted routines search their admitted hosted version. Local routines search the schema-governed Markdown currently in the checkout, including staged, modified and untracked Records. Each call refreshes its operation-local view; a later call sees completed Changes writes and external edits. Search Records supply captured versions for subsequent Changes.
where combines field conditions with AND. Alongside exact scalar/null equality
and $in, declared one-valued integer, number, date and datetime fields accept
$gt, $gte, $lt and $lte:
await general.records.query({ invoices: { $: { where: {
amount: { $gte: 10, $lte: 20 },
date: { $gte: '2026-10-01', $lt: '2026-11-01' },
month: { $gte: '2026-10', $lte: '2026-10' },
}, count: true } } });Supply one lower bound, one upper bound, or one of each. Bounds cannot combine
$gt with $gte, $lt with $lte, or any bound with $in. Operands must be
valid for the declared field, including numeric precision and real day/month
dates; values are never coerced. Missing and null values never match bounds.
A valid interval without matches returns an empty array and requested zero
count. V0 types check expression structure; runtime schema validation owns field
eligibility. Repeat the same bounds when continuing a cursor.
A kind: datetime field is an instant represented as a canonical UTC string
with milliseconds, for example 2026-09-15T14:30:00.000Z. OS validates the real
date/time and exact YYYY-MM-DDTHH:mm:ss.sssZ spelling. Invalid dates, offsets,
missing or excess fractional digits and Date objects reject; values are never
silently converted or rounded. To convert an instant explicitly, use
Date.toISOString(). Returned values remain strings and equality/ranges/order
all use the same canonical representation. Existing string fields need an
explicit schema change to acquire datetime semantics.
The app owns business timezone boundaries. For September 2026 in Europe/London, pass the UTC start (inclusive) and next-month start (exclusive):
await general.records.query({ emails: { $: { where: {
received_at: { $gte: '2026-08-31T23:00:00.000Z', $lt: '2026-09-30T23:00:00.000Z' },
}, order: { received_at: 'desc' } } } });This filters metadata in the query engine before returning selected Records;
the app can then batch-read bodies only for those matches. A calendar date
continues to mean a day/month without a timezone.
DataOperationError.code distinguishes context_unavailable, unauthenticated,
access_denied, invalid_query, request_too_large, capacity and unavailable.
For reads, only capacity and unavailable are retryable; an error never includes private
server details. Server boundaries own query bounds and authorization.
Response handlers receive the same data interface as routines. They execute
in a fresh managed runtime and use the pinned deployment's repository access,
even when the originating routine ran locally. Read current hosted Records when
business logic needs them; assigning known fields needs no preliminary read:
async run({ data }) {
const changes = data.repository('general').changes();
changes.update('transactions/one', { bookingstate: 'booked' });
await changes.publish();
return resolveAsk({ outcome: 'Booking recorded' });
}Schema-driven creates, direct field updates and guarded deletions use the same handle in immediate and captured local routines:
const changes = general.changes();
changes.create({
$ref: 'invoices/INV-124', $filing: { year: '2026' },
type: 'invoice', number: 'INV-124', status: 'draft', amount: 1200,
});
changes.update(first.invoices[0].$ref, { amount: 30 });
changes.delete(first.invoices[1]);
const applied = await changes.apply();apply() validates before writing. Call validate() separately only when you
want to check a proposed change without applying it.
Use reserved $body to author a Record's Markdown content independently of its
schema fields, in the same change set:
changes.create({ $ref: 'emails/email-123', type: 'email', subject: 'Invoice',
$body: 'Hello,\n\nPlease find our invoice attached.\n' });
changes.update(existingEmail.$ref, { $body: revisedMarkdown }); // body-only changeOmitted $body on create means empty; omitted on update preserves the existing
body exactly. A string deliberately replaces it, '' clears it, and null is
invalid. Body-only edits preserve front matter, field-only edits preserve the
body. Ordinary updates take a ref and do not require a version. Bodies round-trip
without Markdown interpretation, formatting, trimming, or fence semantics.
Selections and search return structured fields only. Exact reads remain
body-free unless they explicitly request the body:
const email = await general.records.read('emails/email-123', { body: true });
console.log(email?.$body);Local change batches support 32 MiB of serialized JSON; hosted requests retain their separate 256 KiB limit.
The customer routine owns content and authoring policy. For imported emails,
create with the prepared body, omit $body on ordinary reruns, and preserve existing source bodies on ordinary reruns. Existing notes and fenced blocks
remain ordinary authored content. A fresh version alone does not authorize
overwriting an existing nonempty body.
Creation captures a complete Record and stable $ref, without I/O or a caller
path. OS resolves exactly one collection and its schema-declared filing path.
Optional $filing supplies only unresolved named safe components and is never
stored or returned as Record data; missing components reject by name. Updates
capture only the ref string and explicit fields. The caller need not read first.
OS assigns those fields on the current Record, preserving unspecified fields.
Objects and arrays replace the named field as a whole; the last successful
commit wins when writers assign the same field. Deletion captures only the
selected $ref and $version. For hosted Data, links within the deletion set
are allowed; schema-declared links from any committed Record outside it refuse
the whole group, even if a proposed update removes that link. Hosted validation
checks the complete candidate graph without effects, and apply() repeats
authoritative validation before writing every create/update/delete together.
Success returns { version, created: [{ $ref, $version }], updated: [{ $ref, $version }], deleted: [{ $ref, $version }] }
only after its exact index is ready. Later current reads discover that version.
Deleted identities keep their captured versions; deletion creates no new Record
version or tombstone. A Source deletion removes only that guarded Record, retaining
its File, enrichments and existing durable uses.
During a local Run, changes.apply() validates the touched Records, paths,
authored links and deletion versions without auditing unrelated Record
contents, then writes individual complete files atomically. Completed paths
remain after a later filesystem failure. validate() grants no later
permission to write; subsequent operations read the current checkout. There is
no later Run publication, promotion or prepared repository state.
invalid_change and known-unapplied conflict reject the batch.
Unknown, missing, or invalid field failures include the Record ref and
field name in the SDK error's message, with guidance to check the collection
schema and its relative file location when known. Use
repository.collections.schema(name) to inspect that schema.
The error code and effect remain available for programmatic handling; the
message does not include rejected field values.
Known field failures also expose optional error.detail, exported as
DataValidationDetail. It contains record, field, reason, and, when
known, schemaFile and expected. Reasons are unknown_field, missing_field,
invalid_value, and source_schema_mismatch. expected contains the field
type, many, and nullable, with decimalPlaces for a bounded number or
format: 'month' for a month date. Enum/constant values and rejected values
are excluded. Older errors without detail remain valid.
Other Data errors use this same optional detail field for local read/write
recovery or record-limit facts. Check 'reason' in error.detail before reading
field-validation properties.
When handling a failure, preserve the SDK error's message. For example, a routine that catches Data errors and reports them in its own result can keep the explanation alongside the stable code:
import { DataOperationError } from '@oddington/sdk';
try {
await changes.validate();
} catch (error) {
if (!(error instanceof DataOperationError)) throw error;
return { code: error.code, message: error.message,
...(error.detail ? { detail: error.detail } : {}) };
}When the routine cannot continue, rethrow the original error to preserve its
type and guidance. A new generic Error loses the SDK's bounded failure facts.
apply_pending is retryable and may report uncertain or applied;
retry the same change set to reconcile its retained identity. A transport failure
also keeps that identity. Concurrent apply() calls coalesce, confirmed calls
replay the result, and changed intent requires a new change set. Updates are
bounded with creates and deletions to 10,000 total operations, with one operation
per ref. The complete serialized change request is limited to 32 MiB for local
file changes and 256 KiB for hosted writes. Search is outside this
Changes slice. Source Records use ordinary schema rules. Files checks access
when a consumer reads the referenced content.
Bind a declared Airtable connection with connections.airtable('project-management')
from the package root. Import and binding perform no network or credential work.
The client exposes tables.list() and records.list, get, create, update
and delete. Reads allow table names or stable IDs and return all ordered pages;
writes require a stable table ID and one nonempty batch of at most ten records.
Operations cannot select another workspace, base, token or provider URL.
import { connections } from '@oddington/sdk';
const airtable = connections.airtable('project-management');
const changed = await airtable.records.update({ table: 'tblContacts', records: [{
id: 'recRetained', expected: { Email: '[email protected]', Company: null },
set: { Company: ['recCompany'] },
}] });Preserve original reviewed expected values. Non-null values must match exactly;
null requires an omitted field key and cannot be used as an update value. Reads
preserve Airtable's omitted-field shape. Create requires nonempty fields, update
requires nonempty set, and untouched fields remain unchanged. Guarded methods
read before a single batch write; this leaves a concurrency race and provides no
atomic compare-and-swap, rollback or implicit retry.
Success returns a safe Airtable receipt with the connection, configured base,
table, operation and acknowledged IDs. Create/update also return acknowledged
records separately. AirtableOperationError reports unapplied, partial or
uncertain effects and known safe receipts. Stop after partial or uncertain
work; never repeat the whole change blindly. Credentials must never be retained
in files, publication, logs, handler input, errors or receipts.
Managed routine launches supply workspace and scoped capability.
The defaults read OS_ROUTINE_WORKSPACE and OS_ROUTINE_RUN. Hosts may supply
workspace, capability and request options. Runtime context
provides authority; callers cannot select repository remotes or expand grants.
os routine run <name> [--input <file|->] executes the conventional inert
definition with declared JSON input. Data writes and hosted capability calls
complete live; successful earlier work remains after a later failure. The Run
retains exact local paths, safe hosted effects and Git provenance. --json
returns typed Run state, result/failure and retained local activity.
The former prepare, run --local, command/argv and authority-selection
flags are rejected. os routine deploy <canonical-id> explicitly delivers the
complete definition, including its response handlers; --yes provides automation
consent. os routine run <canonical-id> --remote runs the current successful
deployment with the same input/defaults, ordered steps and result validation.
Remote handles expose current(), watch() and step(id); runs.list() and
runs.get(id) read retained bounded history. Remote summaries omit local activity.
App-started local routines use the same current-checkout and li
