Skip to content

@briefcase-ai/runtime

Terminal window
npm install @briefcase-ai/runtime@4.4.0

The 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:

OptionDefaultEffect
tokenExpirySkewMs300000Treat the token as fresh only if it expires more than this far out. A null tokenExpiresAt counts as not fresh.
refreshFastPathMs60000Skip 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.

OptionDefaultEffect
maxAttempts3Attempts per write before it is dropped
isTransientdeadlock, serialization, ECONNRESET, ETIMEDOUTWhich errors retry
staleAfterMs3600000Age at which sweep() force-closes a row
sweepIntervalMsunsetEnables a background unref’d sweep timer
onEventno-opReceives 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.
  • /integrity ships hashing and chains only. Ed25519 signing and bundled stores are Python-side.
  • /trace prefers the agent over the record: a finish for an unknown id is reported through onEvent as an orphan and discarded, and an exhausted write is counted rather than raised. Watch dropCounts() if the trace is evidence.
  • /connectors coordinates token rotation through your store’s compare-and-swap. A store that ignores expectedRefreshVersion gives up the race protection.

See Integrity and Signing and lakeFS for the task-level walkthroughs.