Skip to content
12 changes: 8 additions & 4 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,14 @@ jobs:
- name: Install shellcheck
run: sudo apt-get update && sudo apt-get install --yes shellcheck
- run: uv sync --locked
- name: Verify channels rollout contract
run: uv run --project . python3 ./tests/scripts/security_workflow.py verify-channels-contract --repo-root .
- name: Run recovery backup/restore smoke
run: uv run --project . python3 ./tests/scripts/security_workflow.py run-recovery-smoke --tmp-root "${RUNNER_TEMP}"
- name: Run repository quality gate
run: uv run python -m clawops supply-chain --repo-root . quality-gate
- name: Coverage summary
run: python3 ./tests/scripts/security_workflow.py write-coverage-summary --coverage-file coverage.xml --summary-file "${GITHUB_STEP_SUMMARY}"
run: uv run --project . python3 ./tests/scripts/security_workflow.py write-coverage-summary --coverage-file coverage.xml --summary-file "${GITHUB_STEP_SUMMARY}"
- name: Install Semgrep CLI
run: python3 -m pip install --disable-pip-version-check "semgrep==1.156.0"
- name: Run Semgrep
Expand All @@ -57,14 +61,14 @@ jobs:
env:
GITLEAKS_VERSION: "8.28.0"
GITLEAKS_SHA256: "a65b5253807a68ac0cafa4414031fd740aeb55f54fb7e55f386acb52e6a840eb"
run: python3 ./tests/scripts/security_workflow.py install-gitleaks --version "${GITLEAKS_VERSION}" --sha256 "${GITLEAKS_SHA256}" --runner-temp "${RUNNER_TEMP}" --github-path-file "${GITHUB_PATH}"
run: uv run --project . python3 ./tests/scripts/security_workflow.py install-gitleaks --version "${GITLEAKS_VERSION}" --sha256 "${GITLEAKS_SHA256}" --runner-temp "${RUNNER_TEMP}" --github-path-file "${GITHUB_PATH}"
- name: Run gitleaks
run: gitleaks git --no-banner --no-color --exit-code 1 --log-level warn --redact
- name: Install syft CLI
env:
SYFT_VERSION: "v1.42.2"
SYFT_SHA256: "1d3cc98b13ce3dfb6083ef42f64f1033e40d7dea292e8ea85ed1cf88efb2f542"
run: python3 ./tests/scripts/security_workflow.py install-syft --version "${SYFT_VERSION}" --sha256 "${SYFT_SHA256}" --runner-temp "${RUNNER_TEMP}" --github-path-file "${GITHUB_PATH}"
run: uv run --project . python3 ./tests/scripts/security_workflow.py install-syft --version "${SYFT_VERSION}" --sha256 "${SYFT_SHA256}" --runner-temp "${RUNNER_TEMP}" --github-path-file "${GITHUB_PATH}"
- name: Generate SBOM
run: syft dir:. -o spdx-json=sbom.spdx.json
- uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
Expand All @@ -75,7 +79,7 @@ jobs:
trivy-config: security/trivy/trivy.yaml
- name: Preserve historical code-scanning categories
if: always()
run: python3 ./tests/scripts/security_workflow.py write-empty-sarif --output "${RUNNER_TEMP}/empty-security.sarif" --information-uri "https://github.com/jsugg/strongclaw"
run: uv run --project . python3 ./tests/scripts/security_workflow.py write-empty-sarif --output "${RUNNER_TEMP}/empty-security.sarif" --information-uri "https://github.com/jsugg/strongclaw"
- uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4
if: always()
with:
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ Read these in order:
4. [`USAGE_GUIDE.md`](USAGE_GUIDE.md)
5. [`platform/docs/ARCHITECTURE.md`](platform/docs/ARCHITECTURE.md)
6. [`platform/docs/PRODUCTION_READINESS_CHECKLIST.md`](platform/docs/PRODUCTION_READINESS_CHECKLIST.md)
7. [`platform/docs/DEVFLOW.md`](platform/docs/DEVFLOW.md)
8. [`platform/docs/PLUGIN_INVENTORY.md`](platform/docs/PLUGIN_INVENTORY.md)
9. [`platform/docs/DEGRADATION.md`](platform/docs/DEGRADATION.md)
7. [`platform/docs/SECURITY_MODEL.md`](platform/docs/SECURITY_MODEL.md)
8. [`platform/docs/SECRETS_AND_ENV.md`](platform/docs/SECRETS_AND_ENV.md)
9. [`platform/docs/POLICY_ENGINE_AND_WRAPPERS.md`](platform/docs/POLICY_ENGINE_AND_WRAPPERS.md)
10. [`platform/docs/CI_AND_SECURITY.md`](platform/docs/CI_AND_SECURITY.md)
11. [`platform/docs/DEVFLOW.md`](platform/docs/DEVFLOW.md)
12. [`platform/docs/PLUGIN_INVENTORY.md`](platform/docs/PLUGIN_INVENTORY.md)
13. [`platform/docs/DEGRADATION.md`](platform/docs/DEGRADATION.md)

