Status: Stable.
Normative home:
fs,kvStorage,tableStorage,sql,nosql,vectorStore,searchIndex,blobStorage,cache.
Why this exists
A node pack keeps files, records and indexes in storage services the host provides, reached through ctx (host-services.md). This document states what a host takes on by advertising one.
Operations
A host advertising a family MUST expose each of its operations to pack code, returning at least the fields shown; it MAY return more. An operation marked † is required only when the named facet is true.
| Family | Operations |
|---|---|
fs (ctx.fs) | read(path) → bytes, contentType? · write(path, bytes, contentType?) → path, sizeBytes · delete(path) → deleted · stat(path) → sizeBytes, modifiedAt, contentType? · list(prefix?, cursor?, limit?) → entries[path, sizeBytes], nextCursor? |
kvStorage (ctx.storage.kv) | get(key) → value?, expiresAt? · put(key, value, ttlSeconds?) → ok · delete(key) → deleted · list(prefix?, cursor?, limit?) → entries[key], nextCursor? · † atomicIncrement(key, delta?) → value · † compareAndSwap(key, expectedValue, newValue) → swapped |
tableStorage (ctx.storage.table) | createTable(name, schema) → ok · insert(table, row) → rowId · get(table, rowId) → row? · query(table, filter?, cursor?, limit?) → rows, nextCursor? · update(table, rowId, patch) → ok · delete(table, rowId) → deleted |
sql (ctx.db.sql) | query(datasourceId, sql, params) → rows, rowCount · execute(datasourceId, sql, params) → rowsAffected · † transaction(datasourceId, operations[sql, params]) → committed |
nosql (ctx.db.nosql) | insert(datasourceId, collection, doc) → id · get(…, id) → doc? · query(…, filter, cursor?, limit?) → docs, nextCursor? · update(…, id, patch) → ok · delete(…, id) → deleted |
vectorStore (ctx.db.vector) | upsert(collection, vectors[id, embedding, metadata?]) → upserted · query(collection, embedding, k, filter?) → matches[id, score, metadata?] · delete(collection, ids) → deleted |
searchIndex (ctx.db.search) | index(index, docs[id, fields]) → indexed · query(index, q, k?, filter?) → hits[id, score, fields?] · delete(index, ids) → deleted |
blobStorage (ctx.storage.blob) | put(bucket, key, bytes, contentType?) → url, sizeBytes · get(bucket, key) → bytes, contentType? · delete(bucket, key) → deleted · list(bucket, prefix?, cursor?) → entries[key, sizeBytes], nextCursor? · † presign(bucket, key, expiresInSeconds, method) → url, expiresAt |
cache (ctx.storage.cache) | get(key) → value?, expiresAt? · put(key, value, ttlSeconds) → ok · delete(key) → deleted |
The facets are kvStorage.atomicIncrement and kvStorage.compareAndSwap, sql.transactions, and blobStorage.presignSupported. presign takes method GET or PUT, and … in a nosql row repeats datasourceId, collection.
Shared rules
- Tenant isolation. A read for one tenant MUST NOT return data another tenant wrote, even under an identical key or name:
kvStoragegetandlist,tableStoragegetandquery,vectorStoreandsearchIndexquery,blobStorageandcacheget. - Datasources.
sqlandnosqldatasources are scoped per tenant, and access to another tenant's datasource MUST be refused. - Backend-invariant. The operation shapes of
sqlandnosqlMUST NOT vary with the advertiseddrivers, nor those ofvectorStoreandsearchIndexwith the advertisedbackends. A host MAY backsqlwith any driver it advertises. - Size limits. A key over
kvStorage.maxKeyBytes, and a write overfs.maxFileSizeBytes,kvStorage.maxValueBytes,blobStorage.maxObjectBytesorcache.maxValueBytes, MUST be refused. AtableStorageinsert MUST be refused oncemaxRowsPerTableis reached. - Expiry.
kvStorageandcacheMUST honour an entry's expiry, as a read sees it, with at most one second of drift.maxTtlSecondscapsttlSecondsfor each.
What a family advertises also names its targets: sql.datasources, nosql.datasources, vectorStore.collections, searchIndex.indexes and blobStorage.buckets. tableStorage advertises maxColumnsPerRow, indexable and fullTextSearch as limits and features, with no further rule.
No error code specific to these families is registered; errors.md governs the code a refused call carries.
fs
- Every
pathMUST be normalized and resolved relative tofs.sandboxRoot. - A path that escapes the root, whether absolute, through
..segments or through a symlink, MUST be refused. The host MUST NOT follow such a link partially. - A permission denial MUST fail the call, never succeed silently or fall through.
- A read of a file over
maxFileSizeBytesMAY fail rather than stream. - The
image(with itsformats),pdfandtransport(ftp,sftp,ssh) sub-surfaces are optional, and gate the pack delegates that use them.
kvStorage
- When
atomicIncrementistrue, increments MUST be atomic across concurrent callers. - When
compareAndSwapistrue, a swap MUST be atomic, with no read-modify-write race. A staleexpectedValuereturnsswapped: falsewithout mutation.
tableStorage
- Rows MUST conform to the table's declared schema: an insert or update whose column types diverge from it MUST be refused.
queryMUST support cursor pagination, andnextCursorMUST be opaque and stable across calls.
sql and nosql
sqlMUST be treated as a parametric template: bound values MUST flow throughparams, never through string interpolation. A host SHOULD verify parameter binding before execution, and a pack MUST NOT concatenate user input intosql.- When
sql.transactionsistrue, a partial failure insidetransactionMUST roll back the whole batch. nosqlfilter operators MUST NOT permit injection. Server-side script evaluation, such as MongoDB$where, MUST be refused unless an explicit allowlist is configured.
vectorStore and searchIndex
vectorStore: anupsertfollowed by aquerywith the same embedding MUST return the inserted ids in the topkmatches whenkis at least the number inserted.searchIndex: anindexfollowed by aquerywith a substring of an indexed field MUST return the indexed id withscoreabove 0.
blobStorage
Presigned URLs MUST expire at the advertised TTL. A presigned request after expiry MUST fail at the storage layer, not after an authorization skip.
Sources: RFCs 0014, 0015, 0016, 0018, 0019.