Skip to content

【WIP】 feat(hooks): add container lifecycle hook system with host-exec support - #1160

Open
SuGuoXiong wants to merge 4 commits into
boxlite-ai:mainfrom
SuGuoXiong:feature-hook
Open

【WIP】 feat(hooks): add container lifecycle hook system with host-exec support#1160
SuGuoXiong wants to merge 4 commits into
boxlite-ai:mainfrom
SuGuoXiong:feature-hook

Conversation

@SuGuoXiong

@SuGuoXiong SuGuoXiong commented Aug 6, 2026

Copy link
Copy Markdown

Overview

Adds a container lifecycle hook system that lets users inject custom logic at
key points in the box lifecycle — create, start, stop, exec — without forking
the runtime or wrapping every API call.

Before:  all lifecycle transitions are opaque
After:   users register hooks at 7 interception points; each hook runs
         synchronously with configurable timeout, priority, and error policy

Hook Points

Runtime.create() ── PostCreate ── Configured ── start()
  ┌─ VM boots ── PreStart ── Container.Start ── PostStart ── Running
  │   (exec)
  │     PreExec ── Exec RPC ── PostExec
  │
  └─ stop()
       PreStop ── Guest.Shutdown ── PostStop ── Stopped

Architecture

Hook (declarative config, serde)  ──►  HookContext (JSON stdin / env vars)
Hook trait (in-process Rust impl) ──►  HookRunner::fire()
                                          │
                            ┌─ HostExec  ─ tokio::process::Command + timeout
                            ├─ GuestExec ─ Execution RPC (deferred to Phase 2)
                            └─ Trait     ─ hook.on_<point>(ctx).await

What ships

Module Details
boxlite::hooks Hook, HookPoint (7 points), HookAction
(HostExec/GuestExec),
HookCondition + ExecHookTrigger, HookErrorPolicy
(Continue/Fail/Retry),
HookContext (JSON stdin + $BOXLITE_* env-var
substitution for 11 variables),
HookRunner::fire(), Hook trait (in-process, not
persisted)
Integration fire() wired into BoxImpl::{start, stop, exec} and
RuntimeImpl::create_box()
CLI --hook, --hook-json, --hook-arg + 8 modifier flags
(--hook-timeout, --hook-on-error, etc.)
Tests 70 unit tests (serde round-trip, substitution, ordering,
condition eval, timeout kill, retry)

Example — auto-snapshot after pip install

let hooks = vec![Hook {
    name: "post-install-snapshot".into(),
    point: HookPoint::PostExec,
    action: HookAction::HostExec {
        program: "boxlite".into(),
        args: vec!["snapshot".into(), "$BOXLITE_BOX_ID".into(),
"--name".into(), "latest".into()],
        env: vec![],
    },
    condition: Some(HookCondition::ExecResult {
        trigger: ExecHookTrigger::CommandMatches("pip*".into()),
    }),
    ..Default::default()  // timeout_secs=30, on_error=Continue
}];
$ boxlite run python:3.12 \
    --hook auto-snapshot:post-exec:host:boxlite \
    --hook-arg snapshot \
    --hook-arg '$BOXLITE_BOX_ID' \
    --hook-arg --name \
    --hook-arg latest \
    --hook-condition-exec-result auto-snapshot=cmd:pip* \
    -- python agent.py

Deferred to later phases

  • GuestExec hooks (Phase 2)
  • Snapshot/restore hooks (Phase 3)
  • Python/Node/Go/C SDK surfaces (Phase 4)

Design: [docs/architecture/container-lifecycle-hooks.md](./docs/architecture/c
ontainer-lifecycle-hooks.md)
Test plan: [docs/architecture/container-lifecycle-hooks-alpha-tests.md](./docs
/architecture/container-lifecycle-hooks-alpha-tests.md)