## Repository map

Expand Down
8 changes: 7 additions & 1 deletion platform/docs/CI_AND_SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ any required lane does not complete successfully.

## Fresh-host acceptance

`.github/workflows/fresh-host-acceptance.yml` exercises the real bootstrap, setup, service activation, and repo-local sidecar/browser-lab flows on hosted Linux and macOS runners.
`.github/workflows/fresh-host-acceptance.yml` exercises the real bootstrap, setup, service activation, and repo-local sidecar/browser-lab flows on hosted Linux and macOS runners. It delegates the reusable execution lane to `.github/workflows/fresh-host-core.yml`.

- Each run writes a GitHub job summary with the runner label, runtime provider,
cache toggles, phase timings, and the effective hosted macOS Colima sizing.
Expand All @@ -61,6 +61,7 @@ sidecars and browser-lab mutable data live in Docker-managed volumes instead of
macOS path without changing the required PR gate.
- The workflow stays declarative by delegating runtime setup, image warming,
diagnostics, and summary generation to executable helper scripts under `tests/scripts/`. Hosted macOS image warming restores a cached Docker image archive when available, then verifies compose image availability with bounded retries and heartbeat logging as a fallback.
- `.github/workflows/nightly.yml` warms the fresh-host caches before it calls the reusable fresh-host core lane for the scheduled validation sweep.
- Repository workflow contract tests verify that shell steps invoking
`tests/scripts/*.py` either call an explicit Python interpreter or target an executable script, so nightly cache warming cannot silently regress on file mode drift.

Expand Down Expand Up @@ -112,6 +113,11 @@ raw tar extraction, traversal-prone archive-member joins, `subprocess`
`shell=True`, and unsafe deserialization helpers.
- `.github/workflows/security.yml` verifies the pinned `gitleaks` and `syft`
tarball SHA-256 digests before extracting the binaries through the dedicated helper script.
- That same security lane now executes two operational smoke checks through
`tests/scripts/security_workflow.py`: channel rollout contract parity
(`verify-channels-contract`) plus a disposable backup/verify/restore cycle
(`run-recovery-smoke`) so launch-critical channel and recovery paths produce
executable CI evidence.
- `.github/workflows/release.yml` now blocks publication on three repo-controlled
prerequisites: the centralized release quality gate, the reusable fresh-host
acceptance workflow, and the reusable memory-plugin verification workflow. It
Expand Down
2 changes: 2 additions & 0 deletions platform/docs/POLICY_ENGINE_AND_WRAPPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ Treat `~/.openclaw/clawops` as service-owned state.

- keep directory mode `0700` on `~/.openclaw/clawops`
- keep file mode `0600` on `~/.openclaw/clawops/op_journal.sqlite`
- keep reviewer packet directories mode `0700` and packet files mode `0600`
- `clawops` normalizes those journal and reviewer-artifact permissions on supported POSIX hosts when it creates or updates them
- do not grant write access to lower-trust workers or shared workspaces

## CLI flow
Expand Down
62 changes: 45 additions & 17 deletions platform/docs/PRODUCTION_READINESS_CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,51 @@
# Production Readiness Checklist

- [ ] dedicated OS user
- [ ] loopback-bound gateway
- [ ] token auth enabled
Use this checklist as the operator-facing go/no-go gate after setup succeeds and
before you treat a host as launch-ready.

## 1. Host and gateway boundary

