@briefcase-ai/runtime
npm install @briefcase-ai/runtime@4.4.0The package has no root export. Import from one of its four subpaths:
/integrity, /lakefs, /connectors, or /trace.
/integrity
import { canonicalJson, computePayloadHash } from '@briefcase-ai/runtime/integrity';
console.log(computePayloadHash({ approved: true }), canonicalJson({ a: 1 }));Exports include HASH_SPEC_VERSION, GENESIS_PRIOR_HASH, canonicalJson,
sha256Hex, computePayloadHash, computeEntryHash, HashChainAppender,
ChainConflictError, verifyChainSegment, and their input, entry, store, and
verification types.
/lakefs
import { createLakefsClient } from '@briefcase-ai/runtime/lakefs';
const lakefs = createLakefsClient({ endpoint: process.env.LAKEFS_ENDPOINT!, accessKeyId: process.env.LAKEFS_ACCESS_KEY!, secretAccessKey: process.env.LAKEFS_SECRET_KEY!,});await lakefs.health();The client supports repository listing, presigned upload and link, text and stream reads, commits, object history, tags, imports, snapshots, branches, deletes, and paginated listing. Reads retry once for 429, 502, 503, and 504; writes are never retried automatically. Request timeouts clamp to 1 through 60 seconds, defaulting to 8 seconds.
/connectors
Pulls resources from a third-party provider on a cursor, rotating OAuth tokens without letting two workers clobber each other.
import { createAesGcmEnvelope, createOAuthState, runConnectorSync, verifyOAuthState, type ConnectorConnection, type ConnectorResource, type ConnectorStore, type ConnectorStrategy, type ResourceSink,} from '@briefcase-ai/runtime/connectors';
const connection: ConnectorConnection = { id: 'conn-1', tenantId: 'tenant-1', provider: 'billing', accessToken: 'access-1', refreshToken: 'refresh-1', tokenExpiresAt: null, refreshVersion: 1, refreshedAt: null,};
interface Invoice extends ConnectorResource { amountCents: number;}
// Back these seven methods with your own database.const store: ConnectorStore = { beginRun: async () => 'run-1', loadConnection: async () => connection, latestCursor: async () => null, compareAndSwapTokens: async () => true, completeRun: async () => undefined, failRun: async () => undefined, markNeedsReconnect: async () => undefined,};
const strategy: ConnectorStrategy<Invoice> = { provider: 'billing', refreshAccessToken: async () => ({ accessToken: 'access-2', expiresInSeconds: 3600, }), fetchResources: async ({ cursor }) => [ { externalId: cursor ?? 'inv-1', occurredAt: new Date(), amountCents: 1200, }, ],};
const sink: ResourceSink<Invoice> = { write: async (resources) => ({ written: resources.length }),};
const result = await runConnectorSync({ tenantId: 'tenant-1', connectionId: 'conn-1', provider: 'billing', store, strategy, sink,});
console.log(result.runId, result.fetched, result.written);runConnectorSync opens a run, loads the connection, refreshes the access token
when needed, fetches, writes to the sink, and closes the run with the advanced
cursor. A throw anywhere in between calls failRun and re-raises.
A refresh is attempted only when the strategy implements refreshAccessToken and
the connection has a refreshToken. Within that, two windows decide whether it
runs:
| Option | Default | Effect |
|---|---|---|
tokenExpirySkewMs | 300000 | Treat the token as fresh only if it expires more than this far out. A null tokenExpiresAt counts as not fresh. |
refreshFastPathMs | 60000 | Skip the refresh if refreshedAt is less than this old, so a second worker does not repeat one. |
now | () => new Date() | Clock injection for tests. |
Token rotation is compare-and-swap on refreshVersion. The loser of a race
reloads the winner’s tokens rather than overwriting them. A
ConnectorRefreshError with kind "terminal" calls markNeedsReconnect and
re-raises, unless another writer already rotated the version, in which case the
sync continues with the new tokens. Kind "transient" always propagates.
The cursor advances to the newest occurredAt at or before the run’s start time,
so a resource stamped in the future cannot skip the ones behind it. A fetch that
returns nothing keeps the prior cursor, or stamps the run’s start time on a first
run that had none.
ConnectorStore, ConnectorStrategy, ResourceSink, ConnectorConnection,
ConnectorResource, RefreshedTokens, and RefreshErrorKind are the types you
implement against.
OAuth state and token storage
const { state, cookieValue, codeChallenge } = createOAuthState({ tenantId: 'tenant-1',});const verified = verifyOAuthState({ state, cookieValue });if (verified.ok) verified.codeVerifier.toUpperCase();
const envelope = createAesGcmEnvelope({ keyProvider: () => new Uint8Array(32),});const sealed = await envelope.seal('refresh-1', 'tenant-1:conn-1');await envelope.open(sealed, 'tenant-1:conn-1');createOAuthState returns the redirect state, the cookie value to set, and a
PKCE verifier with its S256 codeChallenge. verifyOAuthState compares the
nonce with timingSafeEqual, enforces a 10-minute default TTL, and returns
{ ok: false, reason } for missing_cookie, bad_nonce, expired, or
malformed instead of throwing.
createAesGcmEnvelope seals to version.iv.tag.ciphertext in base64url, where
version is the version option (default v2). The key provider must return
exactly 32 bytes, open rejects an envelope stamped with any other version, and
the AAD passed to open must match the one passed to seal.
/trace
Records an agent invocation and its steps as hash-verifiable rows, without putting a database write on the agent’s critical path.
import { InMemoryTraceStore, createTraceRecorder, verifyInvocationRecord,} from '@briefcase-ai/runtime/trace';
const traceStore = new InMemoryTraceStore();const recorder = createTraceRecorder({ store: traceStore });
const invocationId = recorder.invocation.start({ tenantId: 'tenant-1', actorType: 'agent', actorId: 'triage-1', intent: 'classify_ticket', context: { ticket: 'T-1' },});
const stepId = recorder.step.start({ invocationId, stepType: 'model_call', name: 'classify', input: { prompt: 'Escalate?' },});
recorder.step.finish({ stepId, output: { label: 'billing' } });recorder.invocation.finish({ invocationId, outcome: 'success' });
await recorder.close();start and finish return synchronously and queue the write. Writes for one
invocation stay ordered relative to each other, so a step never lands before the
invocation that owns it. flush() awaits the queue; close() stops the sweep
timer, flushes, and makes further calls throw.
Every row carries an integrityHash over its start fields and a finalizeHash
over the outcome, both computed with computePayloadHash from /integrity.
verifyInvocationRecord and verifyStepRecord recompute them and return a
boolean, treating a still-open row as valid.
| Option | Default | Effect |
|---|---|---|
maxAttempts | 3 | Attempts per write before it is dropped |
isTransient | deadlock, serialization, ECONNRESET, ETIMEDOUT | Which errors retry |
staleAfterMs | 3600000 | Age at which sweep() force-closes a row |
sweepIntervalMs | unset | Enables a background unref’d sweep timer |
onEvent | no-op | Receives TraceEvent notifications |
A write that exhausts its retries increments dropCounts() and emits
write_dropped rather than throwing into the agent. sweep() force-closes rows
older than staleAfterMs so a crashed run does not leave an open invocation
forever: an invocation is finished with outcome "dropped", a step with error
{ reason: "dropped" }.
InMemoryTraceStore implements TraceStore for tests and takes a beforeWrite
hook for injecting failures.
Limits
- There is no root export.
import '@briefcase-ai/runtime'fails; import a subpath. /integrityships hashing and chains only. Ed25519 signing and bundled stores are Python-side./traceprefers the agent over the record: a finish for an unknown id is reported throughonEventas an orphan and discarded, and an exhausted write is counted rather than raised. WatchdropCounts()if the trace is evidence./connectorscoordinates token rotation through your store’s compare-and-swap. A store that ignoresexpectedRefreshVersiongives up the race protection.
See Integrity and Signing and lakeFS for the task-level walkthroughs.