Summary by CodeRabbit

  • New Features
    • Added configurable container lifecycle hooks for create, start, stop, exec, snapshot, and restore operations.
    • Supports host or guest commands, priorities, conditions, retries, timeouts, error policies, environment variables, and context-based substitutions.
    • Added CLI options for configuring hooks when creating or running containers.
    • Hooks can observe lifecycle and execution results without interrupting normal output.
  • Documentation
    • Added comprehensive lifecycle hook design specifications and alpha testing guidance.

@SuGuoXiong
SuGuoXiong requested a review from a team as a code owner August 6, 2026 12:05
@boxlite-agent

boxlite-agent Bot commented Aug 6, 2026

Copy link
Copy Markdown

📦 BoxLite review — couldn't complete

claude exited 1

stdout:
{"is_error":true,"duration_api_ms":0,"num_turns":1,"stop_reason":"stop_sequence","session_id":"aca18d93-4a50-4005-be58-fc9840ff49d6","total_cost_usd":0,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{},"permission_denials":[],"terminal_reason":"api_error","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subtype":"success","api_error_status":403,"result":"Your organization has disabled Claude subscription access for Claude Code · Use an Anthropic API key instead, or ask your admin to enable access","type":"result","duration_ms":821,"uuid":"0863d203-c363-4715-bc15-b200f42d61a5"}

stderr:
<empty>

powered by BoxLite

@cla-assistant

cla-assistant Bot commented Aug 6, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

1 similar comment
@cla-assistant

cla-assistant Bot commented Aug 6, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a synchronous container lifecycle hook system with declarative and trait hooks, host and guest execution, context substitution, retries, fire counts, lifecycle integration, CLI configuration, tests, and architecture documentation.

Changes

Container lifecycle hooks

