Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions .github/workflows/podman-cpu-proof.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ on:
- "src/lib/onboard/managed-bootstrap/podman-*.ts"
- "src/lib/onboard/experimental/portable-cpu-delegation-preflight*.ts"
- "src/lib/onboard/experimental/portable-demo-lifecycle.ts"
- "src/lib/onboard/experimental/portable-runtime-*.ts"
- "src/lib/onboard/portable-retirement-authority.ts"
- "src/lib/onboard.ts"
- "src/lib/actions/uninstall/portable-runtime-cleanup.ts"
- "src/lib/actions/uninstall/all-gateway-ports.ts"
- "src/lib/actions/uninstall/run-plan.ts"
- "src/commands/internal/uninstall/run-plan.ts"
- "src/lib/state/portable-uninstall-retirement.ts"
- "src/lib/state/registry/lock.ts"
- "src/lib/onboard/runtime-provider/container-state-mutation.ts"
- "src/lib/onboard/runtime-provider/docker-state-mutation.ts"
- "src/lib/onboard/experimental/portable-host-preparation*.ts"
Expand All @@ -26,6 +35,7 @@ on:
- "test/e2e/live/podman-cpu-lifecycle-helpers.ts"
- "test/e2e/live/podman-cpu-lifecycle-policy.yaml"
- "test/e2e/live/podman-cpu-lifecycle.test.ts"
- "test/e2e/live/podman-portable-uninstall.test.ts"
- "test/e2e/registry/native-runtime-qualification.ts"
- "test/e2e/support/native-runtime-qualification.test.ts"
- "test/e2e/live/portable-cpu-delegation-proof.test.ts"
Expand Down Expand Up @@ -130,8 +140,11 @@ jobs:
- name: Install locked test dependencies
run: npm ci --ignore-scripts

- name: Build shared sandbox-name contract
run: npm run build:policy-boundary
- name: Build and link candidate CLI
run: |
npm run build:cli
npm link --ignore-scripts
nemoclaw --version

- name: Install pinned OpenShell runtime
shell: bash
Expand Down Expand Up @@ -321,8 +334,28 @@ jobs:
printf 'XDG_RUNTIME_DIR=%s\n' "$runtime_dir"
} >>"$GITHUB_ENV"

- name: Start the exact connected gateway required by portable retirement
shell: bash
run: |
set -euo pipefail
gateway_state="$RUNNER_TEMP/nemoclaw-podman-uninstall-gateway"
export DOCKER_HOST="unix://$E2E_PODMAN_SOCKET"
export NEMOCLAW_EXPERIMENTAL_PROFILE=portable
export NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR="$gateway_state"
{
printf 'NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR=%s\n' "$gateway_state"
printf 'OPENSHELL_LOCAL_TLS_DIR=%s/tls\n' "$gateway_state"
} >>"$GITHUB_ENV"
node --input-type=module --eval '
const { startDockerDriverGateway } = (await import("./dist/lib/onboard.js")).default;
await startDockerDriverGateway({ skipSandboxBridgeReachability: true });
'

- name: Prove pinned OpenShell activation and registered-agent Podman CPU lifecycle
run: npx vitest run --project e2e-live test/e2e/live/podman-cpu-lifecycle.test.ts
run: |
npx vitest run --project e2e-live \
test/e2e/live/podman-cpu-lifecycle.test.ts \
test/e2e/live/podman-portable-uninstall.test.ts

- name: Verify Docker stayed unavailable
if: always()
Expand Down Expand Up @@ -367,6 +400,36 @@ jobs:
podman --url "$endpoint" secret ls --format '{{.Name}}' \
>"$diagnostic_dir/podman-secrets.txt" 2>&1 || true

- name: Stop the exact portable-retirement proof gateway
if: always()
shell: bash
env:
E2E_PORTABLE_GATEWAY_STOP_SCOPE: full
run: |
set -euo pipefail
gateway_state="$RUNNER_TEMP/nemoclaw-podman-uninstall-gateway"
test "${NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR:-$gateway_state}" = "$gateway_state"
export NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR="$gateway_state"
gateway_bin="$(command -v openshell-gateway)"
E2E_OPENSHELL_GATEWAY_BIN="$gateway_bin" node --input-type=module --eval '
const { stopHostGatewayProcesses } =
await import("./dist/lib/onboard/host-gateway-process.js");
const result = stopHostGatewayProcesses({}, {
gatewayBin: process.env.E2E_OPENSHELL_GATEWAY_BIN,
openShellGatewayName: "nemoclaw",
openShellGatewayPort: 8080,
scopedGatewayStop: process.env.E2E_PORTABLE_GATEWAY_STOP_SCOPE !== "full",
stateDir: process.env.NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR,
usePgrepFallback: false,
});
if (result.failed.length || result.ownershipFailures?.length) {
console.error(JSON.stringify(result));
process.exitCode = 1;
}
'
openshell gateway remove nemoclaw
rm -rf -- "$gateway_state"

