Skip to content

Physical-Effect Class

A tool whose effect is a change of state in the physical world (motion, force, energy, a relay, a setpoint written to a controller) declares effect_classification: physical plus a physical_effect descriptor. The rule behind it: no model-reachable tool has an unmediated physical effect. LumenFlow governs what an agent may ask a machine to do. It is never part of a real-time or safety-rated control loop, and the governed path is never an emergency stop.

This page documents what ships today (the declaration and physical host, the evidence chain with fail-closed physical traces, and the governed stop). Mediated dispatch with envelopes and signed policy bundles are not available yet.

createPhysicalToolCapability({
  name: 'arm:move_to',
  domain: 'arm',
  version: '1.0.0',
  description: 'Move the arm',
  pack: 'my-arm-pack',
  physical_effect: {
    resource: 'arm-1', // opaque, host-registered
    operation: 'move_to', // from the host vocabulary
    bounds: { x: { unit: 'mm', min: 0, max: 500 } },
    enums: { frame: ['base'] },
    abortable: true,
  },
  handler: { kind: 'in-process', fn },
});
  • A physical capability is always consequential. permission: read is refused, and nothing can label it non_consequential.
  • The input schema is generated by the kernel from the descriptor: a strict object whose fields are bounded finite numbers or vocabulary enums. You cannot supply your own schema; transforms, defaults and open objects are refused (physical.schema-not-generated).
  • Only a pack-owned capability under a host-registered trusted-host-adapter may declare physical. In a pack manifest that means Pack Contract v2, trust_tier: trusted-host-adapter and an adapter_id, and the tool carries no input_schema.
  • A proposal capability (proposal_effect) stays consequential and can never be physical.

The host registers a closed vocabulary of resources and operations. Each operation may carry risk_reducing (no motion and no energy increase, so no bound parameters), safety_rated_configuration (refused at registration), two_person (denied until quorum approvals ship) and safe_place (a motion that brings the machine to a safe place, such as park, home or retract). Every operation that is not risk-reducing must declare command: leased or command: time_bounded, saying how its controller stops on its own if LumenFlow goes silent; registration refuses one that declares neither (physical.command-not-leased). Each parameter carries the unit the host policy uses; a descriptor bound must use the same unit, and the kernel performs no conversion.

PhysicalHostVocabularySchema.parse({
  resources: {
    'arm-1': {
      operations: {
        move_to: { command: 'leased', parameters: { x: { kind: 'bound', unit: 'mm' } } },
        park: { safe_place: true, command: 'leased' },
        stop: { risk_reducing: true },
      },
    },
  },
});

Physical capabilities register only on a physical host: a Linux host-side kernel process running as a dedicated, non-root OS identity that alone owns the reserved devices. Pass physicalHost ({ config, vocabulary }) to initializeKernelRuntime: start-up then calls verifyPhysicalHost and registerPhysicalCapabilities, which registers nothing unless every check passes. A manifest tool declared physical whose resolved capability is not physical, or whose resolved physical_effect is not canonically equal to the declared descriptor (widened bounds, another enum, operation or resource), refuses start-up (physical.classification-mismatch), and a physical tool with no physicalHost refuses it with physical.host-precondition. Verification checks the platform, effective identity, not-in-a-box, device ownership and mode (no group or other access, no symlinks), relay socket permissions, the in-process handler set, full network and reserved endpoints on allowlists, that subprocess confinement is enforced, and a kernel-run confined reachability probe (the same confinement a tool subprocess gets, with the same network posture) that must not reach a reserved device or endpoint, and that each device is a real device node in a directory only the identity or root can write. The result is sealed: only verifyPhysicalHost produces one the gate accepts, and ToolRegistry.register refuses any physical capability the gate did not admit. macOS and Windows are refused with physical.platform-refused.

The default resolver cannot register a physical tool. It builds a subprocess capability with the manifest’s schema, so a manifest-declared physical tool fails physical.schema-not-generated. A physical host supplies its own toolCapabilityResolver that returns in-process adapter capabilities carrying the physical_effect descriptor and a kernel-generated input schema (generatePhysicalInputSchema). The reserved endpoints are currently given twice: in physicalHost.config.reserved_endpoints and in the reservedEndpoints option of the network confinement port; keep them identical.

Some conditions cannot be verified by the kernel and are listed in every report as host assertions: no workload runs as or can switch to the physical identity, the firewall or namespace rule covers every path to each reserved endpoint, no other identity gains device access later, and root and administrators are trusted.