Layer / File(s) Summary
Hook contracts and runtime context
docs/architecture/container-lifecycle-hooks*.md, src/boxlite/src/hooks/mod.rs, src/boxlite/src/hooks/context.rs, src/boxlite/src/hooks/substitution.rs, src/boxlite/src/hooks/fire_count.rs, src/boxlite/src/lib.rs
Defines hook actions, lifecycle points, conditions, error policies, execution context, variable substitution, fire-count storage, public exports, and test specifications.
Hook execution and policies
src/boxlite/src/hooks/runner.rs, src/boxlite/src/hooks/host_exec.rs
Adds hook collection, ordering, filtering, host and guest dispatch, retries, timeouts, error handling, trait callbacks, and execution tests.
Runtime lifecycle integration
src/boxlite/src/runtime/options.rs, src/boxlite/src/runtime/rt_impl.rs, src/boxlite/src/litebox/box_impl.rs
Stores hooks in BoxOptions and fires hooks during box creation, start, exec, and stop operations.
CLI hook configuration
src/cli/src/cli.rs, src/cli/src/commands/create.rs, src/cli/src/commands/run.rs
Adds hook definitions and modifiers to CLI create and run flows, with parsing, validation, duplicate-name checks, and option wiring.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: dorianzheng

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the container lifecycle hook system and host-exec support, which are the main changes.
Description check ✅ Passed The description clearly covers the scope, call flow, architecture, implementation, examples, tests, and deferred features.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/architecture/container-lifecycle-hooks.md`:
- Around line 5-17: Revise the lifecycle-hooks documentation to describe only
the currently implemented create, start, stop, and exec host-hook phases. Remove
GuestExec, snapshot, and restore from the supported behavior and move those
deferred capabilities into a future-work section.
- Line 1: Update the PR description to include end-to-end call graphs showing
the container lifecycle hook flow before and after the change described by
“Container Lifecycle Hook System.” Do not add a “Fixes #<n>” reference, since
this is a feature change.

In `@src/boxlite/src/hooks/fire_count.rs`:
- Around line 24-28: Update FireCount persistence around load and its increment
path so every (box_id, hook_name) count increment is written to the box-state
storage, and restore persisted counts when the BoxImpl runner is created. Ensure
cache invalidation or runtime reconstruction reloads the stored values instead
of resetting fire_count, while preserving load’s existing key-based behavior.

In `@src/boxlite/src/hooks/host_exec.rs`:
- Around line 47-74: Implement staged timeout termination across the hook
execution flow: in src/boxlite/src/hooks/host_exec.rs lines 47-74, update the
host process setup and timeout handling to terminate the appropriate process
group with SIGTERM, wait 5 seconds, then use SIGKILL if needed after execution
has started; in src/boxlite/src/hooks/runner.rs lines 324-365, retain the
cancellation handle or execution state returned by exec and invoke that
termination path on timeout rather than only returning an error; in
docs/architecture/container-lifecycle-hooks.md lines 232-234, retain the
staged-kill documentation only once implemented; and in
docs/architecture/container-lifecycle-hooks-alpha-tests.md lines 452-459, extend
the timeout test to verify process/group termination and orphan cleanup.

In `@src/boxlite/src/hooks/mod.rs`:
- Around line 157-165: Update hook deserialization around the on_error field and
HookPoint::default_on_error() so an omitted on_error policy remains
distinguishable from an explicitly configured policy. Resolve omitted policies
using the current HookPoint default, preserving explicit HookErrorPolicy values
and the documented Fail defaults for pre-start, pre-exec, pre-snapshot, and
pre-restore.

In `@src/boxlite/src/hooks/runner.rs`:
- Around line 64-102: Update the hook execution loop to resolve metadata through
the `HookOrTrait` value rather than accessing `hook.name` or `hook.on_error`
directly. Add helpers for the display name and effective error policy, use the
display-name helper for spans, context, and logging, and use the hook-point
policy for trait hooks; replace the undefined `item` match with the loop
variable.
- Around line 287-305: Update the GuestExec command-building flow around
BoxCommand::new to apply the same context substitution used by HostExec: run
substitution::substitute_args on args and substitution::substitute_env on env
before passing them to BoxCommand::arg and BoxCommand::env. Preserve the
existing user, working_dir, and ctx.to_env_vars handling.
- Around line 11-13: Separate the declarative Hook type from the in-process hook
trait by renaming the config struct to HookConfig and giving the trait in
runner.rs a distinct name, then update all references and exports accordingly.
Apply this in src/boxlite/src/hooks/runner.rs lines 11-13, 19-25, and 393-451;
update related public usage in src/boxlite/src/lib.rs lines 61-64 and
src/boxlite/src/litebox/box_impl.rs line 187; revise the corresponding
terminology and examples in docs/architecture/container-lifecycle-hooks.md lines
700-741.

In `@src/boxlite/src/litebox/box_impl.rs`:
- Around line 535-576: Update the PostExec hook context construction inside the
spawned task’s result loop to clone post_exec_box_id, post_exec_container_id,
post_exec_image, and post_exec_command before passing them to
HookContext::for_post_exec. Keep the captured values available for every
iteration of the while loop.
- Around line 310-332: The current PreStart block runs after ensure_booted and
outside the container-start single-flight, allowing repeated execution and
leaving failed starts Running. Move PreStart execution into
ensure_container_started, guarded by the same single-flight that decides whether
init starts, and ensure it runs exactly once before container_start; on hook
failure, clean up the booted VM and restore the retryable pre-start state before
propagating the error.

In `@src/boxlite/src/runtime/options.rs`:
- Around line 435-437: Add hook-name uniqueness validation in the runtime create
path before BoxOptions is persisted, covering SDK and deserialized callers
rather than relying on CLI validation. Use the existing Hook name field and
return the established validation error when duplicate names are detected;
preserve valid hook configurations unchanged.

In `@src/cli/src/cli.rs`:
- Around line 1068-1074: Validate hook actions and conditions against the
selected hook point before persisting BoxOptions: restrict GuestExec to
post-start, pre-stop, and post-restore, and reject HookCondition::ExecResult
unless the hook point is post-exec. Apply the checks in the CLI hook parsing
path covering the GuestExec and condition handling branches, returning a
validation error for incompatible configurations.
- Around line 1198-1208: The retry parsing branch should validate the optional
exhaustion policy instead of defaulting unknown values to OnExhausted::Continue.
Update the retry policy parsing around the retry branch to accept exactly fail
or continue, reject any other policy, and reject inputs containing more than the
allowed three comma-separated fields.
- Around line 987-993: Update the PR description to include both
before-and-after end-to-end call graphs for the CLI-to-runtime container hook
flow, covering hook parsing, modifier association, and runtime execution. Keep
the documentation near the lifecycle hook description consistent with the
implemented symbols and clearly distinguish the existing flow from the changed
flow.
- Around line 1004-1006: Update the CLI argument model and hook-processing logic
around hook_args and the related handling near the hook construction code so
each --hook-arg carries an explicit hook name or otherwise preserves its
association with the intended --hook. Reject arguments that lack an eligible
matching hook, including attempts to modify a final --hook-json hook, instead of
applying all modifiers to the last hook.
- Around line 1110-1121: Update the modifier application logic around
hook_enabled, hook_priority, and hook_timeout to propagate parse errors instead
of falling back to defaults. Validate parsed timeout_secs and return an error
when it is below one second, while preserving valid values and the existing
find_hook_mut lookups.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cf61af83-2b99-43fd-8f0b-d103f65a14c8

📥 Commits

Reviewing files that changed from the base of the PR and between 924c7ff and 569c742.

📒 Files selected for processing (15)
  • docs/architecture/container-lifecycle-hooks-alpha-tests.md
  • docs/architecture/container-lifecycle-hooks.md
  • src/boxlite/src/hooks/context.rs
  • src/boxlite/src/hooks/fire_count.rs
  • src/boxlite/src/hooks/host_exec.rs
  • src/boxlite/src/hooks/mod.rs
  • src/boxlite/src/hooks/runner.rs
  • src/boxlite/src/hooks/substitution.rs
  • src/boxlite/src/lib.rs
  • src/boxlite/src/litebox/box_impl.rs
  • src/boxlite/src/runtime/options.rs
  • src/boxlite/src/runtime/rt_impl.rs
  • src/cli/src/cli.rs
  • src/cli/src/commands/create.rs
  • src/cli/src/commands/run.rs

@@ -0,0 +1,1184 @@
# Container Lifecycle Hook System

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the required call graphs to the PR description.

Add before-and-after end-to-end call graphs to the PR description. The stated change is a feature, so the bug-fix Fixes #<n> requirement does not apply.

As per coding guidelines, every PR description must include before-and-after end-to-end call graphs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/architecture/container-lifecycle-hooks.md` at line 1, Update the PR
description to include end-to-end call graphs showing the container lifecycle
hook flow before and after the change described by “Container Lifecycle Hook
System.” Do not add a “Fixes #<n>” reference, since this is a feature change.

