# Canonical effects: caller-owned execution

RuleEvo records authority, admission and outcome evidence. A dispatch claim is not
remote completion; a timeout is **unknown**, not failed. The REST core, SDK and
MCP use the same service. This quickstart adds no new Google permissions, public
access, billing rate or hosted arbitrary HTTP proxy.

## One sandbox effect

Download `ruleevo.mjs` and `effect-runner.mjs` from `/sdk/` to your trusted Node 22+
backend. Keep the actor key and private-Site service token in your server's secure
environment; never paste them into prompts, frontend code or ledger fields.

The owner first creates an existing instruction/grant with `sandbox.effect`, then
binds that grant to the exact provider/resource/environment. Binding is immutable,
owner-only and only narrows authority; it does not connect a provider. Keep the
grant actor ID and role aligned with its credential.

```js
// Owner-session request, performed once through the authorized owner workflow:
// POST /api/v1/authorities/<grant-id>/bind
// {providers:["sandbox"],resources:["sandbox:effect"],environment:"sandbox"}

import {RuleEvoClient} from './ruleevo.mjs';
import {RuleEvoEffectRunner,createSandboxEffectAdapter} from './effect-runner.mjs';

const control=new RuleEvoClient({
  baseUrl:process.env.RULEEVO_BASE_URL,
  apiKey:process.env.RULEEVO_API_KEY,
  serviceAccessToken:process.env.RULEEVO_SERVICE_TOKEN,
});
const adapter=createSandboxEffectAdapter(); // local synthetic store, no provider call
const runner=new RuleEvoEffectRunner({control,adapter});
const payload={label:'harmless test'}; // retained by your backend, not sent to the ledger
const effect=await runner.propose({
  grant_id:process.env.RULEEVO_GRANT_ID,
  idempotency_key:'persisted-effect-key-001', // one durable key per intended effect
  operation:'sandbox.effect',provider:'sandbox',resource:'sandbox:effect',
  resource_type:'synthetic',environment:'sandbox',amount_cents:0,currency:'USD',
},payload);
const result=await runner.execute(effect.effect_id??effect.id,payload);
// Inspect result.effect.state. result.dispatched means an adapter attempt only.
```

The server persists a stable effect ID, SHA-256 canonical payload digest, existing
authority/grant, action admission, epoch and outcome history. Raw payloads and
provider credentials stay with the caller. Canonical JSON recursively sorts object
keys and preserves array order; the runner rejects undefined, non-finite numbers,
custom objects, excessive depth and payloads above its 64 KiB local limit. It
snapshots caller input before asynchronous admission to prevent mutable-input drift.

`execute()` checks the local payload digest and pinned adapter target before
admission/dispatch. A single winning claim returns `execute:true`; any other
response means no provider write. Completed, failed, dispatched, unknown and
reconciling effects are never blindly dispatched again. Unknown claims or lost
responses must be inspected and reconciled, not assigned another effect ID.

The caller's provider idempotency key remains important. A stable effect and a
single RuleEvo claim do not prove distributed exactly-once execution, provider
stoppage or safe direct calls outside this adapter.

## Pinned-origin HTTP port

`createHttpEffectAdapter` requires fixed `origin`, normalized `path`, write `method`,
`provider`, `resource`, `operation`, `resourceType` and `environment`. Only a trusted
backend chooses these settings and provider headers; nothing reads a URL from an
effect. HTTPS is required outside local development. Redirects are never followed;
429 and 5xx responses remain unknown. No mutation is automatically retried.
Response bodies are streamed up to 1 MiB and canceled above that bound; raw
upstream errors are not copied into evidence. A 15-second fetch signal bounds
the transport request. `failure_class` accepts only the fixed
`confirmed_no_effect` value on a failed observation; other values are rejected.

```js
import {createHttpEffectAdapter} from './effect-runner.mjs';
const adapter=createHttpEffectAdapter({
  origin:'https://YOUR_AUTHORIZED_PROVIDER',path:'/fixed/write',method:'POST',
  provider:'your-provider',resource:'account:fixed',operation:'record.create',
  resourceType:'record',environment:'sandbox',
  headers:{Authorization:`Bearer ${process.env.YOUR_PROVIDER_KEY}`},
  // Supply classifiers only from that provider's explicit, tested API contract.
  // validatePayload: async (effect,payload) => assert fixed tenant/account fields,
  // classify: async ({status,body}) => ({state:'completed',provider_reference:'...'}),
  // lookup: async (effect,payload) => a read-only status/readback observation,
});
```