The egress proxy accepts reservedEndpoints (hostname, IPv4 or IPv6 address in any spelling, or an IPv4 or IPv6 CIDR; optional port). A reserved endpoint is refused at connect time with proxy.reserved-endpoint, before the allowlist and any loopback grant, and again for every address a name resolves to.

With no host physical policy, a physical call that base policy did not deny returns PHYSICAL_POLICY_HOLD. The hold is final: external-content labels cannot turn it into an approval request, and the tool host refuses to dispatch a physical capability on any path until the policy work unit lands. Unlike the spend branch, a physical call never turns a deny into a hold: policy_denied, scope_denied, reserved_scope_denied and deny_overlay_denied stay denies.

Risk-reducing operations (stop, hold, de-energise) are the exception: they are never held, need no approval, skip provenance checks and run while the governed stop is tripped. A base deny from policy or scope still denies them, so a host should never deny a stop.

See the spend policy for the pattern this follows.

Every trace entry is chained to the one before it, and the chain can be signed and timestamped through a host port (see Evidence Store). On the direct allow path a physical call’s evidence fails closed, reusing the consumed-approval path’s ApprovalExecutionEvidenceError and its crashed outcome:

  • Started trace not recorded: the call is refused before policy and dispatch. The error has code APPROVAL_EXECUTION_EVIDENCE and phase: 'pre_dispatch'. An ordinary (non-physical) tool keeps its existing behaviour and still runs.
  • Finished or progress trace, or the output or progress data, not recorded: the failure is never swallowed. The kernel appends a crashed finish for that receipt (policy id kernel.physical.evidence) and raises the evidence error through onTraceError; after a dispatch it is thrown to the caller with phase: 'post_dispatch'.
  • Even the crashed finish cannot be recorded: that resource is held. The next physical call for it first retries the crashed finish for every open receipt; until each has a finish in the trace, the call is refused with PHYSICAL_POLICY_HOLD and reason_code: physical_evidence_unreconciled (the open receipts are listed in details.unreconciled_receipt_ids). After a restart, orphan reconciliation writes the same crashed finish.

The hold is kept by the tool host in memory, per resource. Risk-reducing operations (stop, hold, de-energise) are exempt: a stop whose started trace fails, or whose resource is held, still runs, and a stop whose finish cannot be recorded never throws; the failure is reported through onTraceError and a crashed finish naming the stop is attempted.