Source: Coding guidelines

Comment on lines +5 to +17
Give BoxLite users the ability to inject custom logic at key points in the
container lifecycle — create, start, stop, exec, snapshot, restore — without
forking the runtime or wrapping every API call. Hooks run synchronously
(blocking) with configurable timeouts, ordered by user-defined priority. They
span the host (commands run on the host OS beside the runtime) and the guest
(commands run inside the container via exec).

This is an **embedded library feature** first — the Rust trait, CLI flags, and
SDK builders are the primary surface. The REST server inherits hooks from box
options supplied at creation and never invents its own. A hook's execution
location is determined by its action type: `HostExec` runs on the same machine
as the boxlite runtime; `GuestExec` runs inside the container via the Execution
RPC.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Limit the documented scope to implemented phases.

This section says GuestExec, snapshot, and restore hooks are available. The PR objectives defer these capabilities. Snapshot and restore lifecycle wiring is also not present in the supplied runtime files.

Document only create, start, stop, and exec host-hook support for this phase. Move deferred behavior to a future-work section.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/architecture/container-lifecycle-hooks.md` around lines 5 - 17, Revise
the lifecycle-hooks documentation to describe only the currently implemented
create, start, stop, and exec host-hook phases. Remove GuestExec, snapshot, and
restore from the supported behavior and move those deferred capabilities into a
future-work section.

Comment on lines +24 to +28
/// Load pre-existing counts from persisted storage (e.g., box state DB).
pub fn load(&self, box_id: &str, hook_name: &str, persisted_count: u64) {
let mut counts = self.counts.lock().unwrap();
counts.insert((box_id.to_string(), hook_name.to_string()), persisted_count);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Persist fire counts at the box-state boundary.

load() only copies a value into the in-memory map. No supplied lifecycle path saves increments or reloads them when BoxImpl is reconstructed. As a result, fire_count resets after a runtime restart and can also reset after cache invalidation.

Persist each increment by (box_id, hook_name) and reload the values when the runner is created.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/hooks/fire_count.rs` around lines 24 - 28, Update FireCount
persistence around load and its increment path so every (box_id, hook_name)
count increment is written to the box-state storage, and restore persisted
counts when the BoxImpl runner is created. Ensure cache invalidation or runtime
reconstruction reloads the stored values instead of resetting fire_count, while
preserving load’s existing key-based behavior.