A 2xx response alone does not prove completion. Without a trusted classifier the
default is unknown, including 202 Accepted. `completed` requires a definitive provider reference. `failed` is accepted only with explicit
`failure_class:'confirmed_no_effect'`; do not infer it from HTTP failure, timeout
or missing readback. Trusted lookup functions must be read-only and use fixed
provider endpoints/least-privilege credentials. Use the optional trusted `validatePayload(effect,payload)` hook to reject mismatched business tenant/account fields before admission/claim. Pinned transport targets do not automatically understand an arbitrary JSON business payload. These hooks are trusted caller
code, not a general endpoint-discovery or security-certification system.

## Drive folder reference port

`createDrivePortEffectAdapter({drive,folderId})` uses your existing caller-owned
Drive port's `getMetadata(id)` and `createFolder({name,parent_folder})`. The effect
must match provider `google_drive`, operation `drive.folder.create`, resource
`drive:<folderId>`, resource type `folder` and sandbox environment by default.
Bind the authority to that exact target. The local payload must be
`{name:'Generated test folder',parent_folder:folderId}`.

The adapter reads fixed-parent metadata before claiming, creates a folder only
after the winning claim, then checks ID, name, parent, folder MIME type and trash
state on readback. A known created ID survives a readback outage as an unknown
provider reference. Reconciliation only reads that ID; it never repeats creation.
If a write response lost the ID, retain unknown until the operator finds reliable
provider evidence. Never create another folder just to clear uncertainty.

These are caller/bridge-recorded observations, not Google-signed attestations.
Direct Google credentials can bypass the adapter. The legacy `RuleEvoDriveGuard`
and `/drive/jobs` APIs remain intact; old jobs are not backfilled into effects.

## Failure injection and reconciliation

`createSandboxEffectAdapter({mode,store})` supports `complete`,
`timeout_before_effect`, `timeout_after_effect` and `confirmed_failure`. It rejects
live effects. Supply the same Map to model a restarted synthetic caller; the Map
is not production durable storage. `lookupUnavailable:true` models readback outage.

After an unknown outcome, use `runner.reconcile(effectId,originalPayload)`. It first
asks the service for reconciliation permission, then invokes only `adapter.lookup`
and records a new outcome observation. It never invokes dispatch. Exceptions or
unclassifiable observations stay unknown. Observation keys make report retries
recognizable; the SDK never automatically repeats a mutation. If reporting itself
fails, retain the effect ID and reconcile rather than dispatching again.

The UI's “Request provider readback” only opens reconciliation state. It has no
provider credentials and cannot confirm completion by itself.
For a pending local admission, “Check local admission” may return no readback
permission; the UI shows the actual service notice instead of implying a provider
lookup. The combined reconciliation filter includes admitting, dispatched, unknown
and reconciling states. Its revoked-authority filter uses only the loaded, bounded
authority projection. Withdrawal denies future protected work but cannot undo
completed effects; admitted/in-flight work may finish and unknown stays unresolved.

## Evidence and measured operations

`control.effectEvidence(id)` exports a finalized signed bundle for completed, failed or denied effects. Unknown/in-flight records remain inspectable and can be exported unsigned; they are not silently finalized to clear uncertainty. Verify using the existing
`verifyReceipt(receipt,previouslyTrustedPublicJwk)` helper; do not treat the
receipt's embedded key as independent issuer proof. A valid signature authenticates
recorded evidence, not the truth of an actor report or external cessation. Legacy
signing-key custody/rotation limits remain documented in the architecture.

The Effects view filters loaded records by state, actor, provider and operation,
highlights unknown/reconciling work, exposes admission snapshots and signed export,
and displays measured service metrics with sample bounds. Loaded rows are not a
lifetime success denominator; no money saved, time saved or universal stop claim
is derived. Starter $10/10,000/$0.001 and historical contracts are unchanged.

Compensation is a separate newly authorized effect, not rollback of a completed
external operation. Scope, expiry, budget and provider support must authorize it.
