Operations, Chains, and Throws
Operations are the durable work context. Chains are workflows with typed steps inside an operation. Throws are replay-inspectable executions of a chain against targets.
Chains have three related forms:
- An interactive chain record owned by an operation and edited through
hovel cli. - A chain template, which is a reusable chain definition with steps but without configured values or target bindings.
- A configured chain file, which represents a chain, its steps, configured string values, selected target bindings, and target config for review, sharing, and one-shot execution.
Interactive editing and persistent representation are separate concerns. chain add <module> remains the fast CLI workflow for building a chain. chain save <file> writes the active chain to a YAML file, and chain load <file> imports or updates a chain from that file. One-shot shell execution throws a configured chain file rather than relying on prompt session state. Target config is essential to configured chain files because it is part of what the operator reviews and confirms before throwing.
The current chain thrower is intentionally small. It executes ordered module steps with string chain and target configuration, persists plans and confirmation records, and records events, artifacts, installed payloads, throw records, and final state produced by the module or provider path. Chain files do not expose ${inputs.*} or ${steps.*} expression syntax today, and they do not support arbitrary expressions, loops, embedded scripting, dynamic graph mutation, or user-defined functions.
Operator-facing chain records are CRUD resources. A chain can be created, selected with chain use, renamed, inspected, listed, deleted, and then thrown against targets selected from the active operation. The active chain is client attachment state: it determines that client's prompt context, default chain for throw, and log topic subscription without changing another client's active chain.
Targets are owned by operations in the operator workflow. A target added in operation context is available to every chain but is not bound to any chain. A target added while alpha is active is still an operation target and is immediately bound to alpha; switching to beta does not make it a beta target until the operator binds it there. Target config follows the target, not the chain. Target sets, also available as target groups, are named operation-owned groups used only to select targets conveniently at plan time. Chain logs remain chain-scoped: chain logs renders the active chain's operation-scoped topic only, and multi-client front ends attached to the same operation and chain observe the same topic.
A throw selects a target source when planning. throw --target <id> is strict and blocks on an incompatible target. throw --target-set <name> and throw --target-group <name> select a named operation target group. Without an explicit selector, an interactive chain throw uses the active chain's bound targets and does not fall back to the whole operation inventory.
Payload-consuming chains are composed from typed capability steps rather than provider-before-exploit ordering alone. A module reports, over JSON-RPC, which step contracts it supports, what each step requires, what it produces, which configuration schema applies, and which prepare-time values it materializes. Hovel loads those contracts into the module catalog, uses them during validation and planning to report typed missing requirements, and executes capability-step chains after confirmation. Persisting an exact contract snapshot at confirmation time, presenting editable prepared values in the review surface, and blocking post-confirmation contract drift are target safeguards rather than current source behavior. Squatter is the first core provider following this model; see Squatter Payload Provider.
Capability-Based Chain Runtime
Chain edges are typed capability matches, not arbitrary JSON handoffs. A step requirement is satisfied by a producer output when the capability type, required attributes, and allowed state match. Hovel owns the core schemas and validates module outputs before accepting them. Modules may add namespaced extension attributes, and a consumer may match on an extension only when it explicitly declares that namespace.
RemoteExecutionCapability
CredentialCapability
PayloadArtifact
PayloadInstance
TransportEndpoint
SessionRef
CleanupHandle
Every capability has a stable id, schema version, current state, producer step, and event history. Capabilities keep mutable current state for operator ergonomics and append-only history for audit. Diagnostic evidence is separate: evidence explains what happened, while capabilities are machine-consumable chain state. Modules may not emit undeclared capability outputs by default; undeclared material must be recorded as diagnostic evidence.
The lifecycle is explicit:
- describe
- prepare
- confirm
- execute
- operate
- cleanup
describequeries live module contracts over JSON-RPC.prepareis pure and local: it validates config and generates planned values, but it does not touch the target or produce final payload bytes.confirmcurrently records the reviewed plan, target set, module or step references, and configuration; freezing contract snapshots and prepared values is planned.executematerializes final artifacts, mutates targets, and records exact hashes and state transitions.operatekeeps sessions attachable, detachable, and reconnectable while installed payload records remain active.cleanupexecutes durable cleanup handles, records verification where possible, and remains retriable until removed, verified, or abandoned.
The target safety model blocks execution if a module reports a different
contract after confirmation; the operator must review the contract diff,
re-prepare if needed, and re-confirm. The current implementation has the
runtime pieces for step.prepare and step.execute,
but generic pre-confirm prepared-value editing and drift blocking remain
pending.
The catalog owns the first planning primitive: it can resolve every enabled
module step against the current capability set and report deterministic
bindings plus typed missing requirements. The same projection is exposed
through module inspect --json so operators and front ends can see
which live JSON-RPC step contracts are ready and which capabilities still need
to be produced.
The first execution primitive is a generic capability step runtime. It executes
an ordered set of module step references, resolves inputs before each step,
calls step.prepare, calls step.execute, records
produced capabilities, applies state transitions, and blocks at the first
missing requirement. This primitive is intentionally not MS17-010-specific; the
MS17-010-to-Squatter path is the representative chain used to prove the model.
Chain files identify both layers explicitly. uses locates the
module package, and optional step locates the JSON-RPC step
contract inside that module. Omitting step on every step
preserves the legacy single-module execution path. Setting step
opts the configured chain file into capability-step execution, and every step
in that file must then name a JSON-RPC step contract, except for the current
Squatter TCP-bind bridge. The approved throw plan records the module
references and callable step references, but not yet full contract snapshots.
A capability-step throw executes the configured ordered steps through the capability runtime rather than the legacy per-module daemon runner. It produces one aggregate run per target. That aggregate run carries the final capability set, diagnostic evidence, logs, and any opened sessions; step-level prepare and execute detail remains available through capability evidence and structured events. One-shot chain-file throws use the same primitive with the configured module catalog and JSON-RPC step runner, so MS17-010-to-Squatter is not a special throw path; it is one ordered capability-step chain.
steps:
- id: exploit
uses: module:ms17-010@v1
step: ms17-010.exploit
- id: install
uses: module:squatter-provider@v1
step: squatter.install_smb
Representative MS17-010 to Squatter Chain
The MS17-010 chain is the representative example for payload and session orchestration. The MS17-010 module demonstrates exploitation and access establishment; it does not speak Squatter's protocol and does not own Squatter sessions.
ms17-010.exploit
produces: RemoteExecutionCapability(connection_bound)
windows.credential.create_local_admin
requires: RemoteExecutionCapability
produces: CredentialCapability(protocol=smb, durable), CleanupHandle
squatter.generate
produces: PayloadArtifact(provider=squatter)
squatter.install_smb
requires: RemoteExecutionCapability, CredentialCapability(protocol=smb), PayloadArtifact
produces: PayloadInstance(provider=squatter, transport=smb-named-pipe),
SmbPipeEndpoint, CleanupHandle, InstalledPayloadDescriptor
squatter.connect_smb
requires: PayloadInstance(provider=squatter, transport=smb-named-pipe),
SmbPipeEndpoint,
CredentialCapability(protocol=smb)
produces: SessionRef(provider=squatter, transport=smb-named-pipe)
The same Squatter provider also supports TCP bind and TCP callback branches.
SMB mode requires a credential capability. TCP bind requires a target endpoint.
TCP callback requires a listener step. If the payload starts but no callback has
arrived, the installed payload record remains installed with
provider-owned callback status in its reconnect metadata; failed refresh or
connect attempts may move it to unreachable.
Prepared Squatter SMB plans allocate neutral random identifiers before confirmation: credential username and password, service name, staged filename, pipe name, transport config, and planned cleanup handles. These identifiers do not include Hovel, Squatter, run ids, or other tool-identifying strings. Hovel evidence stores the full mapping plainly for remediation, including plaintext credentials.
Install success with session connection failure is not a generic failed chain.
Hovel persists the explicit installed payload descriptor, marks the installed
record unreachable, and records warning evidence. The CLI must show
exactly what happened: target, staged path, service name, pipe, plaintext
credential, connection endpoint, exact error, active cleanup handles, and
reconnect/cleanup commands. Downstream steps are blocked only when their
declared requirements need a live SessionRef; steps that require
only a PayloadInstance may continue.
Installed Payload Records During Throws
Installed payload inventory is created only from explicit provider or module records. Hovel must not infer installation from log text, generic artifacts, connection attempts, or the mere existence of a payload binary. Any module may return an installed-payload descriptor, but it must name the provider and payload id that own reconnect semantics.
Hovel core validates the descriptor against the current provider contract, attaches operation, chain, target, throw, run, stamp, and artifact provenance, then writes the current-state row and append-only state event to SQLite. The provider owns opaque reconnect and cleanup descriptors; Hovel owns persistence, handles, audit events, and command/API presentation.
Repeated installs reconcile only with explicit identity. If the provider returns
a stable instance_key or the same payload stamp reports itself
again, Hovel may update the existing record. Without that proof, a successful
install creates a new record even if host and transport look similar.
Operation Attachments
Each implemented CLI client, and each future TUI, REST, or MCP client, attaches to exactly one operation at a time and keeps its own active chain selection.
Concurrency rules:
op use <operation>changes only the caller's attached operation.chain use <chain>changes only the caller's active chain inside the attached operation.chain add,chain config,chain validate, andthrowwithout--chainuse the caller's active chain.target addmutates the operation inventory and, when a chain is active, binds the target to that chain.target bind,target unbind, and chain-contexttarget clearmutate only active-chain target bindings. Operation-contexttarget clearis destructive and requires confirmation.- Saved chain-file throws import or execute the file without moving another client's active chain.
- Mutations to one shared chain are serialized by the daemon and broadcast to subscribers of
operation/<operation>/chain/<chain>/logs. - Different clients may work different chains in the same operation concurrently.
Chain Definition
Current chain files use the flat descriptor shape enforced by
hovel.chain.schema.json and the Go descriptor parser. They do
not contain inputs, phases, or per-step
with blocks. Chain and target configuration are string maps
reviewed before confirmation. The descriptor schema accepts
module:, service:, and provider:
step references, but the current one-shot throw path executes
module: steps.
apiVersion: hovel.dev/v1alpha1
kind: Chain
metadata:
name: ssh-memory-flow
spec:
mode: configured
steps:
- id: survey
uses: module:ssh-survey
- id: exploit
uses: module:ssh-memory@0.1.0
config:
operator.confirmed_lab: "true"
payload.transport: "ssh"
targets:
- id: ssh://10.41.32.2
config:
target.host: "10.41.32.2"
target.port: "22"
Capability-step chain files may add a step field to each
step. A throw that uses capability-step execution must declare
step on every step in the file, except for the current
Squatter TCP-bind bridge step.
A chain template records reusable workflow shape:
- Chain identity. The schema accepts descriptor metadata such as version and description, but current command flows use
metadata.nameas the durable chain identity. - Ordered steps. The schema admits
module:,service:, andprovider:references; current one-shot throws executemodule:references. - Module references with resolved versions when available.
- Configuration schema is resolved from the referenced modules during validation and planning; it is not embedded in the current chain file shape.
- Expected providers, services, listeners, sessions, and artifacts are planning outputs when known, not fields in the current chain descriptor.
A configured chain file must contain enough information to reconstruct the review surface without relying on hidden prompt state. At minimum it records:
- Everything in the chain template.
- Chain-level configured values.
- Target list.
- Per-target configuration values.
Risk review and confirmation metadata live in throw-plan and confirmation records, not in chain files.
Payload plans include selected provider, selected payload query, target-scoped provider configuration, expected format and platform, compatibility facts, and whether pre-throw listener or post-throw connect hooks are required. They do not include generated payload bytes, installed payload handles, runtime session IDs, or provider-side handles; those values are produced after confirmation and recorded as events, artifacts, and installed-payload records.
Current Thrower Support
The implemented thrower supports:
- Sequential steps.
- String chain and target configuration.
- Capability-step input binding through declared module step contracts.
- Per-step status and logging.
- Artifact creation.
- Fact propagation as ordinary step output.
- Cancellation between steps.
- Persisted throw plans and throw records.
- Confirmation records, including prompt confirmations and
--nowbypass confirmations.
review and throw share the same planning and review code path. confirm stops after recording a pre-confirmation. review always displays the reviewed plan, requires the operator to type yes, and records or refreshes the confirmation without starting execution. throw starts execution only after an existing confirmation, an inline typed yes, or an explicit --now bypass has produced a confirmation record.
Eventual Throw Runtime Requirements
The eventual chain runtime should support:
- Sequential steps.
- Parallel steps across targets.
- Conditional branches.
- Step retries.
- Timeouts.
- Cancellation.
- Service startup.
- Service teardown.
- Per-step logging.
- Per-service logging.
- Per-target status.
- Artifact creation.
- Fact propagation.
- Listener and session state.
Productizing PoCs
Hovel exists because productizing proofs of concept is hard. A chain should absorb the work needed to turn a crude one-off script into a repeatable operator workflow:
- Input validation.
- Target normalization.
- Survey and fact collection.
- Payload selection.
- Listener startup.
- Transport selection.
- Error handling.
- Artifact and transcript capture.
- Cleanup.
- Logging.
- Multi-target execution.
Conceptual SSH Memory Chain
This chain is a conceptual example, not a current repository dependency. It uses authenticated SSH as a transport and demonstrates payload delivery/execution in a controlled authorized environment.
The Hovel repository should not need to provide the payload generator, listening post, or target-specific execution components for this example. Those should arrive through providers, managed services, or third-party modules.
Phases:
- service_prepare
- survey
- resolve_payload
- prepare_delivery
- execute
- collect
- cleanup
- service_cleanup
Survey questions:
- What OS is the target?
- What architecture is the target?
- Is
/dev/shmpresent? - Is
/tmpmemory-backed? - Is Python 3 present?
- What shell is available?
- Are required environment variables present?
- What execution constraints exist?
Delivery strategy categories:
stdin_loader
python_ctypes_loader
tmpfs_exec
command_only
Public examples should keep payloads benign and lab-oriented.
Expected outputs:
- Target facts.
- Started services.
- Started listeners.
- Selected strategy.
- Payload metadata.
- Execution transcript.
- Session refs.
- Artifacts.
- Result status.
- Cleanup status.