Comment on lines +47 to +74
let mut child = Command::new(program)
.args(&args)
.envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str())))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.map_err(|e| {
BoxliteError::Internal(format!(
"Failed to spawn HostExec hook '{}' program '{program}': {e}",
hook.name
))
})?;

// Write context JSON to stdin, then close it
if let Some(mut stdin) = child.stdin.take() {
use tokio::io::AsyncWriteExt;
stdin
.write_all(ctx_json.as_bytes())
.await
.map_err(|e| BoxliteError::Internal(format!("Failed to write stdin: {e}")))?;
// stdin is dropped here, closing the pipe
}

let timeout_dur = Duration::from_secs(hook.timeout_secs);

let result = timeout(timeout_dur, child.wait_with_output()).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
set -eu

rg -n -C 3 'tokio\s*=|name = "tokio"|version = "1\.37' \
  Cargo.toml Cargo.lock 2>/dev/null || true

rg -n -C 5 'kill_on_drop|wait_with_output|process_group|start_kill|SIGTERM|SIGKILL' \
  src/boxlite/src/hooks/host_exec.rs \
  src/boxlite/src/hooks/runner.rs

Repository: boxlite-ai/boxlite

Length of output: 6487


🏁 Script executed:

#!/bin/sh
set -eu

sed -n '1,150p' src/boxlite/src/hooks/host_exec.rs
printf '\n--- runner ---\n'
sed -n '280,390p' src/boxlite/src/hooks/runner.rs
printf '\n--- docs ---\n'
sed -n '220,240p' docs/architecture/container-lifecycle-hooks.md
sed -n '445,465p' docs/architecture/container-lifecycle-hooks-alpha-tests.md

printf '\n--- search hook exec APIs ---\n'
rg -n -C 2 'kill_on_drop|process_id|Stdio::from_stdio|child\.' src/boxlite/src/hooks src/box 2>/dev/null || true

printf '\n--- dependency snippets for child/process handling ---\n'
rg -n 'tokio-child|derive_child|process' Cargo.lock Cargo.toml 2>/dev/null || true

Repository: boxlite-ai/boxlite

Length of output: 14464


🏁 Script executed:

#!/bin/sh
set -eu

printf '%s\n' '--- find GuestExec execution interface ---'
rg -n -C 3 'trait .*Execution|impl .*Execution|fn exec\(|exec<|execution\(\)|CancellationToken|start_kill|SIGTERM|SIGKILL' src 2>/dev/null || true