- [ ] dedicated OS user owns the StrongClaw/OpenClaw runtime
- [ ] gateway stays loopback-bound and is only reached through an SSH tunnel
- [ ] token auth is enabled
- [ ] `session.dmScope = per-channel-peer`
- [ ] sandbox mode `all`
- [ ] elevated exec disabled
- [ ] plugins/skills default-deny
- [ ] sidecars healthy
- [ ] `openclaw doctor` clean
- [ ] `openclaw security audit --deep` clean
- [ ] `openclaw secrets audit --check` clean
- [ ] operation journal initialized
- [ ] policy regression suite green
- [ ] backup and restore tested
- [ ] channel allowlists durable
- [ ] browser lab isolated or disabled
- [ ] if browser-lab is enabled, run `clawops verify-platform browser-lab` and confirm loopback-only bindings (`clawops baseline verify` includes browser-lab by default; use `--exclude-browser-lab` only when browser-lab is intentionally out of scope)
- [ ] remote operator access uses SSH tunnel to gateway only
- [ ] sandbox mode is `all`
- [ ] elevated exec is disabled
- [ ] plugins and skills stay default-deny until reviewed

## 2. Release-ready verification commands

Run these command-backed checks and keep the resulting evidence with the host
handoff or launch packet:

- [ ] `clawops doctor` is clean
- [ ] `clawops baseline verify` is clean
- [ ] `clawops verify-platform sidecars` is clean
- [ ] `clawops verify-platform observability` is clean
- [ ] `clawops verify-platform channels` is clean when rollout includes operator channels
- [ ] `openclaw doctor` is clean
- [ ] `openclaw security audit --deep` is clean
- [ ] `openclaw secrets audit --check` is clean

`clawops doctor-host` and `clawops doctor --skip-runtime --no-model-probe` are
useful host-only or degraded diagnostics, but they are **not** launch-ready or
release-ready substitutes for `clawops doctor`.

## 3. Recovery and durability

- [ ] operation journal is initialized and writable
- [ ] `clawops recovery backup-create` succeeds against the production home
- [ ] `clawops recovery backup-verify <archive>` succeeds for a freshly created archive
- [ ] `clawops recovery restore <archive> <clean-home>` has been tested on a clean destination
- [ ] backup retention is configured and reviewed
- [ ] channel allowlists are durable and versioned

## 4. Optional-but-exposed launch surfaces

- [ ] browser-lab is either disabled or isolated to a separate host or hardened OS user
- [ ] if browser-lab is enabled, `clawops verify-platform browser-lab` is clean and browser-lab ports remain loopback-only
- [ ] if browser-lab is intentionally out of scope for this rollout, `clawops baseline verify --exclude-browser-lab` is the documented gate result for the launch packet
- [ ] remote operator access uses an SSH tunnel to the gateway only; never tunnel browser-lab ports such as `9222` or `3128`

`clawops repo doctor` remains an operator/development contract for `repo/upstream`
and managed worktree flows. It is not part of the production baseline or launch
Expand Down
17 changes: 17 additions & 0 deletions src/clawops/approval_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

import dataclasses
import json
import os
import pathlib

from clawops.common import write_json
from clawops.op_journal import Operation, OperationJournal

REVIEW_PACKET_VERSION = 1
LOCAL_DISPATCH_CHANNEL = "local_file"
_OWNER_ONLY_DIR_MODE = 0o700
_OWNER_ONLY_FILE_MODE = 0o600


@dataclasses.dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -50,6 +53,18 @@ def _default_artifact_path(journal: OperationJournal, *, op_id: str) -> pathlib.
return journal.db_path.parent / "reviews" / f"{op_id}.json"


def _normalize_permissions(path: pathlib.Path, mode: int) -> None:
"""Apply owner-only permissions on supported hosts."""
if os.name == "nt":
return
try:
current_mode = path.stat().st_mode & 0o777
except FileNotFoundError:
return
if current_mode != mode:
path.chmod(mode)


