# Trusted object behaviors The optional `packages/behavior` runtime is integrated with `Simulation`. A trusted module can react to an interaction, update its declared state, change permitted components on its own entity, and emit events in one ECS transaction. The editor and player now load the pinned `engine.material-toggle@1` module for authored Touch glow interactions. Shared rooms execute this module on the relay and replicate its public appearance. The installed `engine.material-script@1` adapter now adds [restricted custom responses](RESTRICTED_BEHAVIORS.md); uploaded JavaScript and a general shared script host remain future work. ## Try Touch glow 1. Run `npm run dev:editor` on `codex/engine-foundations` and open the editor URL printed by the launcher. Keep its private key private. 2. Create or select a static object without another interaction. Detach a linked arrangement before editing its behavior. 3. Choose **Behavior… → Touch glow**, set the prompt, active color and glow, then **Preview active finish**. This preview shows the selected material; it does not run the script. 4. Choose **Save changes**, then **Apply to shared room**. Open the development room in two separate browser profiles. 5. Walk within the configured reach and press **E**. Either traveler can toggle it, and both see the same finish. 6. Change its active color, save and apply while it is lit: it stays lit with the new color. Remove the behavior, save and apply to restore the base finish. Durable Undo restores the binding; a removed-and-recreated binding starts inactive. Glow affects the surface; it does not cast local illumination. Use the local light editor to light nearby geometry. Default reach is two metres from the authored interaction point, which follows parent transforms. Up to 16 static bindings are supported per world; moving objects, story-controlled materials and arbitrary script code are excluded from this first creator control. The world upgrades to format 7 (`behavior-world.7`). Inactive and active materials retain immutable content hashes. Authored `MaterialVariants` maps names to those assets; session `MaterialSelection` chooses the current name. Shared packets contain only public selections. The relay checks its admitted traveler pose, runs the pinned module, and persists private execution state, public selection, request receipt and high-water mark together before acknowledging. An elected browser host cannot change behavior selections through other gameplay commits. Live source revisions preserve retained bindings and reset faults only when their own configuration changes. The relay executes installed Touch glow and restricted custom-response interactions synchronously. Custom responses have language-level instruction/allocation limits and engine-owned capabilities; they do not run uploaded JavaScript or shared clock hooks. Typed jobs are available to installed trusted simulation programs as described below. The generic restricted adapter now supports explicit job grants; its saved-room integration, broader creator isolation, arbitrary structural replication and full lifecycle hosting remain open. ## Execution contract Use `defineBehavior` with a stable module ID, a trusted executable revision, a schema-defined state component and defaults, a schema-defined configuration, declared component reads/writes, and synchronous hooks. State components use trusted authority and session or persistent storage. The engine owns runtime metadata and an event outbox; modules cannot access those components or another module's private state through their context. `onSpawn`, `onInput`, `onInteract`, `onTick`, `onEvent`, `onSuspend`, and `onDespawn` are supported hook names. `onJobResult` is delivered only by the runtime job queue. The first dispatch runs `onSpawn` once. `Simulation.step()` dispatches due `onTick` hooks in its behavior phase. Other hooks are explicit host ingress; registering a hook alone does not subscribe it to player input or interactions. Each hook is a separate transaction, including spawn before an interaction. The context provides immutable state/configuration/observations, exact uint64 tick time, fixed dt, seeded randomness, `commands.setState`, `commands.setComponent`, and `commands.emit`. Component writes always target the bound entity. Observations describe the start of the hook and do not change when commands are staged. Retained capability functions expire at return or failure. The required host `authorize` callback receives the entity, module, hook, tick, immutable input event, and components being written. It checks ownership and action preconditions before commit. Throw `BehaviorRejection` for an expected rejection such as insufficient reach: it discards the transaction without faulting the script. Unexpected exceptions, invalid component values, and command-budget failures discard every staged state/component/event/RNG change. Even a capability error caught by the hook invalidates its transaction. Three faults park the binding. Successful calls do not erase its fault history. Identical rebinding, restored checkpoints, and unrelated live world edits preserve that history. Changing its own configuration or explicitly replacing its executable revision resets its faults while retaining state and RNG. `replaceDefinition` requires identical state/configuration schemas; data changes use explicit checkpoint migration. Default limits are 1,024 bindings, 256 hook invocations per tick, 64 commands per hook, 4,096 context operations per hook, and 64 pending events per binding. Event values are bounded to 4,096 JSON characters; state/configuration commands to 16,384. Due ticks run oldest-first with stable entity-ID ties to avoid starvation. Missed ticks do not accumulate a backlog. `deferred` input/event calls must be handled explicitly by the host; the runtime does not silently queue them. The host drains events explicitly and routes them through its authority-aware ingress. Events, sequence numbers, fault metadata, last completed tick, RNG, and custom state are ECS components, so they checkpoint together. Draining multiple bindings is also one transaction. ## Simulation API ```ts const program = { modules: [paintBehavior], authorize: checkAuthority }; const simulation = await Simulation.create(world, undefined, program); const frame = { tick: BigInt(simulation.state.tick), dt: 1 / 30 }; simulation.behaviors!.bind(entityId, paintBehavior.id, configuration, 123, frame); const result = simulation.behaviors!.dispatch(entityId, 'onInteract', { action: 'paint' }, frame); const events = simulation.behaviors!.drain(frame); const checkpoint = await simulation.checkpoint(); const restored = await Simulation.fromCheckpoint(world, checkpoint, program); ``` The executable functions stay in trusted code. Checkpoints contain a manifest of module revisions, schemas, defaults, capabilities, hooks, cadence, and runtime limits. Behavior-enabled simulations use `darkwind.simulation.behavior.1`; simulations without a program retain profile 7 and its existing replay hashes. Restoring a behavior checkpoint requires its matching trusted program. A changed code revision, contract, or budget rejects ordinary restore. `Simulation.migrateBehaviors(world, checkpoint, previousProgram, nextProgram)` performs an explicit upgrade. State and configuration use declared component migration chains. Both old and candidate native runtimes are validated; failed migration leaves the source untouched. Bound module removal, state-component renaming, or adding/removing additional capability component types require a separate entity migration. Same-named additional component versions use their declared migration chains. Optional `program.components` are owned, registered, and pinned in the native manifest. `forkRevision` retains the current program and runtime state across compatible world edits. Lightweight `Simulation.save()` remains the existing player-progress format; it does **not** preserve behavior state. Use native checkpoints for this API. The built-in authored module is selected automatically for worker creation and native recovery. In shared rooms, the relay persists its private state; the browser receives the current appearance again when it rejoins. A solo lightweight reload starts the behavior in its default state. This is trusted JavaScript execution, not a hostile-code sandbox. Cooperative command/context limits cannot interrupt a CPU loop or stop arbitrary globals used by a trusted module. Worker placement, the external watchdog and the separate restricted data interpreter address different parts of that problem. The JavaScript module API itself remains trusted-only. Automatic general event routing is not implemented here. Typed jobs below retain the same trusted-code boundary. ## Verification ```sh node --import tsx --test tests/behavior.test.ts tests/simulation-behavior.test.ts ``` The simulation fixture changes an authored wall's material through a real interaction hook and the production ECS presentation projection. It checks native restore/replay, fault rollback, unrelated live edits, code repair, and explicit schema migrations. Runtime cases cover authority rejection, frozen/expired capabilities, parked-state restoration, queue pressure, exact large ticks, deterministic randomness, and fair scheduling. The authored integration adds `tests/object-behaviors.test.ts`, `tests/behavior-room.test.ts`, and `tests/browser/editor-behaviors.spec.ts` for compiler assets, durable undo, live changes, two-player convergence, duplicate delivery, reach rejection, host handoff, private-state filtering, and relay restart. ## Typed asynchronous jobs Run `npm run demo:jobs` (optionally `-- cygnus`). This terminal SDK example loads an external JSON catalog asynchronously, delivers a typed result on a later simulation tick, and restores the completed state from a native checkpoint. `games/async-observatory/behavior.ts` imports only the public SDK; its host in `tools/cli/async-demo.ts` supplies file I/O. It is an API example, not a new browser scene or a durable shared-room service. The catalog is a small demonstration mapping, not a scientific star catalog. Install `defineBehaviorJob({id, revision, request, result, timeoutMs, execute})` entries in `BehaviorProgram.jobs`. Request/result contracts use `defineComponent` schemas. A behavior declares allowed job IDs in `jobs` and handles `onJobResult`. Inside a synchronous hook, `ctx.jobs.request(id, value)` returns a request ID; `ctx.jobs.cancel(requestId)` requests cancellation. Both stage changes in the same authorized transaction as the hook's state and commands. Failed hooks start no work. Executors receive a frozen validated request, its ID and an AbortSignal; they receive no entity context. Host integrations must provide their own authority policy; the local example intentionally uses an unrestricted operator host. The result event contains `requestId`, `job`, `outcome`, `value`, and `error`. Outcomes are `completed`, `failed` or `cancelled`. Only completed outcomes carry a schema-validated result. The queue records each result before invoking its hook and never delivers on the request tick. Hook commands and result consumption commit together. Deferred delivery remains pending; a rejected/faulted hook consumes the result without retrying external work and retains normal behavior fault handling. Entity handle generations, binding generations, code/configuration identity and capabilities fence old completions. Rebinding/replacing code cancels old work without invoking the replacement behavior with an old result. Defaults are 32 outstanding jobs per world, four running executors, eight result deliveries per tick, 16 retained entries per entity and 4,096 JSON characters per request/result. Program limits can raise the first three to 128/16/32. Installed catalogs allow 32 job types; a behavior can request 16 types. Deadlines are 1–60,000 milliseconds. Finished history is evicted as space is needed. An executor that ignores abort continues occupying a concurrency slot until its promise settles. A synchronous CPU loop still requires the external worker watchdog. Native checkpoints pin job revisions, schemas, budgets and the recovery policy. Completed state survives restore. Unfinished queued work is cancelled on the next pump, rather than replaying potentially non-idempotent effects. Already recorded ready results can be delivered without executing again. Changing the job catalog requires an explicit pending-work migration; ordinary restore rejects it. Code or configuration migrations cancel pending results from the prior binding. Lightweight player saves do not retain jobs. **Acceptance record:** six async cases plus the existing behavior/simulation cases pass (28 total), including rollback, typed output rejection, cancellation/deadline, ignored abort, stale owner/configuration/revision, budgets and native checkpoint restore. `npm run demo:jobs` prints a completed Lyra lookup at tick 2 and identical restored state. Type/import/generated-contract checks pass. The client build passes with its existing large-chunk warnings. **Return during integration/qualification:** finish saved-room/editor installation of the restricted job adapter (the generic interpreter and operator plugin APIs are delivered); define durable shared-host job storage/recovery before enabling external side effects there; qualify the delivered explicit recorded-result replay across checkpoints; exercise ready-but-deferred restore, migration/permission changes, fairness, capacity, device/watchdog and fault matrices. These checks and integrations remain unfinished. No network, worker CPU isolation, external exactly-once effects or PostgreSQL job service is claimed by this increment. The generic restricted adapter and opt-in result recording/replay are described in [Restricted jobs and recorded replay](RESTRICTED_BEHAVIORS.md#restricted-jobs-and-recorded-replay).