printf '%s\n' '--- BoxCommand definition/usages ---'
fd -i 'box|command' src | sed -n '1,80p'
rg -n -C 4 'struct BoxCommand|impl.*BoxCommand|BoxCommand' src/boxlite/src src 2>/dev/null | sed -n '1,240p'

Repository: boxlite-ai/boxlite

Length of output: 50377


Implement forced termination for timed-out hooks.

The timeout paths do not satisfy the documented SIGTERM + grace + SIGKILL behavior.

  • src/boxlite/src/hooks/host_exec.rs#L47-L114: .kill_on_drop(true) cannot create a process group or implement SIGTERM → 5 s grace → SIGKILL. On timeout(...), send the appropriate signal to the child/group if the guest has already started the execution, wait for grace, then escalate.
  • src/boxlite/src/hooks/runner.rs#L312-L360: store the cancellation handle or execution state from exec(...) and call its SIGTERM/SIGKILL path after timeout instead of only returning an error.
  • docs/architecture/container-lifecycle-hooks.md#L220-L234: keep the staged-kill description only after the behavior is implemented.
  • docs/architecture/container-lifecycle-hooks-alpha-tests.md#L452-L459: make the timeout test validate process/group termination and orphan cleanup, not only fire() returning an error.
📍 Affects 4 files
  • src/boxlite/src/hooks/host_exec.rs#L47-L74 (this comment)
  • src/boxlite/src/hooks/runner.rs#L324-L365
  • docs/architecture/container-lifecycle-hooks.md#L232-L234
  • docs/architecture/container-lifecycle-hooks-alpha-tests.md#L452-L459
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/hooks/host_exec.rs` around lines 47 - 74, Implement staged
timeout termination across the hook execution flow: in
src/boxlite/src/hooks/host_exec.rs lines 47-74, update the host process setup
and timeout handling to terminate the appropriate process group with SIGTERM,
wait 5 seconds, then use SIGKILL if needed after execution has started; in
src/boxlite/src/hooks/runner.rs lines 324-365, retain the cancellation handle or
execution state returned by exec and invoke that termination path on timeout
rather than only returning an error; in
docs/architecture/container-lifecycle-hooks.md lines 232-234, retain the
staged-kill documentation only once implemented; and in
docs/architecture/container-lifecycle-hooks-alpha-tests.md lines 452-459, extend
the timeout test to verify process/group termination and orphan cleanup.

Comment on lines +157 to +165
/// Timeout in seconds. The default is 30 s. Must be ≥ 1.
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
/// Only fire when this condition holds. `None` = always fire.
#[serde(default)]
pub condition: Option<HookCondition>,
/// What to do when this hook fails (non-zero exit, timeout, or spawn error).
#[serde(default)]
pub on_error: HookErrorPolicy,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply on_error defaults from HookPoint.

#[serde(default)] calls HookErrorPolicy::default(), which is always Continue. A minimal JSON pre-start, pre-exec, pre-snapshot, or pre-restore hook therefore continues after failure instead of using the documented Fail policy.

Store an omitted policy separately, or implement custom deserialization, then resolve it with HookPoint::default_on_error().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/hooks/mod.rs` around lines 157 - 165, Update hook
deserialization around the on_error field and HookPoint::default_on_error() so
an omitted on_error policy remains distinguishable from an explicitly configured
policy. Resolve omitted policies using the current HookPoint default, preserving
explicit HookErrorPolicy values and the documented Fail defaults for pre-start,
pre-exec, pre-snapshot, and pre-restore.

Comment thread src/cli/src/cli.rs
Comment on lines +987 to +993
/// Container lifecycle hooks.
///
/// Each `--hook` flag defines one hook. The simple syntax is:
/// `<name>:<point>:<host|guest>:<program>` with args via `--hook-arg`.
///
/// Modifiers like `--hook-timeout` and `--hook-on-error` reference the hook
/// by name and must appear after the corresponding `--hook` flag.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add end-to-end call graphs to the PR description.

