# Sandbox CI: the keyless real-kernel confinement proofs. A separate workflow
# from ci.yml because the axis is different — these jobs fan out over
# OS×runner (kernel capabilities), not node versions. The Landlock launcher
# arrives from the registry with `pnpm install` (the npm package family
# `node-addon-landlock-run`, built and released from its own repository), so
# these legs exercise the true consumer path — nothing is compiled here.
name: Sandbox

on:
  push:
    branches: [main, master]
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  # Keyless real-kernel sandbox proofs (sandbox Agent Note § Testing): each ladder
  # rung is only provable on a host where it enforces, so this job fans out
  # an OS×runner matrix — bwrap and Landlock on Linux (separate legs: the
  # Landlock files force the bwrap rung off, so each leg proves exactly one
  # rung; Landlock twice, once per architecture, each confining through the
  # registry-installed launcher), Seatbelt on macOS (sandbox-exec ships with
  # the OS). One node
  # version only: kernel confinement does not vary by node, and ci.yml's
  # node matrix already covers the node axis.
  #
  # The e2e files self-skip where their runner is absent, so a leg that lost
  # its runner (no bwrap, kernel without Landlock, macOS without
  # sandbox-exec) would otherwise pass as a false green — the same trap
  # e2e.yml's key preflight guards against. Each leg therefore asserts BOTH
  # its platform files actually ran: `Test Files  2 passed (2)`, no skips.
  sandbox-e2e:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            runner: bwrap
          - os: ubuntu-24.04
            runner: landlock
          - os: ubuntu-24.04-arm
            runner: landlock
          - os: macos-latest
            runner: seatbelt
    name: sandbox e2e (${{ matrix.runner }}, ${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-node@v6
        with:
          node-version: 24

      - name: Enable corepack (pnpm)
        run: corepack enable

      - name: Install (immutable)
        run: pnpm install --frozen-lockfile

      # The bwrap rung needs bubblewrap on PATH and unprivileged user
      # namespaces. Ubuntu 24.04 gates the latter behind an AppArmor knob;
      # lift it best-effort — on images where the knob is absent the
      # functional probe (and the run-guard below) is the arbiter anyway.
      - name: Install bubblewrap (unrestrict userns)
        if: matrix.runner == 'bwrap'
        run: |
          sudo apt-get update -q
          sudo apt-get install -yq bubblewrap
          sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 \
            || echo "apparmor userns knob absent — the functional probe decides"

      # The unit suite runs on ubuntu in `checks`; this is the one darwin leg
      # in the workflow, so run it here too — the platform-dependent unit
      # expectations (Seatbelt path canonicalization: /tmp IS /private/tmp)
      # take their darwin branch only on this runner.
      - name: Unit tests (darwin parity)
        if: matrix.runner == 'seatbelt'
        run: pnpm run test

      - name: Sandbox e2e (real kernel confinement, world-verified)
        # NO_COLOR: vitest force-enables ANSI color under GITHUB_ACTIONS even
        # without a TTY, which would thread escape codes through the summary
        # line the run-guard greps.
        env:
          NO_COLOR: 1
        run: |
          set -u +e -o pipefail
          out=$(pnpm exec vitest run --config vitest.e2e.config.ts \
            packages/sandbox/sandbox-local/tests/${{ matrix.runner }}.e2e.ts \
            packages/bash/bash-sandbox/tests/${{ matrix.runner }}.e2e.ts 2>&1); status=$?
          echo "$out"
          [ "$status" -eq 0 ]
          # Both platform files must have RUN — a self-skip (runner missing on
          # the very platform that exists to prove it) is a failure, not a pass.
          echo "$out" | grep -qE 'Test Files[[:space:]]+2 passed \(2\)'

      # Publish-path rehearsal, Landlock legs only (the pack gates need built
      # lib/). The e2e packs the workspace closure, installs the tarballs
      # into a throwaway consumer — npm pulling `node-addon-landlock-run`
      # and its platform package from the registry, the true consumer path —
      # and confines through the INSTALLED launcher, asserting it executable
      # apart (a mode-stripped binary must not masquerade as a non-enforcing
      # kernel). Same no-silent-skip guard as above.
      - name: Build packages (lib/ for the pack rehearsal)
        if: matrix.runner == 'landlock'
        run: pnpm run build

      - name: Packed-distribution e2e (pack → install → confine)
        if: matrix.runner == 'landlock'
        env:
          NO_COLOR: 1
        run: |
          set -u +e -o pipefail
          out=$(pnpm exec vitest run --config vitest.e2e.config.ts \
            packages/sandbox/sandbox-local/tests/packed-install.e2e.ts 2>&1); status=$?
          echo "$out"
          [ "$status" -eq 0 ]
          echo "$out" | grep -qE 'Test Files[[:space:]]+1 passed \(1\)'