def build_review_packet(operation: Operation) -> dict[str, object]:
"""Build a stable reviewer packet for one pending operation."""
return {
Expand Down Expand Up @@ -101,6 +116,8 @@ def dispatch_pending_approval(
payload = build_review_packet(operation)
try:
write_json(artifact_path, payload)
_normalize_permissions(artifact_path.parent, _OWNER_ONLY_DIR_MODE)
_normalize_permissions(artifact_path, _OWNER_ONLY_FILE_MODE)
updated = journal.transition(
operation.op_id,
"pending_approval",
Expand Down
8 changes: 7 additions & 1 deletion src/clawops/assets/platform/docs/CI_AND_SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ any required lane does not complete successfully.

## Fresh-host acceptance

`.github/workflows/fresh-host-acceptance.yml` exercises the real bootstrap, setup, service activation, and repo-local sidecar/browser-lab flows on hosted Linux and macOS runners.
`.github/workflows/fresh-host-acceptance.yml` exercises the real bootstrap, setup, service activation, and repo-local sidecar/browser-lab flows on hosted Linux and macOS runners. It delegates the reusable execution lane to `.github/workflows/fresh-host-core.yml`.

- Each run writes a GitHub job summary with the runner label, runtime provider,
cache toggles, phase timings, and the effective hosted macOS Colima sizing.
Expand All @@ -61,6 +61,7 @@ sidecars and browser-lab mutable data live in Docker-managed volumes instead of
macOS path without changing the required PR gate.
- The workflow stays declarative by delegating runtime setup, image warming,
diagnostics, and summary generation to executable helper scripts under `tests/scripts/`. Hosted macOS image warming restores a cached Docker image archive when available, then verifies compose image availability with bounded retries and heartbeat logging as a fallback.
- `.github/workflows/nightly.yml` warms the fresh-host caches before it calls the reusable fresh-host core lane for the scheduled validation sweep.
- Repository workflow contract tests verify that shell steps invoking
`tests/scripts/*.py` either call an explicit Python interpreter or target an executable script, so nightly cache warming cannot silently regress on file mode drift.

Expand Down Expand Up @@ -112,6 +113,11 @@ raw tar extraction, traversal-prone archive-member joins, `subprocess`
`shell=True`, and unsafe deserialization helpers.
- `.github/workflows/security.yml` verifies the pinned `gitleaks` and `syft`
tarball SHA-256 digests before extracting the binaries through the dedicated helper script.
- That same security lane now executes two operational smoke checks through
`tests/scripts/security_workflow.py`: channel rollout contract parity
(`verify-channels-contract`) plus a disposable backup/verify/restore cycle
(`run-recovery-smoke`) so launch-critical channel and recovery paths produce
executable CI evidence.
- `.github/workflows/release.yml` now blocks publication on three repo-controlled
prerequisites: the centralized release quality gate, the reusable fresh-host
acceptance workflow, and the reusable memory-plugin verification workflow. It
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ Treat `~/.openclaw/clawops` as service-owned state.

- keep directory mode `0700` on `~/.openclaw/clawops`
- keep file mode `0600` on `~/.openclaw/clawops/op_journal.sqlite`
- keep reviewer packet directories mode `0700` and packet files mode `0600`
- `clawops` normalizes those journal and reviewer-artifact permissions on supported POSIX hosts when it creates or updates them
- do not grant write access to lower-trust workers or shared workspaces

## CLI flow
Expand Down
62 changes: 45 additions & 17 deletions src/clawops/assets/platform/docs/PRODUCTION_READINESS_CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,51 @@
# Production Readiness Checklist

- [ ] dedicated OS user
- [ ] loopback-bound gateway
- [ ] token auth enabled
Use this checklist as the operator-facing go/no-go gate after setup succeeds and
before you treat a host as launch-ready.

## 1. Host and gateway boundary

- [ ] dedicated OS user owns the StrongClaw/OpenClaw runtime
- [ ] gateway stays loopback-bound and is only reached through an SSH tunnel
- [ ] token auth is enabled
- [ ] `session.dmScope = per-channel-peer`
- [ ] sandbox mode `all`
- [ ] elevated exec disabled
- [ ] plugins/skills default-deny
- [ ] sidecars healthy
- [ ] `openclaw doctor` clean
- [ ] `openclaw security audit --deep` clean
- [ ] `openclaw secrets audit --check` clean
- [ ] operation journal initialized
- [ ] policy regression suite green
- [ ] backup and restore tested
- [ ] channel allowlists durable
- [ ] browser lab isolated or disabled
- [ ] if browser-lab is enabled, run `clawops verify-platform browser-lab` and confirm loopback-only bindings (`clawops baseline verify` includes browser-lab by default; use `--exclude-browser-lab` only when browser-lab is intentionally out of scope)
- [ ] remote operator access uses SSH tunnel to gateway only
- [ ] sandbox mode is `all`
- [ ] elevated exec is disabled
- [ ] plugins and skills stay default-deny until reviewed

## 2. Release-ready verification commands

Run these command-backed checks and keep the resulting evidence with the host
handoff or launch packet:

- [ ] `clawops doctor` is clean
- [ ] `clawops baseline verify` is clean
- [ ] `clawops verify-platform sidecars` is clean
- [ ] `clawops verify-platform observability` is clean
- [ ] `clawops verify-platform channels` is clean when rollout includes operator channels
- [ ] `openclaw doctor` is clean
- [ ] `openclaw security audit --deep` is clean
- [ ] `openclaw secrets audit --check` is clean

`clawops doctor-host` and `clawops doctor --skip-runtime --no-model-probe` are
useful host-only or degraded diagnostics, but they are **not** launch-ready or
release-ready substitutes for `clawops doctor`.

## 3. Recovery and durability

- [ ] operation journal is initialized and writable
- [ ] `clawops recovery backup-create` succeeds against the production home
- [ ] `clawops recovery backup-verify <archive>` succeeds for a freshly created archive
- [ ] `clawops recovery restore <archive> <clean-home>` has been tested on a clean destination
- [ ] backup retention is configured and reviewed
- [ ] channel allowlists are durable and versioned

## 4. Optional-but-exposed launch surfaces

- [ ] browser-lab is either disabled or isolated to a separate host or hardened OS user
- [ ] if browser-lab is enabled, `clawops verify-platform browser-lab` is clean and browser-lab ports remain loopback-only
- [ ] if browser-lab is intentionally out of scope for this rollout, `clawops baseline verify --exclude-browser-lab` is the documented gate result for the launch packet
- [ ] remote operator access uses an SSH tunnel to the gateway only; never tunnel browser-lab ports such as `9222` or `3128`

`clawops repo doctor` remains an operator/development contract for `repo/upstream`
and managed worktree flows. It is not part of the production baseline or launch
Expand Down
17 changes: 17 additions & 0 deletions src/clawops/op_journal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import argparse
import dataclasses
import json
import os
import pathlib
import sqlite3
import time
Expand Down Expand Up @@ -196,6 +197,8 @@
_RETRYABLE_OPEN_ERROR = "unable to open database file"
_CONNECT_ATTEMPTS = 3
_CONNECT_RETRY_DELAY_SECONDS = 0.01
_OWNER_ONLY_DIR_MODE = 0o700
_OWNER_ONLY_FILE_MODE = 0o600


def _is_retryable_open_error(error: sqlite3.OperationalError) -> bool:
Expand Down Expand Up @@ -224,6 +227,18 @@ def _merge_review_payload(
return canonical_json(payload)


def _normalize_permissions(path: pathlib.Path, mode: int) -> None:
"""Apply owner-only permissions on supported hosts."""
if os.name == "nt":
return
try:
current_mode = path.stat().st_mode & 0o777
except FileNotFoundError:
return
if current_mode != mode:
path.chmod(mode)


@dataclasses.dataclass(slots=True)
class Operation:
"""Operation journal row."""
Expand Down Expand Up @@ -316,6 +331,7 @@ def __init__(self, db_path: pathlib.Path) -> None:
def connect(self) -> sqlite3.Connection:
"""Open a SQLite connection with row access by name."""
self.db_path.parent.mkdir(parents=True, exist_ok=True)
_normalize_permissions(self.db_path.parent, _OWNER_ONLY_DIR_MODE)
for attempt in range(1, _CONNECT_ATTEMPTS + 1):
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
Expand All @@ -327,6 +343,7 @@ def connect(self) -> sqlite3.Connection:
raise
time.sleep(_CONNECT_RETRY_DELAY_SECONDS * attempt)
continue
_normalize_permissions(self.db_path, _OWNER_ONLY_FILE_MODE)
return conn
raise AssertionError("unreachable: connect loop returned no SQLite connection")

Expand Down
Loading
Loading