The supplied PR description does not include before-and-after end-to-end call graphs. Add both call graphs for the CLI-to-runtime hook flow.

As per coding guidelines, every PR description must include before-and-after end-to-end call graphs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/src/cli.rs` around lines 987 - 993, Update the PR description to
include both before-and-after end-to-end call graphs for the CLI-to-runtime
container hook flow, covering hook parsing, modifier association, and runtime
execution. Keep the documentation near the lifecycle hook description consistent
with the implemented symbols and clearly distinguish the existing flow from the
changed flow.

Source: Coding guidelines

Comment thread src/cli/src/cli.rs
Comment on lines +1004 to +1006
/// Add an argument to the most recent --hook.
#[arg(long = "hook-arg", value_name = "ARG")]
pub hook_args: Vec<String>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve the target hook for each --hook-arg.

Clap collects hooks and hook_args into separate vectors. The original flag order is lost. A command with multiple hooks therefore appends every argument to the final hook. --hook-arg also modifies a final --hook-json hook despite the documented restriction.

Require a hook name in each argument modifier, or use a per-hook syntax that preserves argument association. Reject --hook-arg when no eligible hook exists.

Also applies to: 1097-1107

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/src/cli.rs` around lines 1004 - 1006, Update the CLI argument model
and hook-processing logic around hook_args and the related handling near the
hook construction code so each --hook-arg carries an explicit hook name or
otherwise preserves its association with the intended --hook. Reject arguments
that lack an eligible matching hook, including attempts to modify a final
--hook-json hook, instead of applying all modifiers to the last hook.

