# Lifecycle Scripts

Lifecycle scripts build, configure, and grade your sandbox. There are four: `build.sh`, `setup.sh`, `answer.sh`, and `score.sh`.

All scripts must begin with `#!/usr/bin/env bashp` — not `#!/bin/bash`. The `bashp` preprocessor inlines library functions like `install::apt_packages` and `scoring::check` before execution. It also implies `set -ex`, so only options that differ from that default need to be set explicitly (e.g. `set -uo pipefail` or `set +e`).

## When each script runs

| Script | When | What to put here |
| --- | --- | --- |
| `build.sh` | Once, at image build time | Package installs, tool downloads, static files, service enables |
| `setup.sh` | Fresh-session setup; see local command behavior below | Readiness waits only — leave blank when possible |
| `answer.sh` | During testing only | The steps a perfect candidate would take |
| `score.sh` | When the candidate submits | Pass/fail checks followed by `scoring::report` |

## build.sh

Runs once when the VM image is built. Results are baked into the image, so sandboxes start fast.

```
#!/usr/bin/env bashp

install::apt_packages vim git nginx
systemctl enable --now nginx
```

Enable and start services here, not in setup.sh

Use `systemctl enable --now <service>` in `build.sh`. The `--now` flag both enables the service for boot and starts it immediately during the build, so subsequent build steps can use it, such as pulling a container image. Enablement is baked into the image; the service starts again when a new VM boots.

Sandbox network access

Sandbox VMs have full outbound internet access. Prefer installing packages in `build.sh` over downloading them at runtime — not because the network is unavailable, but because runtime fetches from external URLs are brittle: upstream packages move, URLs change, and rate limits break otherwise-passing items.

## setup.sh

The platform runs `setup.sh` during fresh candidate or learner session setup. Locally, `sandbox setup`, `sandbox answer`, `sandbox score`, and `sandbox test` run it as part of their lifecycle sequence. **Leave it blank when possible.**

Plain `sandbox shell`, `sandbox start`, and `sandbox attach` do not run `setup.sh`. Background VMs keep their state across attachments and source reloads; updating the mounted script does not reset the VM or apply the script's effects.

Fresh sessions start from the built image. Services enabled in `build.sh` start at boot, so setup should not repeat package installation or service enablement. See the [CLI command and ownership rules](https://skills.staging.lf-cert.cloud/docs/sandbox/cli/#sandbox-setup-answer-score-test) before switching between background development and a fresh lifecycle test.

The only work that belongs in `setup.sh` is waiting for things with a significant, observable startup delay that outlasts SSH availability. Kubernetes cluster readiness is the canonical case (`k8s::wait_for_all`). Fast-starting services like Docker do not qualify — even if a theoretical race with SSH exists, it has never caused failures in practice.

```
#!/usr/bin/env bashp

k8s::wait_for_all
```

For non-Kubernetes sandboxes, `setup.sh` is typically a no-op:

```
#!/usr/bin/env bashp
```

## answer.sh

Applies the reference solution. Never runs in production. Locally, `sandbox answer`, `sandbox score`, and `sandbox test` run it after setup. Write it as the exact steps a correct candidate would take.

```
#!/usr/bin/env bashp

echo "bar" > /tmp/foo
```

## score.sh

Checks the candidate's work. Each check is a function that returns `0` for pass or non-zero for fail. Run checks with `scoring::check` and end the script with `scoring::report`.

```
#!/usr/bin/env bashp

fileExists() {
    test -f /tmp/foo
}

fileContent() {
    grep -q "bar" /tmp/foo
}

scoring::check fileExists
scoring::check fileContent
scoring::report
```

The combined number of `scoring::check` and `scoring::namedcheck` registrations must equal `total_check_count` in `metadata.yaml`.

Scoring can run repeatedly. `sandbox test` runs it twice — before and after `answer.sh` — and production hooks may retry scoring after a transient failure. Keep checks deterministic and idempotent. A source reload does not run scoring or any other lifecycle script automatically.

## Bashp helpers and local libraries

The CLI compiles source scripts into ordinary Bash under `dist/<vm>/scripts/`. A running local VM mounts that VM's dist directory at `/sandbox`, so it executes `/sandbox/scripts/<name>.sh`, not the host's Bashp source.

Functions can come from the CLI's bundled library, a vendored package under `bashp-packages/libs/`, or VM-local `libs/` and `scripts/libs/` directories. Names map to files: `demo::report` can be defined in `cp/libs/demo/report`. Referenced helpers and their transitive dependencies are inlined into the compiled script.

Since v1.25, the preprocessor tracks quotes separately inside nested `$(...)` and `${...}` expressions. Use ordinary Bash quoting rather than adding escaping workarounds.

### Verify library-only reload

Use a VM named `cp` with an image already built, then start [background live reload](https://skills.staging.lf-cert.cloud/docs/sandbox/cli/#sandbox-start-and-stop) with CLI v1.27 or later. From the sandbox directory, create these three source files:

**bashp-packages/libs/vendor/message**

```
function vendor::message() {
    printf '%s\n' 'vendor-v1'
}
```

**cp/libs/demo/report**

```
function demo::report() {
    printf 'local-v1|%s|%s\n' "$(vendor::message)" "$(system::get_os)"
}
```

**cp/scripts/probe.sh**

```
#!/usr/bin/env bashp

demo::report
```

The local function uses both the vendored helper and bundled `system::get_os`. After saving, execute the regenerated script explicitly:

```
sandbox exec --target cp -- bash /sandbox/scripts/probe.sh
```

On a Debian image, the output is:

```
local-v1|vendor-v1|debian
```

Change only `cp/libs/demo/report` from `local-v1` to `local-v2`, or only the vendored helper from `vendor-v1` to `vendor-v2`. Leave `probe.sh` unchanged. Its compiled copy updates automatically; executing it again shows the new library output without restarting the VM.

The same dependency-only reload applies to `setup.sh`, `answer.sh`, and `score.sh`. VM-local changes affect that VM's generated scripts. Shared vendored library changes trigger regeneration for every started VM watching that root. The bundled library is embedded in the CLI; upgrading those bundled implementations requires a CLI update.

Edit source libraries and scripts, not their generated `dist/` copies. Reload does not execute the new code or replace function definitions already loaded into an attached shell.

### Inspect helpers with an agent

CLI v1.26 introduced the read-only [Bashp MCP workflow](https://skills.staging.lf-cert.cloud/docs/sandbox/cli/#sandbox-mcp): find helpers, explain the selected implementation, then resolve the saved lifecycle script or unsaved buffer. Supply the real file context so local overrides and transitive dependencies are resolved for the correct VM.

A resolution `pass` is not a lifecycle, scoring-policy, or runtime test. Keep the independent content checks and full sandbox acceptance test below.

## Test the full lifecycle

Build the current image and verify the untouched and answered states:

```
sandbox build && sandbox test
```

This passes when scoring returns zero checks before `answer.sh` runs, and full marks after. If it fails, the output tells you which check did not pass and at which stage.

Exit any owning shell, or stop background VMs with `sandbox stop`, before this test. `sandbox test` owns and cleans up its fresh VM session and does not enable live reload.
