@mitralab.io/sdk-core
v0.2.11
Published
Environment-neutral contracts and modules shared by Mitra JavaScript SDKs
Readme
Mitra SDK Core
Environment-neutral TypeScript contracts and API modules shared by Mitra JavaScript SDKs.
Most application and Server Function code should install a concrete SDK instead:
@mitralab.io/platform-sdkfor browser applications@mitralab.io/functions-sdkfor Mitra Server Functions
The core package exists so concrete SDKs build direct backend requests from one
contract. It exposes the API values returned by each service. It does not expose
MCP envelopes, CallToolResult, or MCP-formatted text.
The versioned contract corpus in contracts/ is the canonical source for the
MCP, JavaScript, and Python capability matrix. Its manifest identifies the
current fixture and pins every packaged version with SHA-256. Core executes all
success operations and response validation. Functions JavaScript inherits those
checks and owns its HTTP adapter cases. Python consumes all case groups because
it does not depend on Core, using a digest-pinned snapshot so tests stay offline.
Boundary
The package contains:
- common types and data transfer objects
- safe path segment encoding
- structural response validation
- authentication and app members
- Code Studio apps, files, builds, deploys, versions, and rollback
- schema, records, custom queries, SQL, imports, and Data Sources
- Functions, versions, publishing, rollback, executions, visibility, and secrets
- Function scheduling composed into single-Function create, patch, get, and list
- business agents and workflows
- integration configs, resources, templates, tests, proxying, and executions
- Copilot tasks, messages, credentials and the last subscription window each one reported, models, and app connections
- an Agent task live-session state machine with bounded queue and
sendAndWait, and the direct channel to the chat's box - Messenger notifications and composed safe app context
- anonymous public Function execution
- a minimal transport interface injected by each concrete SDK
The package does not contain:
fetchor any other HTTP implementation, or a WebSocket implementation- tokens, authorization headers, or environment variables
- login, sign-up, logout, refresh, browser storage, or auth listeners
- retry, redirect, timeout, or client lifecycle policy
Those concerns stay in the concrete SDK because browser sessions and Server Function runtime credentials have different security and failure semantics.
Agent task live sessions
Core owns the state machine and the direct channel to the chat's box. A concrete SDK implements
AgentTaskEventSource, the Copilot stream a chat falls back to, then composes it with the REST
task module:
import {
createAgentTaskSessionManager,
withAgentTaskSessions,
type AgentTaskEventSource,
type SdkCore,
} from "@mitralab.io/sdk-core"
declare const eventSource: AgentTaskEventSource
declare const core: SdkCore
const sessions = createAgentTaskSessionManager({
tasks: core.agentTasks,
eventSource,
directChannel: { apiUrl: "https://api.mitralab.ai" },
})
const agentTasks = withAgentTaskSessions(core.agentTasks, sessions)
const session = agentTasks.session({ taskId: "task-id", transport: "http" })
const result = await session.sendAndWait("Summarize the app", { timeoutMs: 120_000 })The event source must complete open() after its streaming handshake, so Core opens the channel
before posting the prompt. Core forwards the session's auto, websocket, or http transport
preference to open(); the concrete adapter selects or rejects it. HTTP/SSE has no replay cursor.
During an active turn Core performs one
reconnection and reconciles persisted messages; live deltas across that gap are not guaranteed to
be lossless. Abort and timeout stop the local sendAndWait waiter but do not interrupt the remote
turn. Use cancel() when interruption is intended.
Turn usage. turnEnd and the result of sendAndWait carry usage (AgentTurnUsage) when
the turn's stepFinish reported it, on the direct channel and on the Copilot stream alike. The
Agent message that closed a turn carries the same object as usage in listMessages, and
loadHistory keeps it on that agent item. inputTokens and outputTokens are always there;
reasoningTokens, cacheReadTokens, cacheCreationTokens, model, provider, costUsd
(this turn, USD), costUsdRaw (the provider session so far) and costSource only when the
harness reports them (Codex sends no cost and no cache writes). inputTokens is the input
without cache, as in the legacy platform; cache is counted apart in cacheReadTokens and
cacheCreationTokens. Also optional: requestCount and requests (AgentTurnUsageRequest,
one entry per provider call or per model), durationMs (the provider's time, sent by the box
in stepFinish, so it shows in the live turnEnd and in the history), and authMode
(subscription, api_key, included_ai, custom, unknown, or a newer string passed
through as is) and requestMessageId, which only the Copilot's record has: they show in
listMessages, loadHistory and a recovered turn, never in the live turnEnd. A turn or
message without usage has no usage field. A value the SDK cannot read is dropped rather than
failing the page or the turn: counts must be non-negative integers, costs finite numbers, and
a bad requests item leaves the list.
Direct channel
The direct channel is on when the concrete SDK passes directChannel.apiUrl, and it serves a
business agent's chat, the one with an agentId: only those have a box. Any other chat, or any
chat without apiUrl, stays on the event source and REST inputs, with no channel request and
no T3 default. A business agent's chat asks the Copilot once where it is served
(POST /api/v1/tasks/{id}/channel, through agentTasks.channel) and talks to the box that
answers. The box runs the turn; the Copilot hands out the channel, admits every turn and
receives the box log. A new business agent's chat is created with runtime: "T3" so it is born
on its box, unless the session names a runtime; the Copilot refuses T3 for any other chat
(RUNTIME_REQUIRES_AGENT_APP).
session({ taskId }) reads the task before it opens anything, so the same rule holds for an
existing chat: with no agentId it never asks for /channel; with an agentId it asks and the
Copilot decides. A chat the Copilot will not serve on its box (not on T3, answered for example
with RUNTIME_NOT_T3) is a raw channelDeclined with reason: "unavailable" and the Copilot's
message, and the session goes on through the event source and REST inputs, with no error
event for the app.
- Transport.
websocketuses the box socket.httpuses the box's HTTP routes next to the socket path:POST .../api/mitra/chat/messagesto send andGET .../api/mitra/chat/events(SSE, from a sequence) to read, keeping thegrantandticketquery of the channel URL.autouses the socket when a WebSocket implementation is available and HTTP otherwise, which is the case of a Serverless Function. The grant in those URLs lasts 10 minutes and the dev proxy ticket 60 seconds: a 401 or 403 without anerror_codeon the POST or on opening the stream asks the Copilot for the channel again and retries once on the fresh URLs; a second rejection is an error. One with anerror_codeis a refusal and is answered once, as it came. - Host rule. The channel URL carries a grant, so Core only reaches the API gateway host
(
directChannel.apiUrl, overwss:when the API ishttps:) or a fleet box host overwss:(*.e2b.app,*.e2b-<env>.mitralab.ai), on either transport. - Fallback, always visible. When the Copilot answers 202 or an error (a Copilot without
/channel), the body has nowsUrl, the host is outside the rule, awebsocketsession has no WebSocket, the box cannot be reached (a handshake that times out or is refused, a proxy blocking its host, a network error on the HTTP stream), or the box has no HTTP routes (a 404, or a stream that is nottext/event-stream, from a template older than them), the session stays on the event source and REST inputs and first emits a rawchannelDeclinedevent withreasonunavailable,body,host,websocket, orhttp_unsupported. - Runtimes without WebSocket. Core uses
directChannel.WebSocketwhen given, otherwiseglobalThis.WebSocket; Node 18 and 20 have none, so inject one such aswsor letautotake HTTP. The socket type only asks forreadyState, the fouron*handlers,send, andclose. HTTP usesdirectChannel.fetchwhen given, otherwiseglobalThis.fetch. - Admission. The session counts a message as sent only when the box answers for it: the
stepStartframe on the stream, or the 200 of the HTTP POST, both the box starting the turn the Copilot admitted. The session then emitsaccepted, and from there the turn runs and reaches the Copilot's log even if this process goes away. A refusal is reported once througherror: anerrorframe on the stream, or a POST answered 409 (not admitted), 400 (bad frame), 413 (too large), 503 (nobody to admit) or 504 (no admission or turn within 30 s), each with{error_code, message}, which rejectssendAndWaitwithAgentTaskTurnErrorcarrying that code and message. A box silent for 35 s with the wire up fails the send; a redial pauses that clock and a successful one restarts it. Over REST,acceptedfollows the Copilot's 202. A caller that does not wait for the answer, such as a Serverless Function, awaitsacceptedbefore it returns;sendAndWaitwaits for the whole turn. - Drops. A socket or stream lost in the middle of a turn, or while a message waits for
admission, is redialed with backoff (1, 2, 4, 8, 16 s), asking the Copilot for the channel on
each attempt and replaying the box log from the last sequence seen, so an admission that
happened during the drop still arrives. A POST the network lost is not sent again for the same
reason. The raw
channelReconnectingandchannelConnectedevents report the redial. An idle wire that closes, a socket superseded by another open (4409), a redial that gives up, or a Copilot that stops offering the channel is a disconnect for the session. Opening never replays older frames: what an idle chat missed is history. - Interrupts go to the box too; approvals stay on REST.
- Status of the HTTP transport. Not yet proven against a real box. It needs the box HTTP
routes in the t3code-mitra fork (mitralab-dev/t3code-mitra#180, in progress) and, behind the
dev proxy, a gateway route for them: the gateway only routes
.../api/mitra/chat/wstoday. Until both ship, anhttpsession, or anautoone without WebSocket, falls back to the Copilot withchannelDeclined. The WebSocket transport was proven against a dev box.
Installation
npm install @mitralab.io/sdk-coreNode.js 18 or newer is required. The package has no runtime dependencies; the direct channel needs a WebSocket implementation on runtimes without a global one.
Transport contract
SDK adapters provide one transport per service. A transport receives the service-local path and request options, then returns the parsed response payload.
import { createSdkCore, type Transport } from "@mitralab.io/sdk-core"
declare const iam: Transport
declare const dataManager: Transport
declare const functions: Transport
declare const integration: Transport
declare const codeStudio: Transport
declare const copilot: Transport
declare const messenger: Transport
declare const publicFunctions: Transport
let appId: string | undefined
const core = createSdkCore({
transports: {
auth: iam,
dataManager,
functions,
integration,
codeStudio,
copilot,
messenger,
publicFunctions,
},
getAppId: () => appId,
functions: {
executeInvocationType: "sync",
emptyInput: "empty-object",
},
})
const { data: tasks } = await core.entities.getTable("Task").list({ limit: 20 })
const context = await core.context.getAppContext()The transport owns URL resolution, authentication, serialization, error parsing, redirects, retries, and timeouts. The core never reads or stores credentials.
List methods return the producer's summary DTO when it differs from the detail
response. Custom Query summaries omit sql, Workflow summaries omit
definition, and integration resource summaries contain only id, name,
method, and endpoint. App, integration template, and template config lists
likewise expose their producer summary DTOs, while their get methods return
the complete detail DTOs.
Record list and filter methods preserve the Data Manager envelope with data,
limit, skip, total, and hasMore. Spring list endpoints from Code Studio,
Functions, Data Manager, and Copilot use stable pagination metadata under
page. Integration still returns its legacy flat Spring page metadata, so its
list methods expose totalElements at the top level.
The complete DTOs preserve producer field names and nullability. This includes
Code Studio app routing, domains, color, plan, version, and timestamps;
Workflow execution scope, trigger, current step, context, and timestamps; and
Integration template login/request schemas, config metadata, and resource
parameter schemas. apps.build() returns the AppDeploy produced by the build
endpoint. apps.publish() continues to return the updated AppDefinition.
Code Studio deploys use the producer field names deployUrl and
errorMessage, together with appId, appVersionId, logs, durationMs,
startedAt, finishedAt, and createdAt. Integration execution history uses
success rather than a synthetic status and preserves nullable request,
response, error, source, and duration fields.
Integration configs can be executed by identifier with integration.execute()
or by their app-scoped alias with integration.executeByAlias(). Both methods
send the proxy request unchanged apart from the required source: "SDK" audit
field and validate the same proxy result.
An integration config normally points at a catalog template through templateId.
When the provider has no template, integrationAdmin.create(),
integrationAdmin.bulkCreate(), and integrationAdmin.testCredentials() accept
an inline definition instead, in the same shapes the catalog uses:
await integrationAdmin.create({
alias: "erp-inline",
fieldsSchemaInline: [
{ key: "base_url", label: "Base URL", type: "url", required: true },
{ key: "access_key_code", label: "Access Key Code", type: "secret", required: true },
{ key: "access_key_token", label: "Access Key Token", type: "secret", required: true },
],
requestConfigInline: {
headers: {
"X-Access-Key-Code": "{{access_key_code}}",
"X-Access-Key-Token": "{{access_key_token}}",
},
credential_rules: null,
},
loginConfigInline: null,
values: { base_url: "https://api.example.com", access_key_code: "...", access_key_token: "..." },
})Send templateId or the inline definition, never both and never neither. The
Integration service owns that rule and answers 400; Core forwards whatever the
caller sends. Configs created this way report templateId: null and echo the
three inline fields back on every config response, with secrets in config
masked exactly as they are for template-backed configs.
Inline fields are authored as IntegrationFieldSchemaInput, which makes
placeholder and default optional because the producer stores an omitted one
as null. Responses keep the strict IntegrationFieldSchema, where both
properties are always present.
integrationAdmin.list() is the native equivalent for listing configured
integrations. It calls GET /api/v1/template-configs and returns the producer's
paginated TemplateConfigSummary values. With an app-scoped token, the
Integration service filters the page to that app.
auth, dataManager, functions, and integration remain required for
backward compatibility. codeStudio, copilot, and messenger are optional;
calling their modules without the corresponding transport fails with a
configuration error before making a request.
publicFunctions is deliberately separate and never falls back to the
authenticated Functions transport. Its adapter must target the Functions public
base URL and must not attach Authorization or X-App-Id. It calls
POST /public/v1/functions/{id}/execute with X-Invocation-Type: sync or
async. Public async is fire-and-forget because the producer does not expose
anonymous polling. Callers that need a result use public sync execution, or the
authenticated functions.executeAsync and functions.getExecution methods.
App scope and permissions
Core accepts app identifiers but does not inspect tokens or implement service
authorization. A concrete app-scoped adapter must fix appId to its trusted
runtime value. It must not let caller input select another app. This is
especially important for Code Studio because its alpha endpoints do not enforce
an app claim in every path. apps.list() and apps.create() are tenant-wide and
are not available to app-scoped tokens.
context.getAppContext() always uses the trusted current app and deliberately
excludes app members. The Server Function token does not have MEMBER_READ, so
the composed context must not call IAM's member endpoint. members remains a
separate Core module for callers whose token has that permission. Function
secret operations still require their dedicated permissions. Agent tools that
resolve a business agent_id and the two tenant-wide app collection operations
are not applicable to an app-scoped token. Messenger delivery also depends on
a configured channel. These are service authorization constraints, not changes
to the remaining Core contracts.
Custom Query creation accepts optional isVirtualTable and connectionId
fields and forwards them unchanged to the Data Manager. Omitting
isVirtualTable preserves the producer default of false; connectionId only
selects an external connection for a Virtual Table.
Custom query execution targets the Data Manager alpha contract and sends only
parameters. Data Manager resolves the Data Source from the authenticated app,
so the concrete adapter must use the app-scoped JWT and must not accept a caller
selected Data Source for this operation.
MCP capability coverage
contracts/v0.2.0-beta.0/mcp-tool-parity.json
maps all 120 @McpTool methods from 18 alpha tool classes to the typed Core
surface. Multiplexed MCP tools map to separate SDK methods. Composition and alias
tools record equivalence instead of creating duplicate APIs. Git credentials are
excluded because they are an internal Sandbox endpoint, not an MCP capability.
The deprecated Functions bridge remains owned by @mitralab.io/functions-sdk.
The MCP bulkUpdateFunctions tool maps to functionsAdmin.bulkPatch() and
PATCH /api/v1/functions/bulk, preserving omitted fields. The separate
functionsAdmin.bulkUpdate() method remains a full replacement over PUT.
Single-Function create and patch inputs also expose cronExpression,
cronInputJson, and cronEnabled as one composed scheduling unit. On create,
omitting all three creates no schedule; supplying any of them requires a
non-blank cronExpression. The new schedule uses UTC and starts ACTIVE
unless cronEnabled is false. On patch, null or omitted schedule fields
preserve their stored values, an empty cronInputJson object clears the input,
and a blank cronExpression removes the schedule. A non-blank expression can
create a missing schedule in UTC; cronEnabled explicitly pauses or resumes
it. These composed writes require SCHEDULE_WRITE and FUNCTION_EXECUTE in
addition to the Function write permission.
Function detail and list responses return all three fields when the caller has
SCHEDULE_READ. Without it, all three are null without querying Scheduler. The
same all-null shape represents a Function that has no schedule, so these
responses alone cannot distinguish absence from missing read permission. All
Function bulk create, update, and patch inputs prohibit embedded schedule
fields; their dedicated types omit them. Compose scheduling only through the
single-Function create and patch methods.
The MCP FunctionTools.getExecution operation maps to
functionsAdmin.getExecution(functionId, executionId) and its nested Function
execution route. The runtime-only functions.getExecution(executionId) remains
available for callers of the separate global execution endpoint.
Core deliberately exposes no separate schedule facade. The MCP scheduling capability is composed
through the three cron fields on functionsAdmin.create() and patch(), with state returned by
get() and list(). This keeps one public Function contract instead of duplicating the Scheduler
producer lifecycle.
Legacy Git credentials and record operations selected by jdbcConnectionConfigId have no native
Core equivalent. Git credential minting is an internal Sandbox operation authenticated between
services, and the public Data Manager records API resolves the app Data Source from the token
without accepting a connection selector. Core does not synthesize either behavior through a BFF
or raw SQL.
Error mapping
By default, invalid configuration and invalid responses throw SdkCoreConfigurationError and SdkCoreResponseError. A concrete SDK can inject an SdkCoreErrorFactory so these failures keep that SDK's established public error classes.
Development
npm install
npm run checkThe build produces ESM, CommonJS, .d.ts, and .d.cts artifacts. Package smoke tests install the generated tarball into a clean consumer and validate both module systems and TypeScript resolution.
Release order
Core is the producer for the concrete SDK adapters, so it publishes first. For a prerelease
X.Y.Z-beta.N, currently 0.2.0-beta.1:
- Merge the source, the
package.jsonversion, and the matching contract corpus version tomainin the same pull request. The Release workflow bumps nothing. - Run the Release workflow with version
X.Y.Z-beta.N. It checks that the requested version already matchespackage.json, runs the full package check, then tags and publishes the prerelease under npm'sbetadist-tag. - Confirm
npm view @mitralab.io/[email protected] versionreturns the same version. - Regenerate each adapter lockfile from the npm registry and pin this repository commit in the adapter's contract-source manifest before publishing that adapter.
Stable X.Y.Z releases use npm's default latest dist-tag. The workflow accepts only that stable
form or the prerelease form X.Y.Z-beta.N.
Do not publish an adapter against a local tarball or a file: dependency. Tarballs are only for
pre-release validation while the registry artifact does not exist.
