# Munky integration

Munky learns from your product's events and coordinates work through capabilities you provide.
Your application keeps its own accounts, interface and business data. Munky owns the learning,
execution contracts and result checks. Attesko is a client of this API.

The [public guide and reference](https://api.munky.sh/docs/integration.md) are readable without an
account. The [API reference](API-REFERENCE.md) lists every public route, its authentication and validated
request examples. The deployed Markdown includes that reference and the local runtime guide below
the integration guide; relative file links refer to repository copies. This guide explains how the
pieces fit together.

## Choose your integration

| Need | Use today | Boundary |
| --- | --- | --- |
| Add learning and checked actions to existing software | Trusted backend + project key + tenant/principal mapping + public SDK | Keep your existing customer login. A tenant is a customer workspace, not a developer account. |
| Run supported work on a customer's computer | Register the tenant's Edge Runtime and deliver its scoped credential | Your app supplies native permissions, login UI, secure storage and supported execution adapters. Never distribute a project key. |
| Keep inference and private memory local | Embed the versioned local runtime and a separately qualified model | Runtime archive contains no model weights; external website tasks still need network access. |
| Use MUNKY-hosted end-user accounts and workspace membership | Application accounts, sessions and workspaces APIs through your trusted backend | Separate project/environment identity; explicit invitations and device enrollment. Existing `/product/v1/*` clients need migration. |

MUNKY OS and Attesko consume the same public engine. Their personal/business interfaces and curated
experiences are separate products; the API must still document every capability needed to implement
supported execution. Model quality, arbitrary app compatibility and ready-made provider connectors
must be qualified separately from a successful API request.

## Create a project and key

1. Open [the developer console](https://api.munky.sh/setup).
2. Create an account and enter the six-digit code sent to your email address.
3. Name your project, open **Keys**, and create a **development** key.
4. Save the key in your server's secret store. It is displayed once and cannot be retrieved later.

The intended API origin is `https://api.munky.sh`. During the current domain cutover, use
`https://munkyapi-production.up.railway.app` for the console, API and versioned downloads if the
custom domain does not resolve. Set the SDK's `baseUrl` to the same reachable origin. This temporary
host uses the same Munky service; domain cutover is not complete.

Project keys begin with `munky_sk_` and belong to one
project and environment. Browser sessions use a separate HttpOnly cookie; a project key cannot
sign in to the console. Never put project keys in browser JavaScript or a desktop installer.

```sh
curl --fail-with-body https://api.munky.sh/v1/whoami \
  -H "Authorization: Bearer $MUNKY_API_KEY"
```

The response identifies your project, environment, scopes and available services. Use development
tenants before production. The same external customer ID in two environments has separate state.

## Hosted accounts and workspaces (SDK 0.6.0)

Use this optional path when you want Munky to manage your application's login. Existing software
can keep its own accounts and use the tenant/principal/runtime APIs directly.

Your server needs a newly issued project key with `application-accounts:write` and
`application-workspaces:write`. Existing keys do not silently gain these scopes. Every request is
bound to that key's project and environment. Use different projects for MUNKY OS and Attesko; an
email shared by both products does not share an account, password, session or workspace.

```js
await munky.applicationAccounts.signup({ email, password });
// Show code entry in your app. Never log passwords or codes.
const login = await munky.applicationAccounts.verify({ email, code });
const sessionToken = login.session.token;
const workspace = await munky.applicationWorkspaces.create(sessionToken, {
  kind: "personal", // use "business" for a company workspace
  displayName: "My work",
});
const registration = await munky.applicationWorkspaces.enrollDevice(
  sessionToken, workspace.id, deviceCapabilities,
);
```

These calls belong on your trusted backend. Authenticate the incoming product request, preserve
its application/environment binding and store the session token securely. The backend forwards it
as `x-munky-application-session`; the SDK does this per call and never appends it to a URL. Return
only the appropriate session/device information to the authenticated product client. Use native
secure storage on desktop; for your own web session use an HttpOnly cookie and CSRF protection.
Never embed the project key or a shared session token in public JavaScript or an installer.

`deviceCapabilities` uses the registration schema from the reference. Start with execution `none`
until the app actually implements its declared adapters. The returned registration works with
`createEdgeRuntimeClient` as shown below. A session establishes identity, not purchase authority.
Workspace membership creates an engine principal with no approval permissions. Your integration
must deliberately grant appropriate permissions and implement the signed decision flow separately.

### Login, recovery and session lifecycle

- `applicationSessions.signIn({email,password})` issues an independent seven-day session.
- `applicationSessions.current(token)` reads the account/session, without returning its secret.
- `applicationSessions.revoke(token)` signs out that session; `revokeAll(token)` signs out all
  sessions for that account in that application/environment.
- `applicationAccounts.resend({email})` replaces a pending signup code.
- `startRecovery({email})` and `verifyRecovery({email,code})` recover access to an existing account.
- `changePassword(token,{currentPassword,newPassword})` replaces the password and revokes every
  session, including the caller. A session issued through recovery may omit `currentPassword`
  during its first 15 minutes. Afterwards, sign in again with the new password.

Email codes expire after 15 minutes and allow five wrong guesses. Sending is limited to three
messages per address per hour and 100 per application/environment per hour. Additional signin and
verification budgets apply; a 429 means stop and offer a later retry. Responses do not disclose
whether an address exists. Passwords and session tokens are stored as hashes. Session revocation
is persisted and checked by workspace mutations, including enrollment racing with sign-out.

### Company membership and devices

Use `applicationWorkspaces.list(token)` after login. Personal creation returns the same single-owner
workspace on repeat; a business create makes a new workspace. Business creation and other mutations
are not automatically retried after a lost response. Read the workspace/device list to reconcile
before asking for another operation.

Owners and admins read `members(token,workspaceId)` and create an invitation with
`invite(token,workspaceId,{email,role:"member"})`. Only the owner can invite an admin. The response
contains an invitation ID, expiry and single-use token. Your product delivers it privately; Munky
does not send invitation email from this endpoint. The invited person signs up/signs in to the same
application/environment and calls `acceptInvitation(token,{token:invitationToken})`. Only the exact
verified email can accept; email-domain matching never grants membership. Invitations expire after
seven days and can be revoked with `revokeInvitation(token,workspaceId,invitationId)`.

`removeMember(token,workspaceId,accountId)` revokes membership, device credentials and pending
invitations associated with that member. The owner cannot be removed. Admins cannot remove another
admin. Members see only their own devices; owners/admins can manage workspace devices. Ordinary
members can enroll observation-only devices. Owner/admin membership is required to enroll execution
adapters because current Edge assignments have workspace-wide reach. Fine-grained delegated employee
execution and ownership transfer are not part of this release.

### Pause, resume and data removal

`updatePolicy(token,workspaceId,{paused:true})` lets an owner/admin pause new runtime and backend
work leases, device observations and frame escalations. Read `policy(token,workspaceId)` or resume
with `paused:false`. Pause does not cancel already-issued work or stop a disconnected local process.
Reports, independent product events and result verification remain available to finish recording
work that already happened. Historical imports also remain available.

Tenant export includes workspace/membership/invitation/device metadata without secret tokens or
hashes. Tenant erasure removes that workspace and its device credentials. Application accounts and
sessions remain, since a person can belong to other workspaces; workspace erasure is not account
deletion. An owner can call `applicationWorkspaces.erase(token,workspaceId)` to erase a workspace;
admins and members cannot. Review the returned erasure record for retained audit records.

Use `applicationAccounts.delete(token,{currentPassword})` to delete the current account. As with
password replacement, recent mailbox recovery can replace password proof. Deletion returns 409
`application_account_owns_workspace` until every owned workspace is erased. It removes the scoped
account, password, sessions and memberships and revokes device credentials. Historical audit and
principal identifiers remain without usable authority; other applications and environments remain
untouched. Legacy product-account migration is still required.

## Native application gateway

A native product can use the optional gateway at `/v1/apps/:appId` without receiving a project
key. The operator configures each app ID with a separate server-held key; MUNKY OS uses `munky-os`
and a personal workspace. Attesko must use its own project and business configuration. These routes
reuse the public application accounts/workspaces and phone services; they do not restore
`/product/v1/*`. The generated reference lists each request body.

Use native HTTP networking, such as Swift `URLSession`. Browser requests with an `Origin` header
are refused, no browser CORS access is granted, and cookies are not forwarded. Keep the returned
application session in the platform's secure credential store. Send it only as
`x-munky-application-session`; never put it in a URL. Responses are not cacheable.

| Native gateway path, relative to `/v1/apps/:appId` | Method and input |
| --- | --- |
| `/status` | GET; returns `appId`, `available: true` and configured `workspaceKind`. Missing, revoked or insufficient server credentials return 503. |
| `/accounts/signup`, `/accounts/resend`, `/accounts/verify` | POST; same email/password or email/code schemas as application accounts. |
| `/accounts/recovery/start`, `/accounts/recovery/verify` | POST; email, then email/code. |
| `/sessions/sign-in` | POST email/password; returns account and application session. |
| `/sessions/current`, `/sessions/revoke` | POST `{}` with the session header; read current session or sign out. |
| `/workspaces` | GET lists active memberships; POST `{ "displayName": "My work" }` creates the configured kind. A client cannot supply `kind`; MUNKY OS is personal only. |
| `/workspaces/:workspaceId/devices` | GET lists permitted devices; POST uses `RegisterEdgeRuntimeInput` and returns the scoped runtime credential once. |
| `/workspaces/:workspaceId/devices/:runtimeId` | DELETE revokes a permitted device. |
| `/workspaces/:workspaceId/policy` | GET reads policy; PUT `{ "paused": true }` pauses new work, subject to owner/admin authorization. |
| `/workspaces/:workspaceId/phone-challenges` | POST `{ "phone": "+15555550123", "consent": true }`; principal and tenant come from the caller's membership. |
| `/workspaces/:workspaceId/phone-challenges/:challengeId/verify` | POST `{ "code": "123456" }`. |
| `/workspaces/:workspaceId/phone-contact` | GET reads the caller's masked contact; DELETE revokes it. |
| `/workspaces/:workspaceId/interruptions` | GET for owners/admins; accepts only `status`, `limit`, `cursor` query parameters. Returns the existing uncertainty inbox shape. |
| `/workspaces/:workspaceId/receipts` | GET for owners/admins; optional `limit`. Returns the existing receipt list. |

All workspace, phone and inbox routes require the application-session header. The gateway checks
current membership and derives tenant/principal identifiers for phone and inbox requests; clients
cannot select another principal. The gateway exposes no arbitrary proxy URL, project credential,
work-claim endpoint or approval bypass. Use the enrolled runtime for computer work and the documented
signed decision contract for approvals. A readable interruption is not authority to resolve it.

## Recover console access

Open `/recover`, enter your developer email and use the six-digit code from your inbox. The code
expires after 15 minutes and allows five incorrect attempts. A successful code signs you into your
existing project; it can be consumed only once, including simultaneous requests. Requesting another
code replaces the old code. The response does not disclose whether an email has an account.

Recovery does not change the password or sign out other browsers. It clears a password-attempt
lockout. Password replacement and server-side session revocation are separate unfinished account
features; do not promise them in a client or label this flow “reset password.”

## Use the SDK

Install the versioned SDK package directly from Munky:

```sh
npm install https://api.munky.sh/sdk/munky-sdk-0.8.0.tgz
```

The package includes its public types and runtime code. You do not need access to Munky's private
repository. Keep the resolved integrity hash in your package lockfile.

```js
import { createMunkyClient } from "@munky/sdk";

const munky = createMunkyClient({
  apiKey: process.env.MUNKY_API_KEY,
  baseUrl: "https://api.munky.sh",
  edge: { pseudonymizationKey: process.env.MUNKY_PROJECTION_KEY },
});

const tenant = await munky.tenants.upsert({
  externalId: "customer_2048",
  displayName: "Northwind",
});

const event = await munky.events.track({
  tenantId: tenant.tenantId,
  source: "billing",
  type: "invoice.updated",
  occurredAt: new Date().toISOString(),
  actor: { id: "user_382" },
  subject: { type: "invoice", id: "invoice_8291" },
  changedFields: ["status"],
  before: { status: "overdue" },
  after: { status: "follow_up_sent" },
  correlationId: "follow_up_8291",
});
```

`event.status` is `accepted` or `duplicate`. `unmapped` means the event has been stored but its
meaning has not been confirmed. `removedFieldCount` reports fields excluded by the local projector.
HTTP 200 alone does not mean every event in a batch was accepted: inspect each item.

Your projection key must contain at least 32 characters. Keep it stable and private. The SDK
uses it to replace record and person identifiers with tenant-specific pseudonyms before sending
an event. It allows operational state fields and removes names, contact details, free text and
recognized secrets. Rotating the key changes the identifiers and breaks continuity with old events.

Keep your native event names. Do not invent workflows or send screenshots through the event API.
Unknown event types remain unmapped until their semantics are explicitly confirmed through
`POST /v1/tenants/:tenantId/events/aliases`. Historical import uses `/events/import` and does not
replay old cases as new work.

## Browser and native WebView imports

SDK 0.2.0 provides browser-compatible entries with public declarations:

| Import | Use |
| --- | --- |
| `@munky/sdk/contracts` | Validate and type the shared wire messages |
| `@munky/sdk/edge-runtime` | Device registration, scheduling, events and work |
| `@munky/sdk/observation` | Local observation processing and explicit escalation contracts |
| `@munky/sdk/computer-use` | Scoped computer-use credential and action contracts |

The root `@munky/sdk` and `browser-session` entries run on Node. Do not import the root into a
WebView: it includes server signing helpers. Browser entries do not accept project keys. Supply
native capture, secure credential storage and execution ports through their declared interfaces.
The observation entry contains Munky-owned model coordination; using it does not qualify a model
or permit image upload without the user's configured cloud-help choice.

The same SDK archive includes `native/local-model`, a Rust library for numeric-loopback-only
model transport and experimental task/browser suggestions. Install the SDK before compiling the
native product, then point its Cargo dependency at that directory. Munky maintains the library;
the product supplies UI, capture, secure storage and task authority. The archive contains model
integrity manifests but no weights. Commit the npm/pnpm and Cargo lockfiles together.

SDK 0.3.0 adds `native/runtime-host`, which supervises the core, model and company relay.
The product supplies its identity, resource path and private storage path. Follow the bundled host
README for relay startup before enforcing the local network policy. The host binds request
credentials to its private origin; the product cannot retrieve those credentials.

Existing 0.1.0 and 0.2.0 archive URLs retain their original bytes for pinned consumers.

## Numbered text decisions (SDK 0.4.0)

Install `https://api.munky.sh/sdk/munky-sdk-0.4.0.tgz` to use the Node-only
`@munky/sdk/text-decisions` entry. It also runs in a trusted local Node host; it needs no cloud call
or provider API key to format and interpret a decision. Existing browser entries are unchanged.

`prepareTextDecision(input)` validates a two-to-four-option request and returns `{ version, input,
body, bindingHash }`. Use the exported `TextDecisionInput` type or `TextDecisionInputSchema`. Each
option supplies a stable `id`, plain-language `label` and `consequence`, an `intent` (`preference`,
`purchase` or `decline`), an exact `planHash` and a quote or `null`. Always include a decline choice.
Yes/No mode has exactly two options: the proposed action first, decline second.

Purchase quotes require provider, product, quantity, currency, total due now in minor units, known
tax treatment, renewal amount/interval/start or no recurrence, material terms, source URL and quote
timestamps. USD, EUR, GBP, CAD and AUD are supported; amounts use two minor-unit decimal places.
The request window cannot outlast its quote. Unknown tax is allowed only for a preference, which
must not result in a purchase. Source URLs and prices are supplied evidence, not verified by the SDK.

Persist the exact packet before delivery. After authenticating the recipient and correlating the
provider reply to that request, call `interpretTextReply(packet, reply, now)`. Supply the stored
`requestId`, `tenantId`, `principalId`, `conversationId` and `bindingHash` from trusted routing state,
plus the reply `body`. A selected result returns the stable `optionId`, `intent`, `planHash` and
`bindingHash`; it is not itself an authorized case decision. Revalidate current prices/authority and
atomically persist the existing bound decision before dispatch. Refused results cause no action.

Never fill correlation fields from the newest pending task merely because a plain SMS says “1”.
The transport still needs reliable request correlation, signature/identity checks, durable inbox and
outbox state, replay protection and delivery acceptance. These services are not supplied by this
formatting module. See the text decision section of the repository's `docs/AUTOMATION-PLAN.md`.

## Stored text decisions

Create a queued decision with `POST /v1/tenants/:tenantId/text-decisions` using the SDK input
shape above and `proposals:write`. The tenant and principal must be existing UUIDs in your project
and environment. Keep the same request ID, timestamps and complete input when retrying; an exact
retry returns the stored request, while changed content returns `text_decision_conflict`.

Read it with `GET /v1/tenants/:tenantId/text-decisions/:requestId` (`proposals:read`). Cancel an
unanswered request with `POST /v1/tenants/:tenantId/text-decisions/:requestId/cancel`, sending
`{ "bindingHash": "<stored packet hash>" }` and `proposals:write`. Successful responses are not
cacheable. The generated reference includes complete request bodies and errors.

These routes store decisions. To deliver one, first obtain the user's consent and create a phone
challenge with `POST /v1/tenants/:tenantId/principals/:principalId/phone-challenges`, supplying
`{ "phone": "+15555550123", "consent": true }`. Enter the received code through
`POST /v1/tenants/:tenantId/principals/:principalId/phone-challenges/:challengeId/verify` with
`{ "code": "123456" }`. Both require `principals:write`; never guess or display the verification code
from server state. Phone possession binds a contact, not authority to purchase.

Read the masked contact through `GET /v1/tenants/:tenantId/principals/:principalId/phone-contact`;
DELETE the same route to revoke it. Send an existing request with
`POST /v1/tenants/:tenantId/text-decisions/:requestId/send`, its exact `bindingHash` and
`proposals:write`. New deliveries can include the full question, choices, prices and expiry directly
in the SMS, with a distinct numeric code for each option. Reply with the exact code shown, for example
`1001`. The transport binds that code to the stored request, option, packet hash and verified phone
contact; it never applies a bare `1` or `Yes` to whichever question happens to be newest. Codes are
not intentionally reused during normal retained-database operation. Database restoration requires
the code-allocation recovery gate in Operations before SMS is enabled again.

If the complete prompt (including codes, link and opt-out text) exceeds 1,200 characters, or contains
control characters that could confuse the option layout, delivery retains the private decision
link. Previously sent link-only messages also keep their original behavior. The project key never
receives the link token. Opening a link records no selection; the page offers the original numbered
or Yes/No buttons. Do not forward private links. Older mobile clients may still describe the link
path; direct codes are read from the text itself, not entered into the companion app.

STOP blocks texts across projects; START requires fresh phone verification and does not reactivate
old links or option codes. HELP explains how to use the option code or link shown in the message.
Providers must be configured as described in Operations; unavailable delivery returns
`text_delivery_unavailable`. The worker retains uncertain sends without blindly retrying. Provider
acceptance is not handset delivery. A late or invalid code does not select an option from a newer
question. Duplicate provider callbacks do not create another selection.

Selections remain distinct from an authorized case decision. Revalidate the exact plan, current
quote, authority, principal and account before dispatch and independently verify the outcome.
Account/device integration and the existing case-decision handoff still need acceptance. There is
no public project-key route to impersonate a phone reply or approve a purchase. Tenant export and
erasure cover contacts, challenges and decision records while excluding private tokens.

SDK 0.5.0 wraps this flow as `munky.phoneContacts.startVerification`, `verify`, `get` and `revoke`,
and `munky.textDecisions.create`, `get`, `send` and `cancel`. These methods run on a trusted server
with the scoped project key. Verification sends/code submissions are not automatically retried;
a lost response must be resolved explicitly. Idempotent decision requests preserve their serialized
body across network retries. Existing published SDK versions remain available unchanged.

## Declare what your product can do

Create each capability with `POST /v1/capabilities`. A capability is one action with an argument
schema, allowed bindings, idempotency behavior, risk flags and an independent verifier. The
reference includes a complete request example.

Choose who performs it:

- **Your backend:** poll the tenant's work outbox, call your product, and report the attempt.
- **Munky Edge Runtime:** register an installation and declare the adapters and capability IDs
  it can execute. The runtime receives only work that matches its tenant and ready adapters.

For verification, declare either `PRODUCT_EVENT` or `SIGNED_HTTP_READ`. A product event must
show the authoritative state change. A signed read must query that state independently of the
performer's report. Register its connection and allowed operations before using it. Production
and staging require HTTPS; development can use loopback HTTP. The verifier URL must be reachable
from the MUNKY server, not just from your browser. A loopback provider works when the API and provider
run on the same host; a hosted API cannot reach a service on your laptop through `localhost`.

## Learn, review and perform

1. Send real native events with stable IDs, timestamps and correlation IDs.
2. Read unmapped types, episodes and learned jobs. Run `/learn` when an immediate pass is needed.
3. Supply human evaluation labels and inspect `/accuracy`; missing evidence does not pass a gate.
4. Show the exact proposal in your product and obtain an authorized decision bound to its plan hash.
5. Approve a trial with its capability set, case limit and expiry. A project key by itself is not
   a person's approval: use a registered delegated-principal signing key and the SDK's authority helpers.
6. Present a separate case approval for each customer-facing or money-moving case.
7. Claim work, perform it idempotently, and report `performed`, `refused` or `failed`.
8. Read the receipt after Munky checks the independent event or signed read.

`performed` is not success. A successful receipt requires verification. Unknown targets, missing
inputs and changed state become work uncertainties; the runtime must not guess a replacement.
An authorized decision must bind the exact uncertainty and its current `bindingHash`.

Proposals, trials, case decisions, work, receipts, corrections, labels and learning feedback all
have public routes. Work metrics describe jobs and checked outcomes, never rankings of people.
Stop a trial through its stop endpoint; revoking a runtime prevents it receiving further work.

## Register and use an approval-signing key

A project key authenticates your backend. A separate Ed25519 signature binds an authenticated
person's decision to one exact resource. Create the signing key on your trusted server:

```sh
umask 077
openssl genpkey -algorithm ED25519 -out munky-approval-private.pem
openssl pkey -in munky-approval-private.pem -pubout -out munky-approval-public.pem
```

Keep the private file in your secret manager, outside source control. In the developer console,
open Keys, select the matching environment, and use **Approval-signing key** to register a unique
key ID and the public PEM. Project owners/admins register this trust relationship through their
console session; a project API key cannot establish its own human approval authority.

Create the principal with explicit permissions such as `case.approve`, narrowed to supported
capability IDs. Role labels alone grant nothing. Your backend must authenticate its user, resolve
their tenant/principal, display the exact pending decision, and persist their explicit response
before signing. Do not take principal IDs, approval objects or an approved flag straight from an
untrusted browser request. Re-read the pending approval and confirm the displayed plan is unchanged.

The following server helper signs that already-authenticated decision. Its arguments come from
trusted server state; it is not an unauthenticated approval endpoint:

```js
import { randomUUID } from "node:crypto";
import { signDelegatedPrincipalAssertion } from "@munky/sdk";

function signCaseDecision({ projectId, environment, principalId, approval, keyId, privateKey }) {
  const issuedAt = Math.floor(Date.now() / 1000);
  return signDelegatedPrincipalAssertion({
    keyId,
    privateKey,
    claims: {
      assertionId: randomUUID(),
      projectId,
      environment,
      tenantId: approval.tenantId,
      principalId,
      issuedAt,
      expiresAt: issuedAt + 120,
      operation: "case.approve",
      authorizationScope: { capabilityIds: approval.capabilityIds },
      resourceType: "case-approval",
      resourceId: approval.approvalId,
      bindingHash: approval.planHash,
    },
  });
}
```

Pass the resulting string as `principalAssertion` to
`munky.caseApprovals.approve(approval.approvalId, { principalId, principalAssertion,
planHash: approval.planHash, reviewMinutes: 0 })`. This requires `trials:write` on the project key
and `case.approve` on the principal. Use the actual review duration if you collect it; zero is the
default. Decline uses `case.decline` and `munky.caseApprovals.decline`. Proposal approval uses its
own resource type, operation, plan and request schema from the reference; never reuse a case token.

Assertions expire, may live at most five minutes, and are consumed once. Wrong tenant, principal,
resource, capability, plan, environment or key fails authorization. If a response is lost, read the
approval/work state before retrying; do not invent another approval to bypass replay protection.
To rotate the signing key, register a new key ID, move signing to it and revoke the old key through
`POST /setup/assertion-keys/:keyId/revoke` using the authenticated console session and an
`environment` form field. This is a console operation, not a public project-key endpoint.

A selected phone preference is not this case decision. The complete phone-to-case handoff still
needs acceptance. Only a later independent product event or signed read can prove the action succeeded.

## Run Edge beside an application

The SDK exports `OfficialEdgeRuntime`, `createEdgeRuntimeClient` and
`EDGE_RUNTIME_PROTOCOL_VERSION`. Register through `munky.edgeRuntimes.register(tenantId, input)`.
Registration returns a runtime credential once. Store that credential on the device and persist
rotations through the client's `onCredentialRotated` callback. Keep the project key on your server.

Registration declares the platform, runtime version, observation adapters, execution adapters,
capability IDs and uncertainty channels. An observer supplies projected event envelopes. The
runtime handles health, heartbeat, assignment checks, observations, reports and uncertainty.
Adapters perform the actual actions.

Available execution adapters include recorded browser procedures, connector calls and MCP tools.
Configure only adapters your integration actually implements. Browser procedures require their own
authenticated context, explicit origins, structural checkpoints and an independent result check.
They do not attach to a person's existing browser profile. Connector and MCP adapters require
explicit call ports and allowed operations. Registering an adapter is not proof that every app
or task is supported. Current action adapters require attended execution declarations.

Frame escalation is an optional cloud operation: the runtime sends one redacted PNG to the
configured provider. It requires declared observation support, a supported prompt version and
an available tenant allowance. It is not part of fully local processing. The local model and
local core are separate Munky-owned runtime components; cloud API availability does not establish
offline availability for an integration.

### Device enrollment and renewal

Your backend first authenticates the customer using your own login, checks their workspace membership,
and resolves their tenant mapping. It then calls `munky.edgeRuntimes.register` with a random stable
installation ID and the adapters that installation actually supports. Do not accept a tenant ID or
capability list from a device without checking it against the signed-in customer's permissions.
Send the returned registration only to that authenticated device over TLS; never send the project key.

The device uses the browser-compatible entry and stores credentials in its native secure store:

```js
import { createEdgeRuntimeClient } from "@munky/sdk/edge-runtime";

const device = createEdgeRuntimeClient(registration, {
  baseUrl: apiOrigin,
  onCredentialRotated: async (credential) => {
    await secureStore.save(credential); // resolve only after durable storage
  },
});
await device.heartbeat({
  observedAt: new Date().toISOString(),
  runtimeVersion: appVersion,
  state: "online",
  adapterHealth: [],
  activeAssignmentIds: [],
});
```

`registration`, `apiOrigin`, `appVersion` and `secureStore` are supplied by your application. Implement
adapters and report their actual health before requesting work. Heartbeat may replace the 24-hour
credential during its final six hours; keep heartbeats running while online. After the credential
expires, authenticate with your product backend again and re-enroll the installation. Do not retry
an expired key indefinitely. A lost enrollment response requires authenticated reconciliation; the
registration request can rotate credentials and is not an exactly-once delivery promise.

A runtime key cannot call project administration routes. Device removal calls
`munky.edgeRuntimes.revoke(runtimeId)` from your backend and deletes the local stored credential.
Project/environment checks also apply to revocation. Offline revocation takes effect at the cloud
boundary when the device next contacts it; it cannot retroactively stop a disconnected local action.
Hosted account/workspace APIs below provide the managed path. Existing clients calling `/product/v1/*`
still need to migrate; these endpoints are not aliases for the old routes.

## Embed the local runtime

Download `https://api.munky.sh/runtime/munky-local-runtime-0.14.0.tgz` and its matching `.json`
checksum manifest. Pin the SHA-256 in your application build and verify it before extracting.
The [local runtime guide](LOCAL-RUNTIME.md) describes the private launch protocol and required
Node/Postgres toolchain. This archive contains Munky-owned services, not model weights or an OS
installer. Products consume the archive without building Munky's server source.

## Verify callbacks and retry safely

Use `verifyMunkyRequest` for signed connection calls and `verifyMunkyWebhook` for webhooks. Verify
the original request bytes before parsing JSON. Supply a durable nonce replay check and enforce
the timestamp window. Process repeated delivery IDs idempotently.

The current wire protocol retains versioned `attesko-http-1` and `x-attesko-*` signing fields for
compatibility. These are protocol identifiers, not a different API owner. Use the SDK's verifier
rather than renaming headers. A later protocol version must change both signer and verifier.

For 429 responses, honor `Retry-After`. Retry transient transport failures with a bounded delay;
reuse the operation's idempotency key. Do not retry an ambiguous effect as a new action. A changed
plan, expired approval, revoked principal or mismatched tenant needs a new valid decision.

## Export and remove customer data

Tenant export and audit export return data and evidence without credential material. Tenant erasure
is idempotent and returns deletion counts plus an explicit list of retained records. Audit evidence
and database backup retention are separate from active tenant data; inspect the returned manifest.
Never infer that deleting an application user also deleted a Munky tenant.

## Integration acceptance

A working integration must demonstrate signup and key issuance, tenant isolation, event projection,
capability registration, an authorized decision, one actual effect and an independently verified
receipt. Test retries, revoked access and a second tenant attempting to read the first tenant's data.

A fake provider or synthetic event fixture validates a contract. It does not establish general app
autonomy, customer deployment or a qualified task model. The clean-repository production cutover and
public-domain acceptance are still in progress.


## Mac background tasks and mobile interruptions (SDK 0.7.0)

The Mac runs supported work through its enrolled runtime. Your backend can read pending runtime
interruptions without sharing a runtime key with the phone:

```js
let cursor;
do {
  const page = await munky.workUncertainties.list(tenantId, {
    status: "open", limit: 20, ...(cursor ? { cursor } : {}),
  });
  // Render the structural reason using your app's plain-language copy.
  // Check expiresAt; "open" alone does not prove the request is still answerable.
  showInterruptions(page.uncertainties);
  cursor = page.nextCursor;
} while (cursor);
```

This requires the backend's `uncertainty:decide` project scope. Authenticate the product user and
check their current workspace membership and decision permissions before returning any inbox data.
Project keys never belong in mobile apps. Cursors are opaque pagination positions bound to the
project, environment, tenant and status filter. Restart from page one when refreshing; newly arriving
items can precede an existing cursor. The inbox is read-only and never claims work or grants approval.

The runtime raises a structured uncertainty while holding an active assignment. The engine holds
that work. Show the exact task/account and reason; use the existing signed principal assertion and
`workUncertainties.resolve(id,{principalId,principalAssertion,bindingHash,resolution})` for retry or
cancel. A retry releases the unchanged held work for a fresh assignment; it does not change the plan,
renew an expired approval or establish that login succeeded. Each hold has its own identity; a
delayed retry/cancel for a previous hold cannot release or cancel a later one. The runtime must recheck provider
identity/session and preconditions before performing any effect. A new target, scope or purchase
quote needs a new exact decision. A performed report still requires independent verification.

Recommended product flow: Mac reports login needed → backend inbox → authenticated mobile decision
screen → provider's real authentication → Mac verifies connection → fresh assignment → verified
receipt. Opening a notification is never a decision. A mobile approval cannot satisfy a third-party
passkey/Touch ID challenge by itself. The secure screen must handle denial, expiry and cancellation.

Current implementation: interruption storage, public inbox, signed resolution, scoped runtime work
and independent receipts. Not yet implemented: APNs delivery, Apple passkey handoff, general
persistent browser-login management or complete Namecheap/Railway/email connectors. SDK browser
execution currently uses isolated contexts, not a user's automatically shared Safari session. Build
those connections explicitly; do not describe an adapter interface or engineer-operated browser as
MUNKY OS completing a task. Native permissions and the app's visible task/stop controls remain client
responsibilities. Background execution is unobtrusive, not hidden from the user's task history.


## iOS companion identity and decisions (SDK 0.8.0)

The companion uses the same application account as the Mac, with a separate application session
and mobile credential. It never receives the Mac runtime key, browser cookies, provider secrets
or the integration's project key. A mobile device is not an execution runtime.

A trusted backend calls the routes below with its project key (`application-workspaces:write`)
and the user's `x-munky-application-session`. MUNKY OS clients use the configured native gateway
at `/v1/apps/munky-os` instead: replace `/v1/application-mobile-devices` with `/mobile-devices`
and `/v1/application-workspaces` with `/workspaces`. The gateway supplies the project key.

| Method and route | Purpose |
| --- | --- |
| `POST /v1/application-mobile-devices` | Register or rotate this installation's mobile credential. |
| `GET /v1/application-mobile-devices` | List this account's phone registrations, including revoked devices. |
| `GET /v1/application-mobile-devices/current` | Validate the session and mobile credential together. |
| `DELETE /v1/application-mobile-devices/:deviceId` | Revoke the phone credential and remove its stored push token. |
| `GET /v1/application-workspaces/:workspaceId/mobile-decisions` | Read up to the latest 50 decisions addressed to this workspace member. |
| `GET /v1/application-workspaces/:workspaceId/mobile-decisions/:requestId` | Read one exact decision addressed to this member. |

Registration body:

```json
{
  "installationId": "324f888c-7f83-4096-a1bd-d2ad46164ecf",
  "displayName": "My iPhone",
  "platform": "ios",
  "appVersion": "0.1.0"
}
```

Keep a stable installation UUID in the phone's Keychain. Registration returns
`{device,deviceToken}`; save the `mmob_` token in Keychain and never log it. Re-registering the same
account and installation returns the same device ID and invalidates the previous token. Tokens
are returned only on registration; list/current responses cannot recover them. Registration for
another account does not reuse the first account's identity. A valid new sign-in may register a
previously revoked installation again; revocation is not a permanent installation ban.

Current-device and decision reads also require `x-munky-mobile-device: mmob_...`. Both credentials
must belong to the same project, environment and account. An expired or revoked application
session fails even when the mobile token is valid. Device revocation invalidates that mobile token
across sessions. Changing accounts must discard the old session and mobile credential locally.
A missing, rotated or revoked phone token returns `401 mobile_device_invalid`; an unavailable
managed device returns `404 mobile_device_not_found`. An inaccessible exact decision returns
`404 mobile_decision_not_found`, and absent membership returns `404 application_workspace_not_found`.

An optional registration `push` object contains an APNs hexadecimal `token` and `environment`
(`sandbox` or `production`). Push tokens are encrypted at rest and never returned. Omitting `push`
on re-registration clears its prior token. Device responses report
`push: {registered: boolean, delivery: "not-configured"}`: token storage is implemented, APNs
sending and delivery acceptance are not. Request notification permission in context; a user can
use the companion without push permission.

Decision responses contain the exact request ID, question, context, numbered options, expiry,
binding hash, state and selection. They always report `canAnswer: false` and
`answerChannel: "sms-secure-link"`. Show the details read-only and direct the user to the secure
link in the existing decision text. There is no unsigned mobile answer endpoint or approval on
notification tap. A preference does not authorize a purchase, and a phone approval cannot satisfy
a provider's passkey challenge. Decisions belonging to another principal in the same company are
not returned. The list is limited to 50 without pagination; use the exact request route for older
known requests. Mobile pairing and read-only decisions do not establish DNS execution, APNs
notification delivery or an installed phone's end-to-end acceptance.

## Website task briefs and DNS evidence assessment

The source contracts now include `WebsiteTaskBriefSchema` and
`WebsiteProviderObservationsSchema`. `assessWebsiteTask` is a pure, advisory engine module; it has
no HTTP route, cloud persistence or live provider connector. It is not yet included in a newly
published SDK. Keep the user's goal and background on their Mac and send only a task they explicitly
select. Free text is untrusted context, never permission to spend or alter other systems.

A brief contains `goal`, `background`, registered-zone `domain`, `railwayProjectUrl`,
`railwayServiceUrl`, and `domainScope` (`root`, `www`, or `both`), with optional local `updatedAt`
metadata. IP addresses and numeric TLDs are rejected. Fixed guardrails default when
omitted: no spending, email changes or nameserver changes. Empty draft fields produce missing-context
results. Railway console URLs must select the exact project/service; the selected URL must also
identify the environment. An existing `*.up.railway.app` service URL can instead be resolved by an
authenticated provider read, with the environment selected in the project URL. Do not infer a
registered zone from the last two domain labels: `example.co.uk` requires the observed SLD/TLD.

The connector must authenticate fresh Namecheap and Railway reads before supplying observations.
Each includes provider account ID, read ID, timestamp and exact target. Railway evidence must supply
the actual routing record and verification TXT for every selected hostname, bound to project,
service and environment. Namecheap evidence must include the complete host list, DNS provider,
root-ALIAS capability, SLD/TLD, EmailType, TTL, MX preference and CAA flag/tag metadata. Missing,
stale (over five minutes), future-dated, mismatched or conflicting observations block the review.
Schema validation, `recordsComplete: true`, JSON timestamps and capability strings do **not** prove
provider authenticity. This module cannot promote client-supplied JSON to verified evidence.

A successful assessment returns a review containing the full proposed host list, exact preserved
email configuration, source read IDs, snapshot digest, canonical DNS before/after fingerprints and
a stable idempotency key. It reuses the existing `tools/domain-pilot/dns-plan.mjs` planner; it does
not maintain a second DNS merge algorithm. Existing records are preserved and conflicting routing
records are not silently replaced. `already-configured` only means the supplied DNS snapshot already
contains the required records; it does not prove website availability or certificate issuance.
`executionAuthorized` is always false, and blocked assessments contain no replacement review.

A future executor still needs authenticated re-reads, an exact authorized decision, protection against
concurrent changes, and independent DNS/TLS/HTTP verification. Recheck both the complete provider
snapshot (including EmailType) and canonical record fingerprint immediately before any write; a
request idempotency key is not a provider guarantee against repeated effects.
[Namecheap setHosts replaces omitted host records and supports ALIAS and CAA metadata](https://www.namecheap.com/support/api/methods/domains-dns/set-hosts/).
[Railway requires its actual routing record and verification TXT; root domains need a supported alias mechanism](https://docs.railway.com/integrations/api/manage-domains).

### Local Namecheap and Railway website reads

The local website worker uses authenticated HTTP provider clients in
`tools/domain-pilot/providers/`. Credentials must come from the product's secure storage; these
clients never read an engineer's CLI session or environment. They accept production HTTPS endpoints
only (explicit loopback HTTP transport exists for fixtures), disable redirects and return constant
error codes without provider response bodies. An authenticated read is evidence for a proposed
change, not permission to execute it.

`NamecheapConnector.readZone({sld,tld})` verifies the exact registered domain in the authenticated
account, requires Namecheap-managed DNS, and returns a complete host snapshot with mail mode, MX
priority and CAA metadata. Missing or unsupported metadata blocks the operation. In particular,
Namecheap's published `getHosts` example omits `EmailType`; a response that omits it returns
`namecheap_email_mode_unavailable`. The client does not infer a mail mode from MX records. API access
and the caller's whitelisted IPv4 are prerequisites; browser login alone does not supply them.

`RailwayConnector.readDomains({domain,projectId,serviceId,environmentId,domainScope})` returns
`{observation,statuses}`. Account tokens use the authenticated account ID. Project tokens first
verify their actual project/environment and use that scope as the observation's account identity.
The domain query binds all three target IDs and requires the selected custom domains to exist.
Routing values and TXT verification metadata come from Railway's response, never a guessed service
hostname. New records use a 300-second TTL. Multiple service domains omit the optional unambiguous
service-domain shortcut. Unsupported/missing fields or GraphQL errors stop inspection. This client
does not create custom domains, deploy services or treat certificate status as a verified website.

The Namecheap client's separate `prepare`/`apply` implementation is not exposed as a desktop action.
A consumer must provide exact digest-bound authorization and a durable write-ahead attempt callback
before its single `setHosts` call. It rereads the full snapshot before dispatch, preserves the mail
mode, and performs readback after acknowledged or ambiguous writes. `provider-state-matches` is
provider readback only; independent DNS/HTTPS verification is still required. Ambiguous outcomes
always return `retryAllowed:false`. Fixtures are not live-account acceptance.

Provider contracts checked against [Railway API authentication](https://docs.railway.com/integrations/api),
[Railway domain management](https://docs.railway.com/integrations/api/manage-domains),
[Namecheap getHosts](https://www.namecheap.com/support/api/methods/domains-dns/get-hosts/) and
[Namecheap setHosts](https://www.namecheap.com/support/api/methods/domains-dns/set-hosts/).
The TXT prefix and verification host handling also follow the
[official Railway CLI domain implementation](https://github.com/railwayapp/cli/blob/master/src/commands/domain.rs).
