@rayrun/sdk
v0.13.1
Published
Type-safe client for the Rayrun public API
Readme
@rayrun/sdk
npm install @rayrun/sdkimport { Rayrun } from '@rayrun/sdk';
const rayrun = new Rayrun({ apiKey: process.env.RAYRUN_API_KEY });
const identity = await rayrun.identity.get();
const { items: connections } = await rayrun.connections.list();
const { items: matchingTools } = await rayrun.tools.list({ query: 'create issue' });Create keys in Dashboard → Settings → API keys. The plaintext is shown once.
identity.get() returns the active credential name and UID, authentication method, user,
workspace, and granted scopes. It requires authentication but no resource-specific read scope.
The client covers catalog search, connection and credential setup, source deployments, OAuth links, indexing, tool and client policies, access profiles, Workspace Skills, Activity, review queues, and webhooks. Safe reads retry transient failures; writes do not retry automatically.
Deploy source projects
Use the CLI to read a directory safely, or send the same base64 file representation through the
SDK. The example reads a reviewed CI artifact containing the complete project as an array of
{ path, contentBase64 } objects. Validation is read-only; update stores an encrypted immutable
revision and queues an isolated build. Commit one stable projectId UUID in rayrun.json so every
checkout and retry converges on the same deployment.
import { readFile } from 'node:fs/promises';
import { setTimeout as delay } from 'node:timers/promises';
import { Rayrun } from '@rayrun/sdk';
const files = JSON.parse(await readFile('./source-files.json', 'utf8'));
const apiKey = process.env.RAYRUN_API_KEY;
const manifest = JSON.parse(await readFile('./rayrun.json', 'utf8'));
const sourceProjectId = manifest.projectId;
const workspaceSecretUid = process.env.RAYRUN_GITHUB_SECRET_UID;
if (!apiKey || !sourceProjectId || !workspaceSecretUid) {
throw new Error('Deployment credentials and source project ID are required.');
}
if (!/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(sourceProjectId)) {
throw new Error('rayrun.json projectId must be a UUID.');
}
const rayrun = new Rayrun({ apiKey });
const validation = await rayrun.deployments.validate(files);
const queued = await rayrun.deployments.deploySourceProject(sourceProjectId, {
displayName: validation.name,
files,
secretBindings: { GITHUB_TOKEN: workspaceSecretUid },
});
let deployment = await rayrun.deployments.get(queued.buildUid);
const deploymentTimeoutMs = Number(process.env.RAYRUN_DEPLOY_TIMEOUT_MS ?? 60 * 60_000);
if (!Number.isSafeInteger(deploymentTimeoutMs) || deploymentTimeoutMs <= 0) {
throw new Error('Invalid deployment timeout.');
}
const deploymentDeadline = Date.now() + deploymentTimeoutMs;
while (
['queued', 'running'].includes(deployment.build.status) ||
(deployment.build.status === 'succeeded' &&
(!deployment.release || ['pending', 'verifying'].includes(deployment.release.status)))
) {
if (Date.now() >= deploymentDeadline) throw new Error('Deployment timed out.');
await delay(2_000);
deployment = await rayrun.deployments.get(queued.buildUid);
}
if (deployment.build.status !== 'succeeded' || deployment.release?.status !== 'active') {
throw new Error(
deployment.release?.errorMessage ?? deployment.build.errorMessage ?? 'Deployment failed.',
);
}
const log = await rayrun.deployments.getLog(queued.buildUid);
const releases = await rayrun.deployments.listReleases(queued.connectionId);
const rollbackTarget = releases.items.find(({ status }) => status === 'superseded');
if (!rollbackTarget) throw new Error('No inactive release is available to roll back.');
const rollback = await rayrun.deployments.rollback(queued.connectionId, rollbackTarget.uid);
let rollbackRelease = await rayrun.deployments.getRelease(queued.connectionId, rollback.releaseUid);
const rollbackDeadline = Date.now() + deploymentTimeoutMs;
while (['pending', 'verifying'].includes(rollbackRelease.status)) {
if (Date.now() >= rollbackDeadline) throw new Error('Rollback timed out.');
await delay(2_000);
rollbackRelease = await rayrun.deployments.getRelease(queued.connectionId, rollback.releaseUid);
}
if (rollbackRelease.status !== 'active') {
throw new Error(rollbackRelease.errorMessage ?? `Rollback ${rollbackRelease.status}.`);
}Recover a clean checkout without deploying by resolving the committed project ID. Adopt only an existing legacy upload that has no project identity; project and connection associations cannot be reassigned.
const project = await rayrun.deployments.getSourceProject(sourceProjectId);
console.log(project.connection.uid, project.source.revision, project.activeBuild?.uid);
await rayrun.deployments.adoptSourceProject(sourceProjectId, legacyConnectionId);Use the Dashboard's Deploy services API-key preset. It grants deployments:read for status and
release history, deployments:logs:read for encrypted build logs, and deployments:write for
validation, upload, and rollback. Secret bindings contain workspace-secret UIDs, never plaintext
values.
Manage workspace-secret values without returning them from the API:
const created = await rayrun.secrets.create({
name: 'GITHUB_TOKEN',
value: process.env.GITHUB_TOKEN,
});
const { items: secrets } = await rayrun.secrets.list();
await rayrun.secrets.rotate(created.secret.uid, {
redeploy: true,
value: process.env.NEW_GITHUB_TOKEN,
});
await rayrun.secrets.archive(created.secret.uid);Use secrets:read to list safe metadata and secrets:write to create, rotate, or archive secrets.
Rotation with redeploy: true also requires deployments:write because it restarts bound services.
Connect a GitHub repository once to deploy the selected branch now and on every future push:
const setup = await rayrun.githubRepositories.create({
owner: 'rayrun',
repository: 'hacker-news-mcp',
previewsEnabled: true,
});
console.log(setup.installationUrl);
const repository = await rayrun.githubRepositories.get(setup.uid);
await rayrun.githubRepositories.disconnect(setup.uid);The installation URL grants the Rayrun GitHub App access to the repository. Same-repository pull requests get isolated preview services; fork pull requests are ignored because previews may receive workspace secrets.
Embed Rayrun in an agent product
Map one application user to an external principal, then mint a short-lived, row-scoped MCP session:
const { uid } = await rayrun.externalPrincipals.upsert({
externalId: 'customer_123',
displayName: 'Ada',
metadata: { plan: 'pro' },
});
const session = await rayrun.externalPrincipals.createSession(uid, {
runtimeContext: { accountId: 'customer_123', plan: 'pro' },
});
console.log(session.endpoint, session.sessionToken);Sessions without toolAccessProfileUid are read-only. Pass a profile to define the explicit
session ceiling when the embedded user needs approved mutating tools.
Use createConnectLink when that user must authorize an upstream OAuth account. Correlate its
returned uid with connectLinkUid in the signed external.authorization.completed or
external.authorization.failed webhook. Failure codes are provider_denied, incomplete_response,
authorization_failed, and expired. List and revoke sessions explicitly, and delete
the principal when the application user is removed. Use an API key
with principals:read, principals:write, sessions:read, and sessions:write. The Python
rayrun-sdk distribution exposes the same lifecycle through Rayrun and AsyncRayrun.
Inspect the effective policy applied to one connected client without reproducing policy logic in your application:
const { items: effectivePolicy } = await rayrun.clients.listTools(clientUid, {
query: 'create issue',
});Each result includes the effective decision and the workspace, access-profile, or client layer that restricted it.
Reuse one access ceiling across clients
const { profile } = await rayrun.accessProfiles.create({
name: 'Support agents',
description: 'Read by default; sensitive tools stay blocked.',
});
const policy = await rayrun.accessProfiles.setPolicy(
profile.uid,
'read-only',
profile.toolPolicyVersion,
);
const details = await rayrun.accessProfiles.update(profile.uid, {
description: 'Read-only support access; sensitive tools stay blocked.',
expectedVersion: policy.toolPolicyVersion,
name: 'Support agents',
});
await rayrun.clients.setAccessProfile(clientUid, profile.uid, clientToolPolicyVersion);
const profileHistory = await rayrun.accessProfiles.listConfigurationVersions(profile.uid);
const original = await rayrun.accessProfiles.getConfigurationVersion(
profile.uid,
profileHistory.items.at(-1).uid,
);
await rayrun.accessProfiles.restoreConfigurationVersion(profile.uid, original.version.uid, {
expectedVersion: details.toolPolicyVersion,
reason: 'Restore the reviewed access ceiling',
riskConfirmed: true,
});The effective policy is always the intersection of the workspace, profile, and client rules. A
profile can narrow access but cannot grant something blocked by the workspace or client. History
versions profile details and rules together; assignments and archival stay outside the snapshot.
Restore refuses missing tools and requires riskConfirmed when it would allow a Destructive or
Unknown tool. Profile history is limited to 64 MiB per workspace.
Manage connection configuration history
Connection names, slugs, descriptions, timeouts, enablement, and payload-capture preferences are saved as one immutable configuration snapshot. Credentials, headers, OAuth state, identity and tool policy, health, and index state are deliberately excluded.
const { items: connections } = await rayrun.connections.list();
const connection = connections[0];
const updated = await rayrun.connections.setEnabled(connection.uid, {
enabled: false,
expectedVersion: connection.configurationVersion,
});
const history = await rayrun.connections.listConfigurationVersions(connection.uid, { limit: 25 });
const selected = await rayrun.connections.getConfigurationVersion(
connection.uid,
history.items.at(-1).uid,
);
await rayrun.connections.restoreConfigurationVersion(connection.uid, selected.version.uid, {
expectedVersion: updated.connection.configurationVersion,
reason: 'Restore the reviewed service configuration',
});Every material update records the user or API key, channel, time, and optional reason. Identical
saves are no-ops. Restore creates a new attributed version and fails with version_conflict if the
connection changed after it was read.
Manage hosted tool hooks
Hooks are versioned TypeScript adapters hosted and sandboxed by Rayrun. Pull the generated types and draft, test it without a live upstream call, then create an immutable shadow or active revision.
const { hook } = await rayrun.hooks.get(connectionId, toolUid);
const tested = await rayrun.hooks.test(connectionId, toolUid, {
source: hook.draftSource,
config: hook.draftConfig,
arguments: { query: 'release' },
mockResult: { items: [] },
});
const saved = await rayrun.hooks.saveDraft(connectionId, toolUid, {
source: hook.draftSource,
config: hook.draftConfig,
expectedVersion: hook.version,
reason: 'Normalize upstream release results',
});
const deployed = await rayrun.hooks.deploy(connectionId, toolUid, {
expectedVersion: saved.hook.version,
mode: 'shadow',
});
const runs = await rayrun.hooks.listRuns(connectionId, toolUid, { limit: 25 });
const history = await rayrun.hooks.listDraftVersions(connectionId, toolUid, { limit: 25 });
const prior = await rayrun.hooks.getDraftVersion(connectionId, toolUid, history.items.at(-1).uid);
await rayrun.hooks.restoreDraft(connectionId, toolUid, prior.version.uid, {
expectedVersion: deployed.hook.version,
reason: 'Restore the previous argument mapping',
});setDeployment moves an active or shadow pointer to an existing compatible revision, or deactivates
it with revisionUid: null. Every mutation uses expectedVersion; fetch and reconcile instead of
blindly retrying a version_conflict. Use a full-control API key for hook writes. Read-only keys can
inspect source, generated declarations, revision history, and deployed-run metadata; captured logs
and errors require hooks:write. RayrunApiError.diagnostics carries compiler line and column
details for editor and CI output.
Draft history is immutable and separate from deployment revisions: every material save records the
API key or user, optional change note, source, and non-secret config. Re-saving identical content is
a no-op. Restore always creates a new attributed draft head; it never rewrites the selected version.
Source plus config history is limited to 64 MiB per workspace. Export required versions, then call
hooks.reset with the current hook UID and version to permanently remove that hook's draft history
and deployment revisions. Rayrun blocks connection deletion while hook history remains.
Publish Workspace Skills
Encode each file as a package-relative path and base64 content. Validate before creating when CI needs the parsed name, frontmatter, digests, and warnings before it mutates workspace state.
const files = [
{
path: 'SKILL.md',
contentBase64: Buffer.from(
`---
name: incident-response
description: Investigate and communicate production incidents.
---
# Incident response
`,
).toString('base64'),
},
];
const validation = await rayrun.skills.validate(files);
const created = await rayrun.skills.create({ files, source: 'api' });
await rayrun.skills.publish(created.skillUid, {
deliveryMode: 'both',
expectedVersion: created.configurationVersion,
profileUids: [],
riskConfirmed: validation.riskFlags.length > 0,
});saveDraft creates an immutable revision without changing the published pointer. updateAudience
changes Code/Direct delivery and access-profile targeting without republishing. setEnabled,
archive, export, exportVersion, and restoreVersion cover the reversible lifecycle;
delete permanently removes retained revisions and delivery summaries after name and version
confirmation. The name remains reserved.
Use skills:read for inspection and export and add skills:write deliberately for publication
automation. The ordinary workspace-management key preset excludes skills:write. Rayrun stores
Skill bytes encrypted under the workspace key and serves them only after reauthorizing the MCP
client on each read. allowed-tools is guidance, not access policy.
Publish hosted services to your customers
A publication offers your hosted services to your own customers. They connect their AI clients to the publication's address and sign in with your OpenID Connect provider, without joining your workspace.
const { publication } = await rayrun.publications.create({
connectionUids: [connectionId],
displayName: 'Acme issues',
slug: 'acme-issues',
});
await rayrun.publications.setSignInProvider(publication.uid, {
clientId: 'rayrun-acme-issues',
clientSecret: process.env.ACME_OIDC_CLIENT_SECRET,
expectedVersion: publication.configurationVersion,
issuer: 'https://login.example.com',
scopes: ['openid', 'email', 'profile'],
tokenEndpointAuthMethod: 'client_secret_basic',
});
await rayrun.publications.publish(publication.uid, { reason: 'Launch' });Your customers add publication.mcpUrl to their AI clients. Allow publication.callbackUrl as a
redirect URI at your provider before anybody signs in, and a custom domain's callbackUrl as well
once you add one.
update, setSignInProvider, removeSignInProvider, and restoreConfigurationVersion save a new
configuration version and take the expectedVersion they were read at. The client secret is
write-only: omit it to keep the stored one, which works only while the issuer and client ID stay
the same.
listEndUsers and removeEndUser manage the people who have signed in, listApprovals shows the
calls they are being asked to approve, and setDomain, checkDomain, and removeDomain serve the
publication at a hostname of your own. A domain's blockedBy names what makes every request through
it answer 404 whatever its status: an unpublished publication, a missing sign-in provider, or a
suspended workspace.
Use publications:read to inspect publications and add publications:write to change them; the
Manage publications key preset holds exactly those two. Subscribe a webhook to
publication.changed to follow every change, including a custom domain that starts or stops
serving, and to publication.approval.requested to show an end user the approval link their call
is waiting on.
Speculative programmatic tool calling (streamed run-ahead)
Speculative programmatic tool calling (sPTC)
lets Rayrun start eligible read calls while the model is still writing execute_code. If the
finished program makes the same call, its result may already be waiting. Alex Zhang introduced sPTC;
Rayrun adapts the design from the MIT-licensed
spec-ptc project for an MCP gateway. Rayrun Chat uses it
automatically. Actual latency savings depend on how long model generation and eligible reads
overlap.
The helper below receives three values from your application: the MCP OAuth access token, your MCP
client, and an async iterable containing only execute_code argument deltas. The OpenAI Responses
API emits response.function_call_arguments.delta events; append event.delta. The Anthropic
Messages API emits content_block_delta events whose event.delta.type is input_json_delta;
append event.delta.partial_json.
import { RayrunGateway } from '@rayrun/sdk';
const callExecuteCodeWithRunAhead = async ({ mcp, mcpAccessToken, streamedArgumentDeltas }) => {
const gateway = new RayrunGateway({ accessToken: mcpAccessToken });
const runAhead = await gateway.codeRunAhead.open();
let finalArguments = '';
try {
for await (const delta of streamedArgumentDeltas) {
finalArguments += delta;
void runAhead?.feedArguments(finalArguments);
}
await runAhead?.flush();
const result = await mcp.callTool({
name: 'execute_code',
arguments: JSON.parse(finalArguments),
...(runAhead ? { _meta: runAhead.meta } : {}),
});
return result;
} finally {
await runAhead?.close();
}
};open() returns null when this optional optimization is rate-limited or temporarily unavailable.
feedArguments() coalesces snapshots while one request is in flight; call flush() once before the
final MCP request. Pass cumulative argument snapshots to feedArguments(), not individual provider
deltas. It throws synchronously when a snapshot is not a string, so TypeScript users get the mistake
at the call site. If a snapshot is neither identical to nor an append-only extension of the previous
snapshot, the SDK immediately removes the session metadata and sends close, even while an earlier
feed is in flight. Calling close() after abandoning a model branch also starts cancellation
immediately. If a session becomes unavailable, send the final MCP call normally. Rayrun runs ahead
only allowed tools approved as Read and advertised as read-only and idempotent. The final call
rechecks identity, policy, definition, arguments, and program before using an early result. If the
final code or dry_run differs, Rayrun closes the session and signals cancellation before ordinary
execution. Cancellation is cooperative, but Rayrun never gives a stale speculative result to the
final call.
Manage webhook configuration history
Webhook destination, description, event selection, delivery mode, batch size, enablement, and payload forwarding are saved together as immutable configuration versions. Signing secrets, delivery attempts, retry state, and health stay outside the snapshot.
const createdWebhook = await rayrun.webhooks.create({
eventTypes: ['connection.indexed'],
url: process.env.RAYRUN_WEBHOOK_URL,
});
// Store createdWebhook.secret now; Rayrun returns it only once.
const { items: webhooks } = await rayrun.webhooks.list();
const webhook = webhooks[0];
const updated = await rayrun.webhooks.update(webhook.uid, {
enabled: false,
expectedVersion: webhook.configurationVersion,
reason: 'Pause deliveries during maintenance',
});
const history = await rayrun.webhooks.listConfigurationVersions(webhook.uid, { limit: 25 });
const selected = await rayrun.webhooks.getConfigurationVersion(
webhook.uid,
history.items.at(-1).uid,
);
await rayrun.webhooks.restoreConfigurationVersion(webhook.uid, selected.version.uid, {
expectedVersion: updated.configurationVersion,
reason: 'Restore the reviewed destination',
});
await rayrun.webhooks.replay(webhook.uid, failedDeliveryUid);
await rayrun.webhooks.delete(createdWebhook.uid);Every material update records the user or API key, channel, time, and optional reason. Restore
creates a new version, revalidates the historical destination URL, and preserves the endpoint’s
current signing secret and delivery state. Identical saves are no-ops, stale expected versions fail,
and immutable webhook configuration history is limited to 64 MiB per workspace. Destination paths
and query strings can contain provider tokens, so read-only keys receive null URLs; keys with
webhooks:write receive the full current and historical destination.
Verify webhook signatures
Pass the exact request body and the Rayrun-Signature header before parsing the event. Verification
also rejects timestamps outside a five-minute replay window by default.
import { verifyWebhookSignature } from '@rayrun/sdk';
const valid = verifyWebhookSignature({
body: rawBody,
secret: process.env.RAYRUN_WEBHOOK_SECRET,
signature: request.headers['rayrun-signature'],
});After verification, parse the body as WebhookPayload. Every event has a stable id, type, and
occurredAt; its data shape is narrowed by type. Delivery is at least once, so make processing
idempotent and deduplicate on id.
For activity.call, arguments and result contain the captured tool payloads, or null when
capture was disabled for that connection, the webhook did not opt in with
forwardToolPayloads: true, or no result existed. payloadCaptured distinguishes a metadata-only
webhook from a call where capture was disabled. Rayrun observes MCP tool arguments and results; it
does not receive the host's full Claude or GPT prompt, assistant response, or surrounding chat.
import type { WebhookPayload } from '@rayrun/sdk';
const event: WebhookPayload = JSON.parse(rawBody);
if (event.type === 'review.changed') {
console.log(event.data.reviewUid, event.data.status);
}Verify who is calling a hosted server
When a publication's end user calls a tool, Rayrun signs who they are and sends it to the hosted
server as the Rayrun-Identity header and as _meta["io.rayrun/identity"]. A call from your own
workspace carries neither. Every hosted server is given RAYRUN_CONNECTION_UID and
RAYRUN_IDENTITY_JWKS_URL, so passing process.env is all the configuration verification needs.
import { verifyIdentityAssertion } from '@rayrun/sdk';
const identity = await verifyIdentityAssertion({
assertion: request.headers.get('rayrun-identity'),
environment: process.env,
});
if (identity !== null) {
console.log(identity.endUserUid, identity.externalId, identity.publicationUid);
}It resolves null for a call without an assertion, and for one that fails any check: the signature,
the type, the audience (this server's connection), or the one-minute lifetime. It throws when an
assertion arrives but there is no audience or key set URL to check it with, or when the key set cannot
be fetched, so a misconfigured server fails loudly instead of serving everybody as nobody.
externalId is the subject the publication's sign-in provider asserted, which is what your own user
records already know them by; email is present only when that provider said it was verified.
Version-controlled access policies
Server-side drafts
const document = await rayrun.accessProfiles.exportConfiguration('profile123');
document.configuration.toolAccessMode = 'read-only';
const draft = await rayrun.accessProfiles.saveDraft(document, { expectedDraftRevision: 0 });
const review = await rayrun.accessProfiles.reviewDraft(document.profileUid, draft.draftUid);
await rayrun.accessProfiles.activateDraft(document.profileUid, draft.draftUid, {
reason: 'Reviewed support rollout',
});Draft saving requires policies:write and leaves live access unchanged. listDrafts(profileUid,
{ beforeRevision }) returns up to 100 immutable revisions with attribution; the first save expects
revision zero, and later saves require the latest draft revision. Review is a read-only validated diff,
not an approval record. Activation applies the exact saved document and rejects a stale live version.
clone(profileUid, { expectedVersion, name }) atomically creates an unassigned block-all profile with
the source configuration in its first draft. archive(profileUid, expectedVersion) retires an unused
profile; history and drafts remain readable. Deactivation uses applyConfiguration with block-all
and an empty toolRules list, retaining assignments and recording a live revision.
Direct configuration apply
const document = await rayrun.accessProfiles.exportConfiguration('profile123');
document.configuration.toolAccessMode = 'read-only';
const plan = await rayrun.accessProfiles.planConfiguration(document);
// Review plan.before, plan.after, and plan.requiresRiskConfirmation.
await rayrun.accessProfiles.applyConfiguration(document, { reason: 'Reviewed in PR 42' });Persist the exported JSON in version control for review. The complete configuration is replaced in one
transaction with the exported expectedVersion and workspace as guards. Omitted tool rules revert to
the profile default. A stale document fails with 409 version_conflict; export again and review before
retrying. High-risk grants require riskConfirmed: true. Export and plan require policies:read, and
apply requires policies:write. Client assignments, connections, credentials, and content scanners are
outside this document. Each changed apply records one immutable access-profile revision.