- name: Clean up rootless Podman runtime
if: always()
shell: bash
Expand Down
22 changes: 22 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,28 @@ Subscribe to notifications on that page to receive alerts when new bulletins are

Component-level threat models for security-critical NemoClaw subsystems are documented here so a reviewer or auditor can understand what each subsystem is designed to prevent, which surfaces it protects, and where its guarantees end.

### Portable Uninstall Retirement Record (`#9189`)

**Summary.** Portable uninstall retains `~/.nemoclaw/portable-uninstall-retirement.json` after `--destroy-user-data`.
The current user owns its mode-`0700` parent and the mode-`0600` record.
A later completed onboarding removes the record only after its new authority is durable.

**Threat.** A host crash can interrupt receipt, sandbox registry, or portable configuration removal.
Without a durable discriminator, a retry can enter generic Docker, OpenShell, or model cleanup and remove resources outside the portable receipts.

**Guarantee.** The record keeps every retry on receipt-owned portable cleanup.
It contains a random cleanup ID, receipt basenames derived from SHA-256 hashes of sandbox names, safe relative target identities, and length-framed transaction-scoped SHA-256 content fingerprints.
The fingerprints are dictionary-testable pseudonymous local data.
The record contains no raw sandbox or gateway names, absolute paths, environment values, configuration bytes, credentials, or secrets.
NemoClaw holds one process-bound host fence across every cooperative onboarding, rebuild, and uninstall writer while it publishes or supersedes the record.

**Where the guarantee ends.** NemoClaw state owned by the same operating-system user is not a trust boundary against a malicious process running as that user.
Such a process can change the state before or after a checked filesystem operation.
NemoClaw detects mismatched file identities and fingerprints and exits without restoring or removing the ambiguous generation.
This control covers cooperating NemoClaw processes, crashes, retries, and recycled process IDs.

**Enforced by:** `src/lib/state/portable-uninstall-retirement.test.ts`, `src/lib/actions/uninstall/portable-runtime-cleanup.test.ts`, `src/lib/onboard/portable-resume-lock-boundary.test.ts`, and `src/lib/state/registry-lock.test.ts` cover crash boundaries, async ownership, lock generations, record contents, and completed-onboarding supersession.

### Ollama Auth Proxy Loopback Bind Probe (`#6014`)

**Summary.** The Ollama auth proxy is the token-authenticated network gate in front of a locally-running Ollama backend on every topology where `shouldFrontOllamaWithProxy()` returns true (native Linux, macOS, WSL with a native dockerd runtime). Ollama itself has no built-in authentication. The proxy adds a bearer-token check on its own listen port and forwards to Ollama on the backend port.
Expand Down
13 changes: 6 additions & 7 deletions ci/source-architecture-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"src/lib/onboard/gateway-binding.ts": 52,
"src/lib/runner.ts": 87,
"src/lib/security/redact.ts": 52,
"src/lib/state/onboard-session.ts": 36,
"src/lib/state/onboard-session.ts": 37,
"src/lib/state/registry.ts": 101,
"src/lib/state/state-root.ts": 21,
"src/lib/subprocess-env.ts": 24,
Expand All @@ -43,25 +43,24 @@
"src/lib/actions/sandbox/status-snapshot.ts": 21,
"src/lib/actions/sandbox/policy-channel.ts": 30,
"src/lib/actions/sandbox/process-recovery.ts": 21,
"src/lib/actions/sandbox/rebuild-pipeline.ts": 28,
"src/lib/actions/sandbox/rebuild-pipeline.ts": 29,
"src/lib/actions/sandbox/snapshot.ts": 40,
"src/lib/actions/uninstall/run-plan.ts": 26,

"src/lib/actions/uninstall/run-plan.ts": 25,
"src/lib/inference/local.ts": 21,
"src/lib/inference/onboard-probes.ts": 21,
"src/lib/inference/vllm.ts": 21,
"src/lib/onboard.ts": 202,
"src/lib/onboard.ts": 203,
"src/lib/onboard/machine/handlers/sandbox.ts": 21,
"src/lib/sandbox/config.ts": 22,
"src/lib/shields/index.ts": 23
}
},
"allowedCycles": [],
"maxRootFiles": {
"src/lib/onboard": 307,
"src/lib/onboard": 308,
"src/lib/actions": 19,
"src/lib/actions/sandbox": 183,
"src/lib/state": 38,
"src/lib/state": 39,
"src/lib/inference": 63,
"scripts": 47
}
Expand Down
Loading
Loading