Comment thread src/cli/src/cli.rs
Comment on lines +1068 to +1074
"guest" => HookAction::GuestExec {
command: program,
args: vec![],
env: vec![],
user: None,
working_dir: None,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate hook actions and conditions against the hook point.

The CLI accepts GuestExec at every hook point. The runner skips it when no guest session exists. The hook contract permits guest execution only at post-start, pre-stop, and post-restore. The CLI also accepts HookCondition::ExecResult for non-post-exec hooks even though that condition is PostExec-only.

Reject these incompatible configurations before persisting BoxOptions.

Also applies to: 1126-1130

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/src/cli.rs` around lines 1068 - 1074, Validate hook actions and
conditions against the selected hook point before persisting BoxOptions:
restrict GuestExec to post-start, pre-stop, and post-restore, and reject
HookCondition::ExecResult unless the hook point is post-exec. Apply the checks
in the CLI hook parsing path covering the GuestExec and condition handling
branches, returning a validation error for incompatible configurations.

Comment thread src/cli/src/cli.rs
Comment on lines +1110 to +1121
for (name, val) in &self.hook_enabled {
let hook = find_hook_mut(opts, name)?;
hook.enabled = val.parse::<bool>().unwrap_or(true);
}
for (name, val) in &self.hook_priority {
let hook = find_hook_mut(opts, name)?;
hook.priority = val.parse().unwrap_or(0);
}
for (name, val) in &self.hook_timeout {
let hook = find_hook_mut(opts, name)?;
hook.timeout_secs = val.parse().unwrap_or(30);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject invalid modifier values.

An invalid --hook-enabled value enables the hook. Invalid priority and timeout values silently become 0 and 30. A timeout of 0 also violates the Hook contract and causes an immediate timeout.

Return an error for parse failures and reject timeouts below one second.

Proposed validation
 for (name, val) in &self.hook_enabled {
     let hook = find_hook_mut(opts, name)?;
-    hook.enabled = val.parse::<bool>().unwrap_or(true);
+    hook.enabled = val
+        .parse::<bool>()
+        .map_err(|_| anyhow::anyhow!("Invalid enabled value '{}' for hook '{}'", val, name))?;
 }
 for (name, val) in &self.hook_priority {
     let hook = find_hook_mut(opts, name)?;
-    hook.priority = val.parse().unwrap_or(0);
+    hook.priority = val
+        .parse()
+        .map_err(|_| anyhow::anyhow!("Invalid priority '{}' for hook '{}'", val, name))?;
 }
 for (name, val) in &self.hook_timeout {
     let hook = find_hook_mut(opts, name)?;
-    hook.timeout_secs = val.parse().unwrap_or(30);
+    let timeout_secs: u64 = val
+        .parse()
+        .map_err(|_| anyhow::anyhow!("Invalid timeout '{}' for hook '{}'", val, name))?;
+    if timeout_secs == 0 {
+        anyhow::bail!("Hook '{}' timeout must be at least one second", name);
+    }
+    hook.timeout_secs = timeout_secs;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (name, val) in &self.hook_enabled {
let hook = find_hook_mut(opts, name)?;
hook.enabled = val.parse::<bool>().unwrap_or(true);
}
for (name, val) in &self.hook_priority {
let hook = find_hook_mut(opts, name)?;
hook.priority = val.parse().unwrap_or(0);
}
for (name, val) in &self.hook_timeout {
let hook = find_hook_mut(opts, name)?;
hook.timeout_secs = val.parse().unwrap_or(30);
}
for (name, val) in &self.hook_enabled {
let hook = find_hook_mut(opts, name)?;
hook.enabled = val
.parse::<bool>()
.map_err(|_| anyhow::anyhow!("Invalid enabled value '{}' for hook '{}'", val, name))?;
}
for (name, val) in &self.hook_priority {
let hook = find_hook_mut(opts, name)?;
hook.priority = val
.parse()
.map_err(|_| anyhow::anyhow!("Invalid priority '{}' for hook '{}'", val, name))?;
}
for (name, val) in &self.hook_timeout {
let hook = find_hook_mut(opts, name)?;
let timeout_secs: u64 = val
.parse()
.map_err(|_| anyhow::anyhow!("Invalid timeout '{}' for hook '{}'", val, name))?;
if timeout_secs == 0 {
anyhow::bail!("Hook '{}' timeout must be at least one second", name);
}
hook.timeout_secs = timeout_secs;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/src/cli.rs` around lines 1110 - 1121, Update the modifier application
logic around hook_enabled, hook_priority, and hook_timeout to propagate parse
errors instead of falling back to defaults. Validate parsed timeout_secs and
return an error when it is below one second, while preserving valid values and
the existing find_hook_mut lookups.

Comment thread src/cli/src/cli.rs
Comment on lines +1198 to +1208
_ if s.starts_with("retry:") => {
let parts: Vec<&str> = s[6..].split(',').collect();
if parts.len() < 2 {
anyhow::bail!("retry policy needs max_retries,backoff_secs: got '{s}'");
}
let max_retries: u32 = parts[0].parse()?;
let backoff_secs: u64 = parts[1].parse()?;
let on_exhausted = parts.get(2).map_or(OnExhausted::Continue, |oe| match *oe {
"fail" => OnExhausted::Fail,
_ => OnExhausted::Continue,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject unknown retry exhaustion policies.

retry:1,2,fali silently becomes OnExhausted::Continue. A typo can therefore continue the lifecycle after a hook failure when the operator intended it to fail. Extra fields are also ignored.

Accept only fail and continue when the optional exhaustion policy is present. Reject other values and extra fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/src/cli.rs` around lines 1198 - 1208, The retry parsing branch should
validate the optional exhaustion policy instead of defaulting unknown values to
OnExhausted::Continue. Update the retry policy parsing around the retry branch
to accept exactly fail or continue, reject any other policy, and reject inputs
containing more than the allowed three comma-separated fields.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant