@odla-ai/apps
v0.15.2
Published
The odla platform management SDK — create apps, toggle services (db, o11y, blog, ai, calendar), and delete apps via the registry. Control plane only; data-plane SDKs like @odla-ai/db are what running apps link.
Downloads
1,427
Maintainers
Readme
@odla-ai/apps
⚠️ Early access — pre-1.0. Agents work from bounded runbooks; humans approve credentials, production changes, releases, and merges. APIs and exact package availability can change. Review the documented guarantees and limitations; this software is MIT-licensed and provided without warranty.
The odla platform management SDK — the control-plane client for the app
registry, the way gcloud is to GCP. Use it from agents, CLIs, and CI to
create apps, toggle services (db, o11y, blog, ai, calendar, …), and archive apps.
It is not a data-plane SDK: apps talk to their database with @odla-ai/db,
emit telemetry with @odla-ai/o11y, and so on — those SDKs are what a running
app links; this one is what manages apps.
An app is the platform's project primitive. Services keep their own
credentials and data planes; the registry owns app identity, ownership,
service enablement, monotonic configuration revisions, and the operation
journal. Registration is platform-level and à la carte — an app can enable
blog without ever enabling db.
Every app view includes a random incarnation. The appId is the reusable
project address; the incarnation is the exact lifetime at that address.
Purging and recreating the same id rotates it. Exact project grants, delegated
authority, immutable consumption receipts, and service lifecycle requests bind
that value, so delayed work from the old project cannot act on the replacement.
Wildcard project policy remains intentionally dynamic over projects the
manager owns now.
Ask the runbooks first. odla's operational procedures live in a database, not in this file:
npx @odla-ai/cli runbook ask "<question>"returns the current steps, and unlike anything written here it cannot be out of date. Use it before searching the web or working from memory. This README and the JSDoc in the shipped.d.tsare the version-matched API reference; a runbook is the procedure. Most tasks need an answer from both.
Install
npm i @odla-ai/appsUse from a Worker (service binding)
import { createAppsClient } from "@odla-ai/apps";
const apps = createAppsClient({ fetcher: env.APPS, token: env.APPS_TOKEN });
const app = await apps.resolveApp("my-app");
if (!(await apps.isEnabled("my-app", "o11y", { env: "dev" }))) {
return new Response("o11y not enabled for this app", { status: 403 });
}Use over HTTP
const apps = createAppsClient({ endpoint: "https://odla-apps.example.workers.dev", token: MACHINE_TOKEN });
const created = await apps.createApp({ name: "My App", appId: "my-app" });
await apps.setService(created.appId, "o11y", true, { env: "dev" });
// Record where each environment's deployment RUNS — shown on the app in
// Studio and readable via the public-config endpoint. Null clears it.
await apps.setLink("my-app", "prod", "https://my-app.example.com");
const app = await apps.resolveApp("my-app");
app?.links.prod; // "https://my-app.example.com/"
// Enable + configure the ai service (non-secret half: provider + default
// model, surfaced on public-config). The provider API KEY goes in the
// platform vault — odla-db tenant secrets — never in the registry.
await apps.setAi("my-app", "prod", { provider: "anthropic", model: "claude-sonnet-5" });
// Configure live Google booking. This call stores non-secret intent only; a
// human opens the returned consentUrl. Event data remains in Google.
await apps.setCalendar("my-app", "dev", {
provider: "google",
access: "book",
bookingCalendarId: "primary",
availabilityCalendars: ["primary"],
bookingPageUrl: null,
});
const attempt = await apps.beginGoogleCalendarConnect("my-app", "dev");
console.log(attempt.consentUrl);
const status = await apps.getCalendar("my-app", "dev");Selective-release review and approval
@odla-ai/apps carries the Registry contract shared by Studio and the CLI.
Creating a promotion release plan stores an immutable review document; it does
not call a provider or mutate a source or target. Each target includes its
validated @odla-ai/promotion bundle, exact before values and digests, explicit
identifier mappings, exclusions, precondition observations, and non-secret
provider-readiness evidence.
const plan = await apps.createPromotionReleasePlan("built-not-found", input);
const status = await apps.getPromotionReleaseStatus("built-not-found", plan.planId);A directly signed-in human may call approvePromotionRelease for either
apply or rollback. The approval is short-lived and binds the action, plan
digest, and complete target set. requestPromotionReleaseOperation accepts that
approval only from an enrolled-device session; an ordinary agent token, a human
session, a changed digest, a changed app lifetime, a blocked plan, or an expired
approval fails closed. A repeated device idempotency key returns the same
durable operation. The operation is a dependency-ordered journal: an enrolled
device leases one exact target step with claimPromotionReleaseWork, performs
only that bounded payload, and commits evidence with
recordPromotionReleaseWork. An expired lease is safely reclaimed after a
follower exits; a retry or target rejection remains visible per app.
activate_revision is always last and accepts only a receipt proving the
planned higher revision. Cancellation is allowed before activation; after any
target activates, recovery requires a separately approved rollback operation,
which also publishes a higher revision instead of rewriting history.
Every plan resolves target-local tenant, route, worker, service bindings, Stripe mode and target Price IDs, and calendar mode before approval. Dev targets require test Stripe and sandbox calendar identities. Source provider IDs and unknown resolution fields fail closed instead of being copied across modes.
App-service catalog
APP_SERVICE_CATALOG is the versioned, content-free vocabulary shared by the
Registry, Studio, and CLI. Each entry separates its product kind, same-env
dependencies, lifecycle controller, and Studio management behavior from the
app's mutable enabled value. Services that own a first-class app workspace
also declare its canonical id, label, icon, and ordering here:
import {
APP_SERVICE_CATALOG_SCHEMA_VERSION,
appServiceDefinition,
} from "@odla-ai/apps";
console.log(APP_SERVICE_CATALOG_SCHEMA_VERSION); // odla.app-services/v2
console.log(appServiceDefinition("chat")?.requires); // ["db"]
console.log(appServiceDefinition("code")?.workspace?.id); // codeUnknown ids fail closed in Registry and project-config validation. Add a new enableable service here first; consumers derive their inventories and dependency checks from this catalog instead of carrying local lists.
Studio handoff URLs
Use studioAppSettingsPath when an SDK, CLI, readiness check, or operation
receipt needs to send an operator to the setting that owns an action:
import { studioAppSettingsPath } from "@odla-ai/apps";
studioAppSettingsPath("my-app", "dev"); // /studio/apps/my-app/dev/settings/environment
studioAppSettingsPath("my-app", "prod", "app"); // /studio/apps/my-app/prod/settings/app
studioAppSettingsPath("my-app", "prod", "backups"); // /studio/apps/my-app/prod/settings/backupsThe explicit final segment is intentional. Environment settings own services, Code defaults, end-user auth, AI configuration, and the deployment link. App settings own repository binding, name, owners, category, lifecycle, and the danger zone. The selected environment remains in an app-settings URL as return context; it does not make those controls environment-scoped. Backups settings own the selected environment's restore points and portable database snapshots, plus app-wide repository archive limits when Harness is enabled.
For an interactive chooser, call beginGoogleCalendarConnect(appId, env) and
poll the returned attempt. A healthy result has connected === true,
writable === true, and state === "healthy". Use listGoogleCalendars to
choose the booking calendar and one through ten availability calendars, then
persist that non-secret intent with setCalendar.
Calendar OAuth is a separate human checkpoint. The management SDK receives
only an opaque attempt and authorization URL; Google codes, access tokens, and
refresh tokens stay inside the platform connector. Use
listGoogleCalendars and disconnectCalendar for the owner-safe lifecycle.
The optional public booking-page URL is a legacy fallback, not a credential.
This management surface owns connection and non-secret configuration, not
event data. Trusted app backends use @odla-ai/calendar for live FreeBusy,
upcoming-event reads, and idempotent create/reschedule/cancel operations.
Google is the single source of truth; there is no sync, resync, retention
workflow, or event mirror. Passing { purge: true } to disconnectCalendar
is rejected; public config exposes only aggregate connection/bookability state.
disconnectCalendar stops watches and deletes only that app/environment
connection's encrypted platform token, cursors, and connector state. It does
not revoke the shared user-to-Google-OAuth-project grant, because a project-wide
revoke could invalidate the user's other odla connections. A future explicitly
global revoke operation would need cross-connection accounting and warning.
New automation should always name the environment. For backward compatibility,
passing services to createApp enables them in both default environments,
and omitting env from setService/isEnabled targets the legacy prod alias.
Those shortcuts are intentionally not the dev-first golden path.
Conditional configuration operations
App.configRevision is the Registry compare-and-set revision
(registry:<positive integer>). Every managed name, service, auth, link,
category, and lifecycle write advances it. A reconciler freezes that revision,
desired/observed content digests, and ordered actions into a plan before
calling applyConfigOperation:
const app = await apps.resolveApp("my-app");
if (!app?.configRevision) throw new Error("Registry does not support conditional config");
const receipt = await apps.applyConfigOperation("my-app", {
schemaVersion: "odla.config-operation-request/v1",
expectedRevision: app.configRevision,
desiredRevision: plan.desiredRevision,
observedRevision: plan.observedRevision,
planDigest: plan.planDigest,
idempotencyKey: "deploy-2026-07-29-my-app",
source: { kind: "api", version: "reconciler-1" },
actions: plan.actions,
});Do not hand-author actions: consume a frozen odla.config-plan/v2 document.
The server verifies that the plan digest binds both content revisions, the
Registry revision, and every ordered action. Reservation happens before any
effect. A stale revision, another running app operation, or reuse of an
idempotency key for different bytes fails with a stable conflict.
getConfigOperation(appId, operationId) reads the same journal without
replaying effects. Retrying applyConfigOperation with the same app,
idempotency key, and plan resumes the first incomplete step; a step whose
effect committed before its progress update is recovered from Registry state.
A terminal operation returns the same digest-authenticated receipt on every
retry.
This release conditionally applies app display-name changes, non-destructive service enable/configure actions, and deployment links when their frozen plan does not require a human checkpoint. Production actions, app creation, auth propagation, service disablement, archive/purge, and every secret or credential remain outside this endpoint. Approval-marked, high-risk, and Studio actions are rejected before an operation is reserved.
Credential caveats (agents provisioning with a dev token)
- A developer token authenticates one named agent principal. It does not inherit
its manager's projects: ordinary Studio approval grants project metadata
access, PM execution/Backlog proposals, discussion participation, and
brand-candidate editing on selected exact projects only. PM planning/Ready
approval, app administration, brand approval, and ambient owner access are
excluded. CRM reference reads are also excluded by default; the approving
owner may explicitly select the unchecked read-only
crm.readoption for those exact projects. - The provisioning CLI deliberately requests optional
app.managefor its one exact app. Once the owner approves that immutable request, the agent may create a reserved missing app, configure its services, auth, and link, and administer that app's sibling-service tenants. Ownership, rename/category, and lifecycle mutations remain direct-human operations. - Reusing the same agent-requested, human-approved handle reconnects a fresh ordinary handshake to the existing agent principal. The handle is covered by the immutable request digest; approval cannot rewrite it. Approval leaves the current credential live; it also leaves the current grant set unchanged. Successful replacement collection atomically retires older collected ordinary-handshake credentials and replaces the principal's manager-issued grants with the reviewed exact project set. Newly selected projects are unavailable before collection and omitted projects are revoked during the switch. Manually minted and separately scoped credentials remain explicit inventory items.
- Baseline agents cannot create arbitrary projects, administer tenants, or mint
durable app credentials. A provision handshake is the narrow exception: its
owner-reviewed
app.managegrant and exact-id bootstrap reservation authorize only the selected app lifetime. Other authority-expanding effects still need a signed-in human or a consumed one-action approval receipt. - Pre-principal developer tokens are migrated without project grants and fail
closed (
legacy_reenrollment_required). Re-enroll each automation as a named agent and select its exact projects; the migration never guesses ambient authority from historical ownership. resolveApp()returns only projects visible to the authenticated principal;nullmeans absent or unauthorized and deliberately does not reveal which.- Minting a db key (
POST /admin/apps/:tenant/keys) is additive: existing keys keep working, so provisioning reruns never break deployed Workers. setAuth(appId, env, { publishableKey })needs only the Clerk publishable key; the issuer is derived and applied to that env's db tenant. Workers should fetch the resulting public-config at runtime (short cache) instead of baking auth into env vars — key rotation then needs no redeploy.
Principals and project authority
The registry resolves the actor from the bearer credential. Its exported
Operator keeps two identities separate:
developerIdis the accountable human account used for app ownership.principalId,principalKind,displayName, andhandleidentify the human, agent, or service that actually used the credential.credentialId/credentialKindidentify that credential.managerPrincipalIdis organizational metadata only. It never grants an agent access to a manager's projects.delegatedByPrincipalId,grantId, andgrantVersionpreserve the revocable authority used for the action.
Project grants are resolved server-side from the live registry immediately before an action. Clients supply a bearer credential, not a principal, manager, or grant claim. A revoked grant therefore denies the next action, and changing an agent's display label cannot change either its identity or its authority. An exact grant also binds the app incarnation; a same-id recreation does not revive it. Historical receipts remain audit evidence but cannot replay as current authority in a later incarnation. Here, immutable means append-only through the Registry contract and read-only through its public SQL view. Registry code and operators holding the underlying D1 binding remain trusted; receipts are not cryptographic tamper evidence against a compromised control-plane writer.
This remains true for a shared project run by three humans: each person can manage a friendly named agent, but every agent keeps its own credential and only the exact project grants that a human approved. Neither co-ownership nor manager attribution permits impersonation. An authority-expanding effect must consume a one-action approval receipt bound to the resource, actor, credential, live grant revision, expiry, and idempotency key.
Signed-in human owners approve one exact change through
consumeHumanExactAuthority(appId, input). The SDK sends only the capability,
project capability, effect, exact resource, action digest, and retry key. The
registry derives the Clerk-bound human identity, short-lived self-grant, and
immutable receipt; callers cannot submit an actor, grant, expiry, or evidence.
An identical retry returns the original receipt, while reusing the retry key
for a different action fails. This is the approval boundary for changes such as
accepting an agent's brand proposal—not a switch that disables unused features.
A provisioned app normally has a different Clerk issuer and subject namespace
from odla Studio. It must not send that local bearer to verifyOperator or
persist or exchange either Clerk session. A machine-authenticated
service-binding client calls
verifyAppHuman(appId, env, appToken) for the owner projection and
consumeAppHumanExactAuthority(appId, env, appToken, input) for an exact
approval receipt. The service credential is transport authority only: every
call verifies the local JWT against the exact configured issuer and audience,
normalizes its signed canonical email, requires exactly one active platform
human with that email, and rechecks the app/auth lifetime and current ownership.
Missing, invalid, unmatched, or ambiguous emails, backend keys, agents,
devices, removed owners, and stale app/auth configuration remain denied. Never
log or persist the Clerk bearer.
Provisioning contract
Service Workers implement POST /svc/enablement (PROVISION_PATH); the
registry calls it when an operator toggles the service for an app, and stores
the returned service-owned config (e.g. an o11y serviceId). Hosted services
are enabled only after their controller acknowledges the operation. Missing
bindings/credentials and unknown services fail closed; ai and blog are
registry-owned capabilities, as declared by APP_SERVICE_CATALOG. Deletion calls destroy for every
provisioned environment, including disabled services whose data was retained.
Partial failures leave the app record in place and return structured operation
status so an idempotent retry can finish cleanup. Every controller request
carries app.incarnation; service controllers keep their physical tenant or
Durable Object incarnation separate and compare the Registry lifetime before
suspend, resume, destroy, backup-policy, or delayed sink work. calendar is a
registry-owned connector: enablement stores non-secret intent, disable
suspends booking access while retaining connector state, and destroy deletes
that app/environment's encrypted token and state without revoking the shared
Google project grant. There are no odla-db event-mirror rows to purge. The SDK
preserves structured failures on AppsError.details.