Every kernel runtime has a governed stop for its workspace. It is a supervisory stop request for physical calls. It is not an emergency stop and not a safety function: the controller’s safety functions and the hardware emergency stop stay underneath it.

  • Tripping. Any principal may trip it, including the model, through the kernel:stop tool (registered on a physical host). The tool is never held, approved, policy-checked or refused, and malformed input, an already-aborted caller signal or a lost Connected Compute lease still trips. In fact every authenticated kernel:stop request trips the runtime’s stop as the first step, on every host, before anything checks it: the runtime’s execution facade (even while the runtime is closing), the agent runtime’s tool policy, checkpoints and agent loop, Connected Compute, the heartbeat, the task tool command, task replay and MCP tool_execute all trip first. If the request is then refused (a closed runtime, a tampered workspace, an unreadable workspace file, a bad selector, a held approval, a tool outside the catalog, an unknown tool on a non-physical host), the refusal is reported and recorded as usual, and the stop is already tripped. The HTTP tool API and the surfaces MCP server have no stop route. Host code can trip it through runtime.getGovernedStop().trip(...). A kill a Connected Compute runner sees (from Cloud, or from a local supervisor through compute run --control-file) trips it too, wherever the runner first sees it, for every assignment type (local tool, agent, playbook and agent:launch), even before the assignment’s runtime opens, and the kill’s terminal report waits until that trip is recorded. While an assignment’s runtime is open, the kill also trips that runtime at once, so a physical call in flight aborts and the next tool call is refused. A runner that cannot open a governed stop for the workspace at all exits with an error instead of reporting the kill, even if the assignment then fails. A held kill never recreates a workspace removed after it. Cloud does not deliver stops or kills to connected runners yet. Until then, a Cloud kill whose trip is not recorded when the runner ends is lost. If your own layer dispatches tool calls, route a stop through forwardStopRequestFirst before your checks.
  • What a trip does. It aborts every in-flight physical call in this process at once (the adapter’s signal fires), aborts this runtime’s sandbox launches, and then records the trip on the kernel event log. Every stop request is recorded, so a second stop while tripped also stops an operator’s motion in another process. If the trip cannot be recorded, the process stays tripped, raises an owner notice (physicalHost.onGovernedStopNotice) and retries. If the state cannot be read at start-up, the runtime starts tripped. A stop is acknowledged only once it is on the event log: until then the stop tool answers GOVERNED_STOP_NOT_DURABLE (“held in memory, not yet durable”) and the operator command and runtime.getGovernedStop().trip(...) resolve false (the process stays alive for that bounded wait), while the process keeps retrying with bounded backoff, even after the runtime closes. Stop requests are idempotent: if you get no acknowledgement, request the stop again; a repeated request joins the one already held and adds no work. Nothing is inferred at restart, so a trip nobody requests again (the lease-loss trip, or a stop in a turn the agent loop refuses) is lost if the process ends before it is recorded.
  • Your own dispatcher. If you pass your own subprocessDispatcher to initializeKernelRuntime, build it on a KernelLaunchAbortRegistry and pass the same registry as sandboxSubprocessDispatcherOptions.abortRegistry. Without it, start-up is refused: the stop must abort exactly this runtime’s launches.
  • Other processes. Every process on the same workspace re-reads the stop before each physical call, and while a physical call is in flight it checks every 100 ms, so a trip anywhere aborts it within 250 ms by default and never more than 1 s. If the stop cannot be read within that bound (a locked or failing event log), in-flight physical calls are aborted and new ones refused. The bound covers signalling: an adapter that ignores its signal is stopped by its controller’s lease.
  • Non-abortable operations. An operation whose descriptor sets abortable: false is not aborted by a trip; it finishes within its lease and the next call is refused.
  • While tripped. Every physical call is refused with GOVERNED_STOP_TRIPPED, except risk-reducing operations and a safe-place motion the operator runs (below).
  • Lease loss. Pass the run’s execution lease as ToolExecutionOptions.leaseSignal, or mark the signal you already pass with markExecutionLeaseSignal(signal); losing it during a physical call trips the stop.

Resetting the stop, and running a safe-place motion while it is tripped, are operator commands. A model can never reach them; there is no reset tool.

const operator = await runtime.getGovernedStop()!.authenticateOperator(credential);
const grant = await operator.authorizeSafePlaceMotion({ tool_name: 'arm:park', input: {} });
await execution.executeTool('arm:park', {}, selector, { operatorCommand: grant });
await operator.reset('Cell checked and clear');

The runtime authenticates the operator with the host callerAuthenticator, refuses a principal the host declares model-controlled, and asks the approval authoriser’s authorizeOperatorCommand (for governed_stop.reset or governed_stop.safe_place_motion). Without that method, or without a modelControlledPrincipals classifier that implements isModelControlledIdentity (for example staticModelControlledPrincipals), every operator command is refused. Configure both on the runtime: initializeKernelRuntime({ callerAuthenticator, approvalResolverAuthorizer, modelControlledPrincipals }), where approvalResolverAuthorizer.authorizeOperatorCommand decides who is an operator. If a runtime started without them, the stop cannot be reset from it: add them and restart the runtime. The trip survives the restart, and the operator can then reset it. If the latest stop record cannot be read (for example a corrupt record), the workspace stays tripped and reset is refused while the row is the latest; restarting the runtime (which records a fresh trip over it) or any new trip lets an authenticated operator reset, and the corrupt row stays in history. A safe-place grant is single use, bound to one tool and its exact input, and lasts 60 seconds.

A physical adapter sends its controller only leased or time-bounded commands. runLeasedCommand implements the driver side: it sends the command with a lease, renews it at half the lease while the call runs, and when the kernel aborts the call (or a renewal fails) it stops renewing and then sends the controller’s own stop command. A stop that arrives while the command is still being sent is honoured: no renewal is ever sent after it.

async function park(input, ctx, options) {
  const result = await runLeasedCommand({
    signal: options.signal,
    leaseMs: 500,
    controller, // send / renew / stop for your controller interface
    completion: controller.done(),
  });
  return result.outcome === 'completed'
    ? { success: true, data: result.value }
    : { success: false, error: { code: 'STOPPED', message: 'Stopped by the governed stop' } };
}

If the kernel process dies, nothing renews the lease and the controller stops on its own.