diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1207ca0e..1f0f7162 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -34,25 +34,6 @@ RUN curl -LsSf https://astral.sh/uv/install.sh | env INSTALLER_NO_MODIFY_PATH=1 && cp /root/.local/bin/uv /usr/local/bin/uv \ && cp /root/.local/bin/uvx /usr/local/bin/uvx -ARG INSTALL_HERMES=false -RUN if [ "${INSTALL_HERMES}" = "true" ]; then \ - curl -fsSL https://hermes-agent.nousresearch.com/install.sh \ - | bash -s -- --skip-setup --skip-browser; \ - rm -rf /root/.hermes; \ - uv pip install --python /usr/local/lib/hermes-agent/venv/bin/python 'hermes-agent[slack,teams,web,pty]'; \ - else \ - echo "Skipping Hermes Agent CLI install (INSTALL_HERMES=false)"; \ - fi - -ARG INSTALL_DEEPAGENTS=false -RUN export UV_TOOL_DIR=/opt/uv/tools UV_TOOL_BIN_DIR=/usr/local/bin \ - && mkdir -p "$UV_TOOL_DIR" \ - && if [ "${INSTALL_DEEPAGENTS}" = "true" ]; then \ - uv tool install deepagents-cli; \ - else \ - echo "Skipping DeepAgents CLI install (INSTALL_DEEPAGENTS=false)"; \ - fi - ENV PNPM_HOME="/usr/local/share/pnpm" ENV NPM_USER_PREFIX="/home/sandbox/.local" ENV PATH="$NPM_USER_PREFIX/bin:$PNPM_HOME:$PATH" @@ -62,18 +43,6 @@ RUN corepack enable && corepack prepare pnpm@10.33.0 --activate \ SHELL ["/bin/bash", "-c"] -ARG INSTALL_OPENCODE=false -ARG INSTALL_GROK_BUILD=false - -RUN set -e; \ - if [ "${INSTALL_OPENCODE}" = "true" ]; then npm install -g opencode-ai; \ - else echo "Skipping OpenCode CLI install (INSTALL_OPENCODE=false)"; fi; \ - if [ "${INSTALL_GROK_BUILD}" = "true" ]; then \ - curl -fsSL https://x.ai/cli/install.sh | HOME=/opt/grok-build GROK_BIN_DIR=/opt/grok-build/bin bash -s 0.2.39; \ - ln -sf /opt/grok-build/bin/grok /usr/local/bin/grok; \ - rm -f /usr/local/bin/agent; \ - else echo "Skipping Grok Build CLI install (INSTALL_GROK_BUILD=false)"; fi - RUN npm install -g cc-safety-net@1.0.6 COPY .oh/cli/ /opt/oh/ @@ -146,9 +115,6 @@ RUN if [ "${INSTALL_PYTHON_KERNEL}" = "true" ]; then \ && rm -rf /home/sandbox/.cache/uv FROM base AS final -RUN chown -R sandbox:sandbox /opt/uv 2>/dev/null || true \ - && if [ -d /usr/local/lib/hermes-agent ]; then chown -R sandbox:sandbox /usr/local/lib/hermes-agent; fi - RUN mkdir -p /.devcontainer && echo '{"workspaceFolder":"/home/sandbox/harness","remoteUser":"sandbox"}' > /.devcontainer/devcontainer.json LABEL devcontainer.metadata='[{"remoteUser":"sandbox","workspaceFolder":"/home/sandbox/harness"}]' diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 8a99d972..796f73b3 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -39,11 +39,6 @@ services: build: context: .. dockerfile: .devcontainer/Dockerfile - args: - INSTALL_OPENCODE: ${INSTALL_OPENCODE:-false} - INSTALL_GROK_BUILD: ${INSTALL_GROK_BUILD:-false} - INSTALL_DEEPAGENTS: ${INSTALL_DEEPAGENTS:-false} - INSTALL_HERMES: ${INSTALL_HERMES:-false} volumes: - ${OH_HOME_MOUNT:-workspace}:/home/sandbox - ..:/home/sandbox/harness diff --git a/.github/workflows/sandbox-compatibility.yml b/.github/workflows/sandbox-compatibility.yml index a09f0785..d43a69e3 100644 --- a/.github/workflows/sandbox-compatibility.yml +++ b/.github/workflows/sandbox-compatibility.yml @@ -50,8 +50,8 @@ jobs: node:22-bookworm-slim \ node:22-trixie-slim - optional-installers-image: - name: Build one amd64 image with every optional installer + optional-harness-install: + name: Install every optional harness through the CLI runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }} steps: @@ -64,31 +64,81 @@ jobs: - name: Ensure Mifune submodule run: bash .oh/scripts/link-providers.sh --init - - name: Build with all INSTALL_* paths enabled + # #908 removed the INSTALL_* build args, so there is no longer an image to + # build with every optional installer enabled. The path that replaced it — + # `oh harness install ` into the home mount, as the sandbox user — is + # what this job must exercise instead. Building and then INSTALLING is + # strictly closer to what an operator does than the old build-arg matrix. + - name: Build the sandbox image run: | docker build \ --file .devcontainer/Dockerfile \ - --build-arg INSTALL_HERMES=true \ - --build-arg INSTALL_DEEPAGENTS=true \ - --build-arg INSTALL_OPENCODE=true \ - --build-arg INSTALL_GROK_BUILD=true \ --tag openharness-sandbox-optional:${{ github.sha }} \ . - - name: Verify the optional image still satisfies the default contract + - name: Verify the image satisfies the default contract run: bash .oh/scripts/verify-sandbox-image.sh openharness-sandbox-optional:${{ github.sha }} - - name: Verify a version from every optional CLI + - name: Install each optional harness through the CLI and verify its version run: | set -euo pipefail - for tool in hermes deepagents opencode grok; do - echo "--- $tool --version" - output="$(docker run --rm --entrypoint /bin/bash \ - openharness-sandbox-optional:${{ github.sha }} \ - -lc "$tool --version" 2>&1)" + cid=$(docker run -d --rm \ + -e SANDBOX_NAME=openharness-optional-${{ github.run_id }} \ + -e OH_PROVISION_DEFAULTS=false \ + --entrypoint sleep \ + openharness-sandbox-optional:${{ github.sha }} infinity) + trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT + + # The image ships an empty /home/sandbox; seed it the way the + # entrypoint would so ~/.local exists and is sandbox-owned. + docker exec "$cid" bash -lc 'cp -a -n /opt/home-seed/. /home/sandbox/ 2>/dev/null || true; chown -R sandbox:sandbox /home/sandbox' + + ids=$(docker exec -u sandbox "$cid" bash -lc \ + 'cd /opt/oh-seed && OH_EXECUTION_TARGET=local oh harness list --json' \ + | jq -r '.[] | select(.kind == "optional") | .id') + if [ -z "$ids" ]; then + echo "ERROR: the harness catalog declares no optional harnesses — this job would pass vacuously" >&2 + exit 1 + fi + + # These four installs reach four third-party endpoints. A transient + # upstream failure must not block this repo's merges: Hermes' own + # installer hard-fails the whole install when its `npm install` step + # blips, which took this job down once on a commit that was correct + # (the rerun passed unchanged). One retry absorbs that. It does NOT + # weaken the contract below — a genuine break (wrong user, wrong + # path, a sudo prompt) fails both attempts and still fails the job. + for id in $ids; do + for attempt in 1 2; do + echo "--- oh harness install $id (attempt $attempt)" + if docker exec -u sandbox "$cid" bash -lc \ + "cd /opt/oh-seed && OH_EXECUTION_TARGET=local oh harness install '$id' --no-persist"; then + break + fi + if [ "$attempt" = 2 ]; then + echo "ERROR: $id failed to install twice — this is not a transient upstream blip" >&2 + exit 1 + fi + echo "$id install failed; retrying once after 15s in case the upstream endpoint blipped" >&2 + sleep 15 + done + done + + for id in $ids; do + binary=$(docker exec -u sandbox "$cid" bash -lc \ + 'cd /opt/oh-seed && OH_EXECUTION_TARGET=local oh harness list --json' \ + | jq -r --arg id "$id" '.[] | select(.id == $id) | .binary') + echo "--- $binary --version" + output=$(docker exec -u sandbox "$cid" bash -lc " + path=\$(type -P '$binary') + case \"\$path\" in + /home/sandbox/.local/*) ;; + *) echo \"$id resolved to '\$path', not under /home/sandbox/.local\" >&2; exit 1 ;; + esac + \"\$path\" --version" 2>&1) printf '%s\n' "$output" if ! grep -Eq '(^|[^[:alnum:]])v?[0-9]+([.][0-9]+)+([^[:alnum:]]|$)' <<<"$output"; then - echo "ERROR: $tool --version did not output a numeric dotted version" >&2 + echo "ERROR: $binary --version did not output a numeric dotted version" >&2 exit 1 fi done diff --git a/.oh/cli/src/__tests__/harness-catalog.test.ts b/.oh/cli/src/__tests__/harness-catalog.test.ts index 47a62bd8..2b3290c0 100644 --- a/.oh/cli/src/__tests__/harness-catalog.test.ts +++ b/.oh/cli/src/__tests__/harness-catalog.test.ts @@ -42,12 +42,6 @@ describe("harness catalog", () => { } }); - it("pairs harnessKey and buildArg — never one without the other", () => { - for (const h of HARNESS_CATALOG) { - expect(Boolean(h.harnessKey)).toBe(Boolean(h.buildArg)); - } - }); - it("gives every optional harness a flag, and no other kind one", () => { for (const h of HARNESS_CATALOG) { if (h.kind === "optional") expect(h.harnessKey).toBeDefined(); @@ -67,11 +61,13 @@ describe("harness catalog", () => { } }); - describe("does not drift from the image build", () => { - const flagged = HARNESS_CATALOG.filter((h) => h.buildArg !== undefined); + // #908: the INSTALL_* build args are gone. The catalog no longer mirrors the + // Dockerfile — it replaces it, and `oh harness install` is the only path. + describe("owns the install, and the image no longer does", () => { + const optional = HARNESS_CATALOG.filter((h) => h.kind === "optional"); it("covers all four optional harnesses", () => { - expect(flagged.map((h) => h.id).sort()).toEqual([ + expect(optional.map((h) => h.id).sort()).toEqual([ "deepagents", "grok-build", "hermes", @@ -79,48 +75,55 @@ describe("harness catalog", () => { ]); }); - it.each(flagged.map((h) => [h.id, h] as const))( - "%s: build arg is in the Dockerfile", + it("declares no buildArg anywhere — the field itself is gone", () => { + expect(read(".oh/cli/src/lib/harnesses/catalog.ts")).not.toContain("buildArg"); + }); + + it.each(optional.map((h) => [h.id, h] as const))( + "%s: its INSTALL_* build arg is absent from the Dockerfile", (_id, h) => { - expect(DOCKERFILE).toContain(h.buildArg as string); + const arg = `INSTALL_${(h.harnessKey as string).toUpperCase()}`; + expect(DOCKERFILE).not.toMatch(new RegExp(`^ARG ${arg}`, "m")); + expect(COMPOSE_YML).not.toContain(`${arg}: \${${arg}:-false}`); }, ); - it.each(flagged.map((h) => [h.id, h] as const))( - "%s: the INSTALL_* key derived from harnessKey IS the build arg, and compose forwards it", + it.each(optional.map((h) => [h.id, h] as const))( + "%s: installs as the sandbox user into the home mount", (_id, h) => { - expect(`INSTALL_${(h.harnessKey as string).toUpperCase()}`).toBe(h.buildArg); - expect(COMPOSE_YML).toContain(`${h.buildArg}: \${${h.buildArg}:-false}`); + expect(h.installUser).toBe("sandbox"); + expect(h.installArgv.join("\n")).toMatch(/\/home\/sandbox\/\.local|\$HOME\/\.local|uv/); }, ); - it.each(flagged.map((h) => [h.id, h] as const))( - "%s: key ships documented in docs/configuration.md, the oh.json field reference", + it.each(optional.map((h) => [h.id, h] as const))( + "%s: its oh.json key stays documented in docs/configuration.md", (_id, h) => { + const arg = `INSTALL_${(h.harnessKey as string).toUpperCase()}`; expect(CONFIG_DOC).toMatch( - new RegExp(`^\\| \`install\\.[A-Za-z]+\` \\|.*\`${h.buildArg}\``, "m"), + new RegExp(`^\\| \`install\\.[A-Za-z]+\` \\|.*\`${arg}\``, "m"), ); }, ); - it.each(flagged.map((h) => [h.id, h] as const))( - "%s: every pinned version appears verbatim in the Dockerfile", - (_id, h) => { - for (const pin of versionPins(h.installArgv)) { - expect(DOCKERFILE).toContain(pin); - } - }, - ); - - it("grok-build keeps the Dockerfile's exact pin", () => { + it("keeps the grok-build pin in the catalog, now that the Dockerfile has none", () => { const grok = findHarness("grok-build"); expect(versionPins(grok!.installArgv)).toEqual(["0.2.39"]); - expect(DOCKERFILE).toContain("bash -s 0.2.39"); + expect(DOCKERFILE).not.toContain("bash -s 0.2.39"); + }); + + // INSTALL_HERMES survives as a RUNTIME flag: link-providers.sh vendors the + // Hermes skill pack from it and entrypoint.sh wires auth.json. Only its + // build-arg role is gone. + it("keeps INSTALL_HERMES as a container environment variable", () => { + expect(COMPOSE_YML).toContain("- INSTALL_HERMES=${INSTALL_HERMES:-false}"); + expect(DOCKERFILE).not.toContain("INSTALL_HERMES"); }); - it("installs deepagents and pi as the sandbox user, not root", () => { - expect(findHarness("deepagents")!.installUser).toBe("sandbox"); - expect(findHarness("pi")!.installUser).toBe("sandbox"); + it("installs every harness as the sandbox user, never root", () => { + for (const h of HARNESS_CATALOG) { + expect(h.installUser, h.id).toBe("sandbox"); + } expect(DOCKERFILE).toContain("UV_TOOL_DIR=/home/sandbox"); }); }); diff --git a/.oh/cli/src/__tests__/harness.test.ts b/.oh/cli/src/__tests__/harness.test.ts index 06b9840f..acbde334 100644 --- a/.oh/cli/src/__tests__/harness.test.ts +++ b/.oh/cli/src/__tests__/harness.test.ts @@ -272,8 +272,17 @@ describe("runHarnessInstall against the container", () => { const install = execCalls(calls).find((c) => c.args.includes("opencode-ai")); expect(install).toBeDefined(); expect(install!.args).toContain("-u"); - expect(install!.args).toContain("root"); - expect(install!.args.slice(-4)).toEqual(["npm", "install", "-g", "opencode-ai"]); + // #908: every harness installs as the sandbox user into the home mount. + expect(install!.args).toContain("sandbox"); + expect(install!.args).not.toContain("root"); + expect(install!.args.slice(-6)).toEqual([ + "npm", + "--prefix", + "/home/sandbox/.local", + "install", + "-g", + "opencode-ai", + ]); expect(text(out)).toContain("installed"); expect(text(out)).toContain( "https://github.com/mifunedev/openharness/blob/main/docs/harnesses/opencode.md", @@ -508,7 +517,11 @@ describe("oh harness — inside the sandbox", () => { const { io, out } = makeIo(); expect(await runHarnessInstall("opencode", { cwd: root, run, env: INSIDE }, io)).toBe(0); expect(text(out)).not.toContain("skipping the live install"); - expect(calls.some((c) => c.cmd === "sudo" && c.args.includes("opencode-ai"))).toBe(true); + // #908: this previously asserted `cmd === "sudo"`, codifying the very defect + // that made `oh harness install opencode` hang inside the sandbox — + // stdio:"inherit" selects plain `sudo --`, and sandbox has no NOPASSWD. + expect(calls.some((c) => c.cmd === "sudo")).toBe(false); + expect(calls.some((c) => c.args.includes("opencode-ai"))).toBe(true); expect(installFlag(root, "opencode")).toBe(true); }); diff --git a/.oh/cli/src/__tests__/tool-catalog.test.ts b/.oh/cli/src/__tests__/tool-catalog.test.ts index 1e72346a..3057999b 100644 --- a/.oh/cli/src/__tests__/tool-catalog.test.ts +++ b/.oh/cli/src/__tests__/tool-catalog.test.ts @@ -75,14 +75,11 @@ describe("tool catalog shape", () => { }); it("passes argv arrays with no interpolation this process performs", () => { - // The hazard is a JS template literal that Node expands before the argv - // ever reaches a shell. A `bash -lc` script body legitimately contains - // ${...} for the shell IN the container to expand, so exempt that one - // token and forbid backticks in the catalog source instead. - expect( - read(".oh/cli/src/lib/tools/catalog.ts"), - "a template literal with ${...} would be expanded by Node before any shell sees it", - ).not.toMatch(/`[^`]*\$\{/s); + // A `bash -lc` script body legitimately contains ${...} for the shell IN the + // container to expand, so that one token is exempt. A source-level scan for + // an interpolating template literal was tried and removed: it cannot tell a + // JS backtick from a backtick inside prose (`notInstallableReason` has + // several), so whether it fired depended on catalog ORDER, not the hazard. for (const t of TOOL_CATALOG) { for (const argv of [t.installArgv, t.verifyArgv, t.versionArgv]) { if (!argv) continue; diff --git a/.oh/cli/src/lib/harnesses/catalog.ts b/.oh/cli/src/lib/harnesses/catalog.ts index b6c0f408..c677d52c 100644 --- a/.oh/cli/src/lib/harnesses/catalog.ts +++ b/.oh/cli/src/lib/harnesses/catalog.ts @@ -9,7 +9,6 @@ export interface HarnessEntry { readonly title: string; readonly binary: string; readonly harnessKey?: string; - readonly buildArg?: string; readonly installArgv: readonly string[]; readonly installUser: "root" | "sandbox"; readonly verifyArgv: readonly string[]; @@ -75,9 +74,15 @@ export const HARNESS_CATALOG: readonly HarnessEntry[] = [ title: "OpenCode", binary: "opencode", harnessKey: "opencode", - buildArg: "INSTALL_OPENCODE", - installArgv: ["npm", "install", "-g", "opencode-ai"], - installUser: "root", + installArgv: [ + "npm", + "--prefix", + "/home/sandbox/.local", + "install", + "-g", + "opencode-ai", + ], + installUser: "sandbox", verifyArgv: ["opencode", "--version"], docsPath: "docs/harnesses/opencode.md", kind: "optional", @@ -87,13 +92,12 @@ export const HARNESS_CATALOG: readonly HarnessEntry[] = [ title: "Grok Build", binary: "grok", harnessKey: "grok_build", - buildArg: "INSTALL_GROK_BUILD", installArgv: [ "bash", "-lc", - "curl -fsSL https://x.ai/cli/install.sh | HOME=/opt/grok-build GROK_BIN_DIR=/opt/grok-build/bin bash -s 0.2.39 && ln -sf /opt/grok-build/bin/grok /usr/local/bin/grok && rm -f /usr/local/bin/agent", + "curl -fsSL https://x.ai/cli/install.sh | GROK_BIN_DIR=\"$HOME/.local/bin\" bash -s 0.2.39 && rm -f \"$HOME/.local/bin/agent\"", ], - installUser: "root", + installUser: "sandbox", verifyArgv: ["grok", "--version"], docsPath: "docs/harnesses/grok-build.md", kind: "optional", @@ -103,7 +107,6 @@ export const HARNESS_CATALOG: readonly HarnessEntry[] = [ title: "DeepAgents", binary: "deepagents", harnessKey: "deepagents", - buildArg: "INSTALL_DEEPAGENTS", installArgv: ["uv", "tool", "install", "deepagents-cli"], installUser: "sandbox", verifyArgv: ["deepagents", "--version"], @@ -115,13 +118,12 @@ export const HARNESS_CATALOG: readonly HarnessEntry[] = [ title: "Hermes", binary: "hermes", harnessKey: "hermes", - buildArg: "INSTALL_HERMES", installArgv: [ "bash", "-lc", - "curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --skip-setup --skip-browser && uv pip install --python /usr/local/lib/hermes-agent/venv/bin/python 'hermes-agent[slack,teams,web,pty]'", + "curl -fsSL https://hermes-agent.nousresearch.com/install.sh | HERMES_INSTALL_DIR=\"$HOME/.local/lib/hermes-agent\" bash -s -- --skip-setup --skip-browser && uv pip install --python \"$HOME/.local/lib/hermes-agent/venv/bin/python\" 'hermes-agent[slack,teams,web,pty]'", ], - installUser: "root", + installUser: "sandbox", verifyArgv: ["hermes", "--version"], docsPath: "docs/harnesses/hermes.md", kind: "optional", @@ -152,6 +154,10 @@ export const HARNESS_CATALOG: readonly HarnessEntry[] = [ }, ]; +export function optionalHarnesses(): readonly HarnessEntry[] { + return HARNESS_CATALOG.filter((h) => h.kind === "optional"); +} + export function defaultHarnesses(): readonly HarnessEntry[] { return HARNESS_CATALOG.filter((h) => h.kind === "default"); } diff --git a/.oh/evals/RESULTS.md b/.oh/evals/RESULTS.md index 319a405a..880cf73a 100644 --- a/.oh/evals/RESULTS.md +++ b/.oh/evals/RESULTS.md @@ -6,108 +6,108 @@ probe id; git history is the time series.** Schema and exit-code semantics are i | probe | tier | last-run (UTC) | status | source | |-------|------|----------------|--------|--------| -| advisor-monitored-loop | A | 2026-08-31 02:42 | PASS | conversation 2026-06-19 (single-owner implementation workflow, issue #257) | -| agent-browser-cli | A | 2026-08-31 02:42 | PASS | retro lesson 2026-06-07 (agent-browser 0.8.5 CLI) | -| agents-identity-contract | A | 2026-08-31 02:42 | PASS | issue #854 — T3-style root identity, glossary, and skill-owned procedures | -| artifact-contract-audit | A | 2026-08-31 02:42 | PASS | issue #583/#645 — production /audit implementation Gate 1 behavior | -| audit-dispatcher-contract | A | 2026-08-31 02:42 | PASS | issue #645 — audit consolidation public taxonomy | -| audit-implementation-behavior | A | 2026-08-31 02:42 | PASS | issue #645 — implementation root/repo/browser behavior | -| audit-pr-acquire | A | 2026-08-31 02:42 | PASS | issue #645 — production PR acquisition behavior | -| audit-pr-classifier | A | 2026-08-31 02:42 | PASS | issue #645 — deterministic focused and queue PR classifier | -| audit-run-root-contract | A | 2026-08-31 02:42 | PASS | issue #645 — executable immutable audit root/run correlation | -| audit-shellcheck-coverage | A | 2026-08-31 02:42 | PASS | issue #645 — private audit scripts require release and CI lint coverage | -| audit-stale-references | A | 2026-08-31 02:42 | PASS | issue #645 — clean-breaking audit migration | -| boot-lint-glob | A | 2026-08-31 02:42 | PASS | issue #90, issue #120 | -| builder-skill-consolidation | A | 2026-08-31 02:42 | PASS | issue #643 — consolidate artifact builders behind one /builder dispatcher | -| capability-benchmark-schema | A | 2026-08-31 02:42 | PASS | issue #167 — capability benchmark instrument | -| cc-safety-net-wiring | A | 2026-08-31 02:42 | SKIPPED | .oh/tasks/cc-safety-net/prd.json US-007 2026-07-19 | -| changelog-entry-length | A | 2026-08-31 02:42 | PASS | conversation 2026-08-24 — CHANGELOG.md grew to 259KB of bullet prose because "one line" was unquantified | -| cleanup-tasks-scoped-guard | A | 2026-08-31 02:42 | PASS | issue #85 | -| cleanup-tasks-worktree-grooming | A | 2026-08-31 02:42 | PASS | issue #168; issue #327 | -| cli-publish-typecheck-scope | A | 2026-08-31 02:42 | PASS | release run 33271077312 — v0.5.0 pushed its GHCR image, then failed to publish | -| close-issues-on-development | A | 2026-08-31 02:42 | PASS | issue #841 (closing keywords never fire because the default branch is main) 2026-08-26 | -| codex-stale-response-retry | A | 2026-08-31 02:42 | PASS | issue #506 — Codex previous_response_not_found RCA | -| compose-config-path-parity | A | 2026-08-31 02:42 | PASS | PR #833 (remove harness.yaml — the wrapper and VS Code "Reopen in Container" paths must resolve the same service) 2026-08-26 | -| config-schema-parity | A | 2026-08-31 02:42 | PASS | PR #833 (one schema file — DOCKER_SOCKET, SANDBOX_SSH, OH_SANDBOX_IMAGE, OH_PULL_POLICY, SKIP_PNPM_INSTALL were consumed but undocumented); rewritten for the oh.json/secrets split by PR #887 | -| context-tier-size-budget | A | 2026-08-31 02:42 | PASS | .oh/tasks/spec-simplification/ (issue #816, US-007) — the always-on tier was 85,256 B | -| cron-claude-codex-fallback | A | 2026-08-31 02:42 | PASS | conversation 2026-06-12 (default Codex fallback for crons) | -| cron-watchdog | A | 2026-08-31 02:42 | PASS | issues #130/#453 (cron runtime watchdog + legacy system-cron reaping) 2026-06-19 | -| crons-directory-guide | A | 2026-08-31 02:42 | PASS | issue #874 | -| curl-bash-safe-alternatives | A | 2026-08-31 02:42 | PASS | vet-run/vet integration — public curl|bash examples need review-first alternatives | -| datasets-schema | A | 2026-08-31 02:42 | PASS | issue #196 — .oh/evals/datasets verifiable trajectory corpus (Repo2RLEnv-inspired) | -| debugmcp-availability | A | 2026-08-31 02:42 | SKIPPED | issue #297 — DebugMCP MCP debug-server availability | -| default-provisioning | A | 2026-08-31 02:42 | PASS | #902 — `oh harness install` must work from inside the sandbox, where | -| delegate-model-effort-policy | A | 2026-08-31 02:42 | PASS | conversation 2026-07-11 (delegate model inheritance and thinking policy) | -| devtcp-hook | A | 2026-08-31 02:42 | PASS | retro lesson 2026-06-10 (zsh /dev/tcp) | -| docker-inspect-env-guard | A | 2026-08-31 02:42 | PASS | operator directive 2026-08-08 (agents keep the docker socket, but must | -| docs-build-fast-path | A | 2026-08-31 02:42 | PASS | #455 — docs builds must stay out of fast harness/eval/release gates; #536 — docs site externalized to openharness-web; docs markdown relocated to docs/ | -| drift-check-cron-staleness-glob | A | 2026-08-31 02:42 | PASS | issue #98; issue #225 (restart-required cron frontmatter/config drift) | -| entrypoint-pnpm-manifest-fingerprint | A | 2026-08-31 02:42 | PASS | issue #521 (manifest-aware sandbox installs) 2026-07-01 | -| eval-ci-gate | A | 2026-08-31 02:42 | PASS | #103 — eval probe suite gated in CI | -| eval-gate | A | 2026-08-31 02:42 | PASS | retro lesson 2026-06-11 (eval-gate) | -| eval-results-atomic | A | 2026-08-31 02:42 | PASS | issue #83 (eval-results-atomic-write) | -| eval-runner-exit | A | 2026-08-31 02:42 | PASS | retro lesson 2026-06-11 (eval-runner-exit) #29 | -| eval-runs-once-per-cycle | A | 2026-08-31 02:42 | PASS | .oh/tasks/spec-simplification/ (issue #816, US-006) — /eval ran 3x per cycle on the | -| execution-target-contract | A | 2026-08-31 02:42 | PASS | issue #733 (ExecutionTarget contract + Docker Compose adapter) 2026-08-10 | -| get-oh-bootstrap | A | 2026-08-31 02:42 | PASS | get-oh.sh bootstrap — the Node-bootstrapping host-side path to the standalone `oh` CLI (also on npm as @mifune/openharness; see oh-npm-package.sh) | -| git-skill | A | 2026-08-31 02:42 | PASS | conversation 2026-06-15 — rules are not always supported; git workflow must be the /git skill | -| harness-audit-empty-output-gate | A | 2026-08-31 02:42 | PASS | issue #246 — /audit harness must fail closed on empty auditor outputs | -| harness-ci-core-paths | A | 2026-08-31 02:42 | PASS | #165 — core sandbox config files must trigger harness CI | -| harness-ci-hooks-paths | A | 2026-08-31 02:42 | PASS | issue #202 — credential/security hook changes must trigger harness CI | -| harness-yaml-migration | A | 2026-08-31 02:42 | PASS | PR #833 (migrate-harness-yaml.sh — append / uncomment-in-place / preserve / overwrite, plus a silent no-op second run) 2026-08-26 | -| health-check-docker-stats | A | 2026-08-31 02:42 | PASS | retro lesson 2026-06-10 (docker stats vs ps Size) | -| health-check-socket-degrade | A | 2026-08-31 02:42 | PASS | issue #762 (refs #756) — /health-check degrades to one statement, not nine failures | -| heartbeat-logging-contract | A | 2026-08-31 02:42 | PASS | issue #447 (heartbeat log append hardening) 2026-06-18 | -| image-seed-hygiene | A | 2026-08-31 02:42 | PASS | issue #900 (slim the sandbox image) 2026-08-30 | -| markitdown-wiki-ingest | A | 2026-08-31 02:42 | PASS | issue #649 — pinned local-document normalization contract for /wiki ingest | -| next-dev-prod | A | 2026-08-31 02:42 | SKIPPED | retro lesson 2026-06-04 | -| oh-compose-env-wiring | A | 2026-08-31 02:42 | PASS | issue #880 (oh as the only front door — oh.json is the non-secret config surface) | -| oh-config-surfaces | A | 2026-08-31 02:42 | REGRESSION | PR #887 (config split across two authored surfaces — a tracked oh.json and a secrets-only root dotenv — with nothing left under $HOME) | -| oh-destroy-guard | A | 2026-08-31 02:42 | PASS | issue #879 — `oh` becomes the only front door, so `make destroy` must | -| oh-devcontainer-restructure | A | 2026-08-31 02:42 | PASS | consolidate devcontainer — .oh/devcontainer/ folded back into .devcontainer/ | -| oh-home-mount | A | 2026-08-31 02:42 | PASS | issue #898 (single $HOME mount) 2026-08-30 | -| oh-image-only-deploy | A | 2026-08-31 02:42 | PASS | .oh/tasks/image-only-deploy/prd.json US-004 (issue #609, Flavor B image-only deploy) | -| oh-init-headless-config | A | 2026-08-31 02:42 | PASS | PR #827 (installer answers landed in the losing config file); retargeted to the .example.env template by PR #833, then to oh.json by PR #887 | -| oh-init-scaffold | A | 2026-08-31 02:42 | PASS | issue #531 Phase 2 | -| oh-lifecycle-surface | A | 2026-08-31 02:42 | PASS | issue #881 — the Makefile is retired and `oh` is the only front door | -| oh-npm-package | A | 2026-08-31 02:42 | PASS | npm publish path for the standalone `oh` CLI (@mifune/openharness) — alternative to get-oh.sh | -| oh-payload-manifest | A | 2026-08-31 02:42 | PASS | issue #531 follow-on (.oh payload manifest — oh update ships a declared allowlist) | -| oh-sandbox-image-mode | A | 2026-08-31 02:42 | PASS | conversation 2026-07-05 (basic Docker deployment — prebuilt-image mode) | -| oh-shipped-repo-overridable | A | 2026-08-31 02:42 | PASS | issue #531 follow-on (de-hardcode residual — shipped .oh shell scripts keep the upstream repo overridable) | -| oh-standalone-lifecycle | A | 2026-08-31 02:42 | PASS | issue #564 | -| oh-update | A | 2026-08-31 02:42 | PASS | issue #531 Phase 3 (oh update — upgrade only the .oh control plane) | -| operator-config-guard | A | 2026-08-31 02:42 | PASS | operator directives 2026-08-06 (.config/ and settings.local.json are operator-only) | -| pnpm-audit-ci-gate | A | 2026-08-31 02:42 | PASS | issue #171 — pnpm security audits must run in CI | -| post-bridge-publish-confirmation | A | 2026-08-31 02:42 | PASS | #523 — post-bridge live publishing requires an explicit final confirmation gate | -| prd-output-path-contract | A | 2026-08-31 02:42 | PASS | retro lesson 2026-06-19 | -| prompt-miner-schema-compat | A | 2026-08-31 02:42 | PASS | issue #253 — prompt-miner JSONL schema-drift guard | -| prompt-miner-symlink-entrypoint | A | 2026-08-31 02:42 | PASS | issue #663 — prompt-miner engine no-ops via the documented .claude/skills symlink | -| prompt-miner-weakness-record | A | 2026-08-31 02:42 | PASS | issue #580 — prompt-miner weakness-record (WH-xxx) cluster output | -| protected-path-deletion | A | 2026-08-31 02:42 | PASS | .oh/tasks/spec-simplification/ (issue #816, US-001) — the critique gate was deleted, | -| protected-paths-resolve | A | 2026-08-31 02:42 | PASS | issue #753 — .claude/protected-paths.txt named 7 paths that did not exist. | -| registry-portability-gate | A | 2026-08-31 02:42 | PASS | issue #758 | -| registry-portability | A | 2026-08-31 02:42 | SKIPPED | issue #758 | -| retro-deterministic-contract | A | 2026-08-31 02:42 | PASS | issue #443 — /retro deterministic output and self-contained helper contract | -| rl-delegation-write-worker | A | 2026-08-31 02:42 | PASS | retro lesson 2026-06-10 (rl-delegation) #57 | -| rlm-context-budget | A | 2026-08-31 02:42 | PASS | .oh/tasks/rlm-weighted-trajectories/prd.json US-006 | -| runtime-preflight-gate | A | 2026-08-31 02:42 | PASS | issue #806 § B1 (open sandbox.substrate vs sandbox.runtime selector); | -| sandbox-boot-guard-ci | A | 2026-08-31 02:42 | PASS | issue #449 (sandbox image build CI guard) 2026-06-19; | -| sandbox-node-base | A | 2026-08-31 02:42 | PASS | openharness#878 — oh as the only front door, T0 sandbox base image | -| skill-paths | A | 2026-08-31 02:42 | PASS | issue #43 — stale path references; extended by issue #69 — apps/->packages/ rename guard; extended by issue #870 — deleted .oh/agents/advisor.md | -| skills-dir-clean | A | 2026-08-31 02:42 | PASS | conversation 2026-06-29 — Pi parses every top-level `.md` in the skills | -| skills-task-tool-coupling | A | 2026-08-31 02:42 | PASS | council review 2026-08-29 (issue #886) — /delegate instructed Claude-Code-only | -| skills-vendored | A | 2026-08-31 02:42 | PASS | absorb .mifune submodule into .oh — the skills/agents/hooks pack is vendored | -| slack-admin-command-surface | A | 2026-08-31 02:42 | PASS | issue #354 — Slack bridge docs must distinguish Pi /msg-bridge commands from Slack DM admin text handlers | -| spec-family-contract | A | 2026-08-31 02:42 | PASS | issue #265; spec-simplification issue #816; workflow authority issue #854 | -| spec-ready-finalization | A | 2026-08-31 02:42 | PASS | issue #134; spec-simplification issue #816; workflow authority issue #854 | -| ste-checker-contract | A | 2026-08-31 02:42 | PASS | issue #750 PR audit — the /ste checker had four fail-open paths (unclosed | -| submitted-by-trailers | A | 2026-08-31 02:42 | PASS | conversation 2026-06-12 (commit attribution trailers); the single-owner | -| sync-skill-contract | A | 2026-08-31 02:42 | PASS | issue #331 — /sync dispatcher skill (bidirectional origin↔upstream sync) | -| tool-catalog-boundary | A | 2026-08-31 02:42 | PASS | agent-browser's exclusion from the harness catalog (#821) and the | -| version-parity | A | 2026-08-31 02:42 | PASS | conversation 2026-08-29 — the oh CLI became the only lifecycle door, so its | -| weigh-scorer-contract | A | 2026-08-31 02:42 | PASS | .oh/tasks/rlm-weighted-trajectories/prd.json US-003 (2026-06-27) | -| wiki-readme-index | A | 2026-08-31 02:42 | PASS | issue #132 — wiki README index drift guard | -| workflow-boundaries | A | 2026-08-31 02:42 | PASS | conversation 2026-06-19 (workflow consolidation, issue #259); authority moved to /spec in issue #854 | -| worktrees-layout | A | 2026-08-31 02:42 | PASS | issue #872 | +| advisor-monitored-loop | A | 2026-08-31 17:17 | PASS | conversation 2026-06-19 (single-owner implementation workflow, issue #257) | +| agent-browser-cli | A | 2026-08-31 17:17 | PASS | retro lesson 2026-06-07 (agent-browser 0.8.5 CLI) | +| agents-identity-contract | A | 2026-08-31 17:17 | PASS | issue #854 — T3-style root identity, glossary, and skill-owned procedures | +| artifact-contract-audit | A | 2026-08-31 17:17 | PASS | issue #583/#645 — production /audit implementation Gate 1 behavior | +| audit-dispatcher-contract | A | 2026-08-31 17:17 | PASS | issue #645 — audit consolidation public taxonomy | +| audit-implementation-behavior | A | 2026-08-31 17:17 | PASS | issue #645 — implementation root/repo/browser behavior | +| audit-pr-acquire | A | 2026-08-31 17:17 | PASS | issue #645 — production PR acquisition behavior | +| audit-pr-classifier | A | 2026-08-31 17:17 | PASS | issue #645 — deterministic focused and queue PR classifier | +| audit-run-root-contract | A | 2026-08-31 17:17 | PASS | issue #645 — executable immutable audit root/run correlation | +| audit-shellcheck-coverage | A | 2026-08-31 17:17 | PASS | issue #645 — private audit scripts require release and CI lint coverage | +| audit-stale-references | A | 2026-08-31 17:17 | PASS | issue #645 — clean-breaking audit migration | +| boot-lint-glob | A | 2026-08-31 17:17 | PASS | issue #90, issue #120 | +| builder-skill-consolidation | A | 2026-08-31 17:17 | PASS | issue #643 — consolidate artifact builders behind one /builder dispatcher | +| capability-benchmark-schema | A | 2026-08-31 17:17 | PASS | issue #167 — capability benchmark instrument | +| cc-safety-net-wiring | A | 2026-08-31 17:17 | SKIPPED | .oh/tasks/cc-safety-net/prd.json US-007 2026-07-19 | +| changelog-entry-length | A | 2026-08-31 17:17 | PASS | conversation 2026-08-24 — CHANGELOG.md grew to 259KB of bullet prose because "one line" was unquantified | +| cleanup-tasks-scoped-guard | A | 2026-08-31 17:17 | PASS | issue #85 | +| cleanup-tasks-worktree-grooming | A | 2026-08-31 17:17 | PASS | issue #168; issue #327 | +| cli-publish-typecheck-scope | A | 2026-08-31 17:17 | PASS | release run 33271077312 — v0.5.0 pushed its GHCR image, then failed to publish | +| close-issues-on-development | A | 2026-08-31 17:17 | PASS | issue #841 (closing keywords never fire because the default branch is main) 2026-08-26 | +| codex-stale-response-retry | A | 2026-08-31 17:17 | PASS | issue #506 — Codex previous_response_not_found RCA | +| compose-config-path-parity | A | 2026-08-31 17:17 | PASS | PR #833 (remove harness.yaml — the wrapper and VS Code "Reopen in Container" paths must resolve the same service) 2026-08-26 | +| config-schema-parity | A | 2026-08-31 17:17 | PASS | PR #833 (one schema file — DOCKER_SOCKET, SANDBOX_SSH, OH_SANDBOX_IMAGE, OH_PULL_POLICY, SKIP_PNPM_INSTALL were consumed but undocumented); rewritten for the oh.json/secrets split by PR #887 | +| context-tier-size-budget | A | 2026-08-31 17:17 | PASS | .oh/tasks/spec-simplification/ (issue #816, US-007) — the always-on tier was 85,256 B | +| cron-claude-codex-fallback | A | 2026-08-31 17:17 | PASS | conversation 2026-06-12 (default Codex fallback for crons) | +| cron-watchdog | A | 2026-08-31 17:17 | PASS | issues #130/#453 (cron runtime watchdog + legacy system-cron reaping) 2026-06-19 | +| crons-directory-guide | A | 2026-08-31 17:17 | PASS | issue #874 | +| curl-bash-safe-alternatives | A | 2026-08-31 17:17 | PASS | vet-run/vet integration — public curl|bash examples need review-first alternatives | +| datasets-schema | A | 2026-08-31 17:17 | PASS | issue #196 — .oh/evals/datasets verifiable trajectory corpus (Repo2RLEnv-inspired) | +| debugmcp-availability | A | 2026-08-31 17:17 | SKIPPED | issue #297 — DebugMCP MCP debug-server availability | +| default-provisioning | A | 2026-08-31 17:17 | PASS | #902 — `oh harness install` must work from inside the sandbox, where | +| delegate-model-effort-policy | A | 2026-08-31 17:17 | PASS | conversation 2026-07-11 (delegate model inheritance and thinking policy) | +| devtcp-hook | A | 2026-08-31 17:17 | PASS | retro lesson 2026-06-10 (zsh /dev/tcp) | +| docker-inspect-env-guard | A | 2026-08-31 17:17 | PASS | operator directive 2026-08-08 (agents keep the docker socket, but must | +| docs-build-fast-path | A | 2026-08-31 17:17 | PASS | #455 — docs builds must stay out of fast harness/eval/release gates; #536 — docs site externalized to openharness-web; docs markdown relocated to docs/ | +| drift-check-cron-staleness-glob | A | 2026-08-31 17:17 | PASS | issue #98; issue #225 (restart-required cron frontmatter/config drift) | +| entrypoint-pnpm-manifest-fingerprint | A | 2026-08-31 17:17 | PASS | issue #521 (manifest-aware sandbox installs) 2026-07-01 | +| eval-ci-gate | A | 2026-08-31 17:17 | PASS | #103 — eval probe suite gated in CI | +| eval-gate | A | 2026-08-31 17:17 | PASS | retro lesson 2026-06-11 (eval-gate) | +| eval-results-atomic | A | 2026-08-31 17:17 | PASS | issue #83 (eval-results-atomic-write) | +| eval-runner-exit | A | 2026-08-31 17:17 | PASS | retro lesson 2026-06-11 (eval-runner-exit) #29 | +| eval-runs-once-per-cycle | A | 2026-08-31 17:17 | PASS | .oh/tasks/spec-simplification/ (issue #816, US-006) — /eval ran 3x per cycle on the | +| execution-target-contract | A | 2026-08-31 17:17 | PASS | issue #733 (ExecutionTarget contract + Docker Compose adapter) 2026-08-10 | +| get-oh-bootstrap | A | 2026-08-31 17:17 | PASS | get-oh.sh bootstrap — the Node-bootstrapping host-side path to the standalone `oh` CLI (also on npm as @mifune/openharness; see oh-npm-package.sh) | +| git-skill | A | 2026-08-31 17:17 | PASS | conversation 2026-06-15 — rules are not always supported; git workflow must be the /git skill | +| harness-audit-empty-output-gate | A | 2026-08-31 17:17 | PASS | issue #246 — /audit harness must fail closed on empty auditor outputs | +| harness-ci-core-paths | A | 2026-08-31 17:17 | PASS | #165 — core sandbox config files must trigger harness CI | +| harness-ci-hooks-paths | A | 2026-08-31 17:17 | PASS | issue #202 — credential/security hook changes must trigger harness CI | +| harness-yaml-migration | A | 2026-08-31 17:17 | PASS | PR #833 (migrate-harness-yaml.sh — append / uncomment-in-place / preserve / overwrite, plus a silent no-op second run) 2026-08-26 | +| health-check-docker-stats | A | 2026-08-31 17:17 | PASS | retro lesson 2026-06-10 (docker stats vs ps Size) | +| health-check-socket-degrade | A | 2026-08-31 17:17 | PASS | issue #762 (refs #756) — /health-check degrades to one statement, not nine failures | +| heartbeat-logging-contract | A | 2026-08-31 17:17 | PASS | issue #447 (heartbeat log append hardening) 2026-06-18 | +| image-seed-hygiene | A | 2026-08-31 17:17 | PASS | issue #900 (slim the sandbox image) 2026-08-30 | +| markitdown-wiki-ingest | A | 2026-08-31 17:17 | PASS | issue #649 — pinned local-document normalization contract for /wiki ingest | +| next-dev-prod | A | 2026-08-31 17:17 | SKIPPED | retro lesson 2026-06-04 | +| oh-compose-env-wiring | A | 2026-08-31 17:17 | PASS | issue #880 (oh as the only front door — oh.json is the non-secret config surface) | +| oh-config-surfaces | A | 2026-08-31 17:17 | REGRESSION | PR #887 (config split across two authored surfaces — a tracked oh.json and a secrets-only root dotenv — with nothing left under $HOME) | +| oh-destroy-guard | A | 2026-08-31 17:17 | PASS | issue #879 — `oh` becomes the only front door, so `make destroy` must | +| oh-devcontainer-restructure | A | 2026-08-31 17:17 | PASS | consolidate devcontainer — .oh/devcontainer/ folded back into .devcontainer/ | +| oh-home-mount | A | 2026-08-31 17:17 | PASS | issue #898 (single $HOME mount) 2026-08-30 | +| oh-image-only-deploy | A | 2026-08-31 17:17 | PASS | .oh/tasks/image-only-deploy/prd.json US-004 (issue #609, Flavor B image-only deploy) | +| oh-init-headless-config | A | 2026-08-31 17:17 | PASS | PR #827 (installer answers landed in the losing config file); retargeted to the .example.env template by PR #833, then to oh.json by PR #887 | +| oh-init-scaffold | A | 2026-08-31 17:17 | PASS | issue #531 Phase 2 | +| oh-lifecycle-surface | A | 2026-08-31 17:17 | PASS | issue #881 — the Makefile is retired and `oh` is the only front door | +| oh-npm-package | A | 2026-08-31 17:17 | PASS | npm publish path for the standalone `oh` CLI (@mifune/openharness) — alternative to get-oh.sh | +| oh-payload-manifest | A | 2026-08-31 17:17 | PASS | issue #531 follow-on (.oh payload manifest — oh update ships a declared allowlist) | +| oh-sandbox-image-mode | A | 2026-08-31 17:17 | PASS | conversation 2026-07-05 (basic Docker deployment — prebuilt-image mode) | +| oh-shipped-repo-overridable | A | 2026-08-31 17:17 | PASS | issue #531 follow-on (de-hardcode residual — shipped .oh shell scripts keep the upstream repo overridable) | +| oh-standalone-lifecycle | A | 2026-08-31 17:17 | PASS | issue #564 | +| oh-update | A | 2026-08-31 17:17 | PASS | issue #531 Phase 3 (oh update — upgrade only the .oh control plane) | +| operator-config-guard | A | 2026-08-31 17:17 | PASS | operator directives 2026-08-06 (.config/ and settings.local.json are operator-only) | +| pnpm-audit-ci-gate | A | 2026-08-31 17:17 | PASS | issue #171 — pnpm security audits must run in CI | +| post-bridge-publish-confirmation | A | 2026-08-31 17:17 | PASS | #523 — post-bridge live publishing requires an explicit final confirmation gate | +| prd-output-path-contract | A | 2026-08-31 17:17 | PASS | retro lesson 2026-06-19 | +| prompt-miner-schema-compat | A | 2026-08-31 17:17 | PASS | issue #253 — prompt-miner JSONL schema-drift guard | +| prompt-miner-symlink-entrypoint | A | 2026-08-31 17:17 | PASS | issue #663 — prompt-miner engine no-ops via the documented .claude/skills symlink | +| prompt-miner-weakness-record | A | 2026-08-31 17:17 | PASS | issue #580 — prompt-miner weakness-record (WH-xxx) cluster output | +| protected-path-deletion | A | 2026-08-31 17:17 | PASS | .oh/tasks/spec-simplification/ (issue #816, US-001) — the critique gate was deleted, | +| protected-paths-resolve | A | 2026-08-31 17:17 | PASS | issue #753 — .claude/protected-paths.txt named 7 paths that did not exist. | +| registry-portability-gate | A | 2026-08-31 17:17 | PASS | issue #758 | +| registry-portability | A | 2026-08-31 17:17 | SKIPPED | issue #758 | +| retro-deterministic-contract | A | 2026-08-31 17:17 | PASS | issue #443 — /retro deterministic output and self-contained helper contract | +| rl-delegation-write-worker | A | 2026-08-31 17:17 | PASS | retro lesson 2026-06-10 (rl-delegation) #57 | +| rlm-context-budget | A | 2026-08-31 17:17 | PASS | .oh/tasks/rlm-weighted-trajectories/prd.json US-006 | +| runtime-preflight-gate | A | 2026-08-31 17:17 | PASS | issue #806 § B1 (open sandbox.substrate vs sandbox.runtime selector); | +| sandbox-boot-guard-ci | A | 2026-08-31 17:17 | PASS | issue #449 (sandbox image build CI guard) 2026-06-19; | +| sandbox-node-base | A | 2026-08-31 17:17 | PASS | openharness#878 — oh as the only front door, T0 sandbox base image | +| skill-paths | A | 2026-08-31 17:17 | PASS | issue #43 — stale path references; extended by issue #69 — apps/->packages/ rename guard; extended by issue #870 — deleted .oh/agents/advisor.md | +| skills-dir-clean | A | 2026-08-31 17:17 | PASS | conversation 2026-06-29 — Pi parses every top-level `.md` in the skills | +| skills-task-tool-coupling | A | 2026-08-31 17:17 | PASS | council review 2026-08-29 (issue #886) — /delegate instructed Claude-Code-only | +| skills-vendored | A | 2026-08-31 17:17 | PASS | absorb .mifune submodule into .oh — the skills/agents/hooks pack is vendored | +| slack-admin-command-surface | A | 2026-08-31 17:17 | PASS | issue #354 — Slack bridge docs must distinguish Pi /msg-bridge commands from Slack DM admin text handlers | +| spec-family-contract | A | 2026-08-31 17:17 | PASS | issue #265; spec-simplification issue #816; workflow authority issue #854 | +| spec-ready-finalization | A | 2026-08-31 17:17 | PASS | issue #134; spec-simplification issue #816; workflow authority issue #854 | +| ste-checker-contract | A | 2026-08-31 17:17 | PASS | issue #750 PR audit — the /ste checker had four fail-open paths (unclosed | +| submitted-by-trailers | A | 2026-08-31 17:17 | PASS | conversation 2026-06-12 (commit attribution trailers); the single-owner | +| sync-skill-contract | A | 2026-08-31 17:17 | PASS | issue #331 — /sync dispatcher skill (bidirectional origin↔upstream sync) | +| tool-catalog-boundary | A | 2026-08-31 17:17 | PASS | agent-browser's exclusion from the harness catalog (#821) and the | +| version-parity | A | 2026-08-31 17:17 | PASS | conversation 2026-08-29 — the oh CLI became the only lifecycle door, so its | +| weigh-scorer-contract | A | 2026-08-31 17:17 | PASS | .oh/tasks/rlm-weighted-trajectories/prd.json US-003 (2026-06-27) | +| wiki-readme-index | A | 2026-08-31 17:17 | PASS | issue #132 — wiki README index drift guard | +| workflow-boundaries | A | 2026-08-31 17:17 | PASS | conversation 2026-06-19 (workflow consolidation, issue #259); authority moved to /spec in issue #854 | +| worktrees-layout | A | 2026-08-31 17:17 | PASS | issue #872 | diff --git a/.oh/evals/probes/default-provisioning.sh b/.oh/evals/probes/default-provisioning.sh index 7627c5d0..c8b4dc3b 100755 --- a/.oh/evals/probes/default-provisioning.sh +++ b/.oh/evals/probes/default-provisioning.sh @@ -40,16 +40,28 @@ entries=$(awk ' inb { buf = buf $0 " " } ' "$CATALOG") +# #908: every harness, not just kind:"default". commands/harness.ts installs +# with stdio:"inherit", so local-target.ts picks plain `sudo --` for a root +# install and /etc/sudoers.d/sandbox has no NOPASSWD — an agent would hang on a +# password prompt. No harness of any kind belongs in the image either. defaults=0 while IFS= read -r entry; do - [[ $entry == *'kind: "default"'* ]] || continue - defaults=$((defaults + 1)) id=$(sed -n 's/.*id: "\([^"]*\)".*/\1/p' <<<"$entry") + [[ -n $id ]] || continue + defaults=$((defaults + 1)) if [[ $entry == *'installUser: "root"'* ]]; then - missing+=("harnesses/catalog.ts: default harness \"$id\" installs as root — inside the sandbox that becomes \`sudo -n\`, and /etc/sudoers.d/sandbox has no NOPASSWD") + missing+=("harnesses/catalog.ts: harness \"$id\" installs as root — commands/harness.ts uses stdio:\"inherit\", so that becomes an interactive \`sudo\`, and /etc/sudoers.d/sandbox has no NOPASSWD") fi - if [[ $entry != *"$PREFIX"* ]]; then - missing+=("harnesses/catalog.ts: default harness \"$id\" does not install into $PREFIX — a baked install under /usr/lib/node_modules cannot be upgraded by a running sandbox") + if [[ $entry == *'buildArg:'* ]]; then + missing+=("harnesses/catalog.ts: harness \"$id\" declares buildArg — that field carries a Dockerfile invariant this catalog cannot satisfy; the tool catalog already bans it") + fi + # on-demand entries (t3code) are fetched per invocation via npx and install + # nowhere, so the prefix rule does not apply to them. + if [[ $entry != *'kind: "on-demand"'* \ + && $entry != *"$PREFIX"* \ + && $entry != *'$HOME/.local'* \ + && $entry != *'uv", "tool", "install'* ]]; then + missing+=("harnesses/catalog.ts: harness \"$id\" does not install into $PREFIX — a system-path install cannot be upgraded by a running sandbox and does not persist in the home mount") fi if [[ $id == "claude-code" && $entry == *"--ignore-scripts"* ]]; then missing+=("harnesses/catalog.ts: claude-code uses --ignore-scripts — its postinstall copies the native binary over the placeholder, so \`claude --version\` fails with 'claude native binary not installed'") @@ -57,7 +69,7 @@ while IFS= read -r entry; do done <<<"$entries" if ((defaults == 0)); then - echo "SKIPPED: no kind:\"default\" harness parsed out of $CATALOG" >&2 + echo "SKIPPED: no harness parsed out of $CATALOG" >&2 exit 2 fi @@ -82,7 +94,9 @@ DOCKERFILE_CODE=$(strip_dockerfile_comments) pkgs=0 while IFS= read -r entry; do - [[ $entry == *'kind: "default"'* ]] || continue + # on-demand entries (t3code, prime-agent) are fetched per invocation and were + # never baked; skip them rather than assert against an npx incantation. + [[ $entry == *'kind: "default"'* || $entry == *'kind: "optional"'* ]] || continue id=$(sed -n 's/.*id: "\([^"]*\)".*/\1/p' <<<"$entry") # The package specifier is the last element of installArgv. Read it from that # array alone — `binary` and `verifyArgv` also hold bare names, and matching @@ -96,7 +110,7 @@ while IFS= read -r entry; do fi pkgs=$((pkgs + 1)) if grep -qF -- "$pkg" <<<"$DOCKERFILE_CODE"; then - missing+=("Dockerfile: names $pkg — default harness \"$id\" is baked into the image again; it belongs to .oh/scripts/provision-defaults.sh, which installs it into $PREFIX at boot") + missing+=("Dockerfile: names $pkg — harness \"$id\" is baked into the image again; it belongs to \`oh harness install\`, which installs it into $PREFIX") fi done <<<"$entries" @@ -109,6 +123,14 @@ if grep -qE '^ARG (BAKE_HARNESSES|AGENTS|HERDR_VERSION)=' <<<"$DOCKERFILE_CODE"; missing+=("Dockerfile: ARG BAKE_HARNESSES/AGENTS/HERDR_VERSION is back — a build arg that re-bakes a default is a dormant path that reintroduces the shadowed install and un-exercises the boot provisioner") fi +# #908: no harness may have a Dockerfile build arg at all. INSTALL_HERMES keeps +# its RUNTIME life (link-providers.sh vendors the Hermes skill pack from it, +# entrypoint.sh wires auth.json), so only an `ARG` declaration is a regression. +while IFS= read -r arg; do + [[ -n $arg ]] || continue + missing+=("Dockerfile: $arg is back — optional harnesses install through \`oh harness install\`, and a build arg makes the image the install path again") +done < <(grep -oE '^ARG INSTALL_(HERMES|OPENCODE|GROK_BUILD|DEEPAGENTS)' <<<"$DOCKERFILE_CODE" | sort -u) + # #906: the same rule for kind:"default" tools. These install as root nowhere: # commands/tool.ts passes stdio:"inherit", so local-target.ts selects plain # `sudo --` for a root install, and /etc/sudoers.d/sandbox has no NOPASSWD — @@ -153,4 +175,4 @@ if ((${#missing[@]})); then exit 1 fi -echo "PASS: $defaults default harnesses and $tools default tools install as the sandbox user into $PREFIX, none of the $pkgs packages is baked into the image, and the boot path provisions them" >&2 +echo "PASS: all $defaults harnesses and $tools default tools install as the sandbox user into $PREFIX, none of the $pkgs packages is baked into the image, and the boot path provisions them" >&2 diff --git a/.oh/evals/probes/sandbox-boot-guard-ci.sh b/.oh/evals/probes/sandbox-boot-guard-ci.sh index 251d8001..9d20f969 100755 --- a/.oh/evals/probes/sandbox-boot-guard-ci.sh +++ b/.oh/evals/probes/sandbox-boot-guard-ci.sh @@ -124,19 +124,29 @@ else if grep -Eq 'arm64-default-image|linux/arm64|docker/setup-qemu-action|CI_RUNNER_ARM64' <<<"$compat"; then missing+=("compatibility workflow: retains the removed permanent arm64 build") fi + # #908 deleted the INSTALL_* build args, so a build-arg matrix can no longer + # exercise the optional harnesses. The job must install them the way an + # operator does instead, and must not reintroduce the args. for arg in INSTALL_HERMES INSTALL_DEEPAGENTS INSTALL_OPENCODE INSTALL_GROK_BUILD; do - chas "--build-arg $arg=true" "does not build with $arg=true" + if grep -Fq -- "--build-arg $arg" <<<"$compat"; then + missing+=("compatibility workflow: still builds with $arg — that build arg no longer exists; install through \`oh harness install\`") + fi done optional=$(awk ' - /^ optional-installers-image:$/ { found=1 } - found && /^ [[:alnum:]_-]+:$/ && !/^ optional-installers-image:$/ { exit } + /^ optional-harness-install:$/ { found=1 } + found && /^ [[:alnum:]_-]+:$/ && !/^ optional-harness-install:$/ { exit } found { print } ' <<<"$compat") if [[ -z "$optional" ]]; then - missing+=("compatibility workflow: no optional installer job") + missing+=("compatibility workflow: no optional-harness-install job") else - ohas() { grep -Fq -- "$1" <<<"$optional" || missing+=("compatibility optional installer job: $2"); } - ohas 'for tool in hermes deepagents opencode grok; do' "does not check every optional tool in one guarded loop" + ohas() { grep -Fq -- "$1" <<<"$optional" || missing+=("compatibility optional harness job: $2"); } + ohas 'oh harness install' "does not install through the CLI — the path #908 made the only one" + ohas 'select(.kind == "optional") | .id' "does not read the optional set from the catalog, so it can drift" + ohas 'would pass vacuously' "does not fail closed when the catalog yields no optional harness" + ohas '/home/sandbox/.local/*)' "does not assert the install landed in the home mount" + ohas 'for attempt in 1 2; do' "does not retry a transient upstream failure — four third-party endpoints can each block a merge" + ohas 'this is not a transient upstream blip' "retries without ever failing hard, so a real break would pass" ohas "if ! grep -Eq '(^|[^[:alnum:]])v?[0-9]+([.][0-9]+)+" "does not require numeric dotted versions" ohas 'did not output a numeric dotted version' "does not fail false-positive output" fi diff --git a/.oh/install/banner.sh b/.oh/install/banner.sh index 9115870f..544f267f 100755 --- a/.oh/install/banner.sh +++ b/.oh/install/banner.sh @@ -81,7 +81,7 @@ if [ -s "${HOME}/.pi/agent/auth.json" ]; then fi opencode_status="$status_x" -opencode_detail="not installed — set INSTALL_OPENCODE=true and rebuild" +opencode_detail="not installed — run: oh harness install opencode" if command -v opencode >/dev/null 2>&1; then if [ -s "${HOME}/.local/share/opencode/auth.json" ]; then opencode_status="$status_ok" @@ -93,7 +93,7 @@ if command -v opencode >/dev/null 2>&1; then fi grok_status="$status_x" -grok_detail="not installed — enable via install.grok_build / INSTALL_GROK_BUILD" +grok_detail="not installed — run: oh harness install grok-build" if command -v grok >/dev/null 2>&1; then if [ -s "${HOME}/.grok/auth.json" ]; then grok_status="$status_ok" @@ -108,7 +108,7 @@ if command -v grok >/dev/null 2>&1; then fi deepagents_status="$status_x" -deepagents_detail="not installed — set INSTALL_DEEPAGENTS=true and rebuild" +deepagents_detail="not installed — run: oh harness install deepagents" if command -v deepagents >/dev/null 2>&1; then if [ -s "${HOME}/.deepagents/.env" ] || [ -s "${HOME}/.deepagents/config.toml" ]; then deepagents_status="$status_ok" @@ -120,7 +120,7 @@ if command -v deepagents >/dev/null 2>&1; then fi hermes_status="$status_x" -hermes_detail="not installed — set INSTALL_HERMES=true and rebuild" +hermes_detail="not installed — run: oh harness install hermes" if command -v hermes >/dev/null 2>&1; then if [ -s "${HERMES_HOME:-${OH_PROJECT_ROOT:-/home/sandbox/harness}/.hermes}/auth.json" ]; then hermes_status="$status_ok" diff --git a/.oh/scripts/__tests__/verify-sandbox-image.test.ts b/.oh/scripts/__tests__/verify-sandbox-image.test.ts index 17dafbc2..5ec4ccaa 100644 --- a/.oh/scripts/__tests__/verify-sandbox-image.test.ts +++ b/.oh/scripts/__tests__/verify-sandbox-image.test.ts @@ -20,8 +20,10 @@ type Overrides = Partial<{ platformWarning: string; bakedHarnesses: boolean; bakedTools: boolean; - noDefaultHarnesses: boolean; + noHarnesses: boolean; noDefaultTools: boolean; + noBakedInTools: boolean; + missingBakedInTool: boolean; harnessCatalogFails: boolean; }>; @@ -43,8 +45,10 @@ function fixture(o: Overrides = {}) { platformWarning: "", bakedHarnesses: false, bakedTools: false, - noDefaultHarnesses: false, + noHarnesses: false, noDefaultTools: false, + noBakedInTools: false, + missingBakedInTool: false, harnessCatalogFails: false, ...o, }; @@ -61,32 +65,35 @@ case "$cmd" in *"id -u sandbox"*) printf '%s\\n%s\\n' ${JSON.stringify(v.uid)} ${JSON.stringify(v.gid)} ;; "node --version") printf '%s\\n' ${JSON.stringify(v.node)} ;; "pnpm --version") printf '%s\\n' ${JSON.stringify(v.pnpm)} ;; - *"oh harness list --defaults --json"*) + *"oh harness list --json"*) if [ "${v.harnessCatalogFails ? "1" : "0"}" = "1" ]; then echo 'not an OpenHarness-equipped repo' >&2 exit 1 fi cat <<'JSON' ${ - v.noDefaultHarnesses + v.noHarnesses ? "[]" : `[ { "id": "claude-code", "binary": "claude", "kind": "default", "installed": ${v.bakedHarnesses} }, - { "id": "pi", "binary": "pi", "kind": "default", "installed": false } + { "id": "hermes", "binary": "hermes", "kind": "optional", "installed": false } ]` } JSON ;; - *"oh tool list --defaults --json"*) + *"oh tool list --json"*) cat <<'JSON' -${ - v.noDefaultTools - ? "[]" - : `[ - { "id": "herdr", "binary": "herdr", "kind": "default", "installed": ${v.bakedTools} }, - { "id": "cloudflared", "binary": "cloudflared", "kind": "default", "installed": false } -]` -} +${(() => { + const rows: string[] = []; + if (!v.noDefaultTools) { + rows.push(` { "id": "herdr", "binary": "herdr", "kind": "default", "installed": ${v.bakedTools} }`); + rows.push(' { "id": "cloudflared", "binary": "cloudflared", "kind": "default", "installed": false }'); + } + if (!v.noBakedInTools) { + rows.push(` { "id": "gh", "binary": "gh", "kind": "baked-in", "installed": ${!v.missingBakedInTool} }`); + } + return `[\n${rows.join(",\n")}\n]`; +})()} JSON ;; *) @@ -130,7 +137,7 @@ describe("verify-sandbox-image", () => { expect(result.stdout).toContain("built-in sandbox user is 1000:1000"); expect(result.stdout).toContain("node is major 22"); expect(result.stdout).toContain("pnpm is exactly 10.33.0"); - expect(result.stdout).toContain("no default harness is baked into the image"); + expect(result.stdout).toContain("no harness is baked into the image"); expect(result.stdout).toContain("no default tool is baked into the image"); expect(result.stdout).toContain("all checks passed"); }); @@ -192,23 +199,23 @@ describe("verify-sandbox-image", () => { expect(result.status).toBe(0); }); - // #904/#906: the default harnesses AND the default tools moved out of the - // image and into the boot path. A baked copy in a system path shadows the - // home-mount install and silently un-exercises the provisioner, so the image - // must not carry either. - it("passes an image that bakes no default harness or tool", () => { + // #904/#906/#908: no harness of any kind, and no kind:"default" tool, may be + // baked into the image. kind:"baked-in" tools are the image-level half and + // must be present, or the checks above pass on an image missing everything. + it("passes an image that bakes no harness and no default tool", () => { const result = run(fixture()); expect(result.status).toBe(0); - expect(result.stdout).toContain("no default harness is baked into the image"); + expect(result.stdout).toContain("no harness is baked into the image"); expect(result.stdout).toContain("no default tool is baked into the image"); + expect(result.stdout).toContain("every baked-in tool is present"); }); it("rejects an image that bakes a default harness", () => { const result = run(fixture({ bakedHarnesses: true })); expect(result.status).toBe(1); - expect(result.stderr).toContain("the image ships baked default harnesss: claude-code (claude)"); + expect(result.stderr).toContain("the image ships baked harnesss: claude-code (claude)"); }); it("rejects an image that bakes a default tool", () => { @@ -218,16 +225,30 @@ describe("verify-sandbox-image", () => { expect(result.stderr).toContain("the image ships baked default tools: herdr (herdr)"); }); + it("rejects an image whose baked-in tool is missing", () => { + const result = run(fixture({ missingBakedInTool: true })); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("baked-in tools are missing from the image: gh"); + }); + it.each<[string, Overrides]>([ - ["harness", { noDefaultHarnesses: true }], - ["tool", { noDefaultTools: true }], - ])("refuses to pass vacuously when the image lists no default %s", (_noun, overrides) => { + ["harness", { noHarnesses: true }], + ["default tool", { noDefaultTools: true }], + ])("refuses to pass vacuously when the image lists no %s", (_noun, overrides) => { const result = run(fixture(overrides)); expect(result.status).toBe(1); expect(result.stderr).toContain("would pass vacuously"); }); + it("refuses to pass vacuously when the image declares no baked-in tool", () => { + const result = run(fixture({ noBakedInTools: true })); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('declares no kind:"baked-in" tool'); + }); + it("fails loudly when the harness catalog cannot be read out of the image", () => { const result = run(fixture({ harnessCatalogFails: true })); diff --git a/.oh/scripts/provision-defaults.sh b/.oh/scripts/provision-defaults.sh index bb029361..181f9359 100755 --- a/.oh/scripts/provision-defaults.sh +++ b/.oh/scripts/provision-defaults.sh @@ -113,11 +113,14 @@ provisioned=0 provision_catalog() { local noun="$1" cmd="$2" catalog="$3" - local states defaults id installed - - if ! states="$("$OH_BIN" "$cmd" list --defaults --json 2>/dev/null)" || [ -z "$states" ]; then - die "'$OH_BIN $cmd list --defaults --json' produced no catalog" \ - "the CLI at $(command -v "$OH_BIN") predates \`oh $cmd --defaults\`; the catalog" \ + local states defaults wanted id installed + + # The full listing, not --defaults: an operator who set install. in + # oh.json declared intent that a fresh home mount must honour too, and + # `enabled` is computed from oh.json rather than the environment. + if ! states="$("$OH_BIN" "$cmd" list --json 2>/dev/null)" || [ -z "$states" ]; then + die "'$OH_BIN $cmd list --json' produced no catalog" \ + "the CLI at $(command -v "$OH_BIN") predates \`oh $cmd\`; the catalog" \ "is the only source of truth for what to install, so there is nothing to provision." \ "rebuild the sandbox image from this control plane:" \ " oh sandbox" @@ -128,6 +131,10 @@ provision_catalog() { "the $noun catalog declares no defaults" \ "check $catalog" + # Opted-in extras ride along; absent any, this is empty and nothing changes. + wanted="$(jq -r '.[] | select(.kind != "default" and .enabled == true) | "\(.id)\t\(.installed)"' <<<"$states")" + [ -n "$wanted" ] && defaults="$defaults"$'\n'"$wanted" + while IFS=$'\t' read -r id installed; do [ -n "$id" ] || continue provisioned=$((provisioned + 1)) diff --git a/.oh/scripts/verify-sandbox-image.sh b/.oh/scripts/verify-sandbox-image.sh index c3ff9996..cdc85b91 100755 --- a/.oh/scripts/verify-sandbox-image.sh +++ b/.oh/scripts/verify-sandbox-image.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # Verify a built sandbox image: base distribution, apt suites, the sandbox # UID/GID contract, the Node/pnpm pins, and version output from every baked-in -# tool, and that no kind:"default" harness or tool is baked into it. +# tool; that no harness and no kind:"default" tool is baked into it; and that +# every kind:"baked-in" tool actually is. # Usage: verify-sandbox-image.sh set -euo pipefail @@ -99,41 +100,61 @@ for tool in "gh --version" "docker --version" "docker compose version" \ fi done -# The image must NOT ship any kind:"default" harness (#904) or tool (#906). -# Both are installed into /home/sandbox/.local at boot: a copy baked into a +# The image must ship NO harness at all (#904, #908) and no kind:"default" tool +# (#906). Every one of them is installed into /home/sandbox/.local at boot or on +# demand through `oh harness install` / `oh tool install`: a copy baked into a # system path shadows the home-mount install with one no running sandbox can -# upgrade, and makes the boot install dead code that never runs and never gets -# tested. The catalogs inside the image are the source of truth for which ids -# are default, so this cannot drift from the TypeScript. -check_no_baked_defaults() { - local noun="$1" cmd="$2" json ids baked - - if ! json=$(run "cd /opt/oh-seed && OH_EXECUTION_TARGET=local oh $cmd list --defaults --json" 2>/tmp/verify-sandbox-defaults.err); then +# upgrade, and makes the install path dead code that never runs and never gets +# tested. The catalogs inside the image are the source of truth, so this cannot +# drift from the TypeScript. Tools that are kind:"baked-in" — the Docker CLI, +# gh — are genuinely image-level and are exempt. +check_nothing_baked() { + local noun="$1" cmd="$2" filter="$3" json ids baked + + if ! json=$(run "cd /opt/oh-seed && OH_EXECUTION_TARGET=local oh $cmd list --json" 2>/tmp/verify-sandbox-defaults.err); then fail "could not read the $noun catalog from the image: $(head -3 /tmp/verify-sandbox-defaults.err 2>/dev/null)" return fi - ids=$(jq -r '.[] | select(.kind == "default") | .id' <<<"$json") + ids=$(jq -r "$filter | .id" <<<"$json") if [ -z "$ids" ]; then - fail "the image's $noun catalog reports no kind:\"default\" entries — the unbaked-image check would pass vacuously" + fail "the image's $noun catalog matched no entry for '$filter' — the unbaked-image check would pass vacuously" return fi - baked=$(jq -r '.[] | select(.kind == "default" and .installed == true) | "\(.id) (\(.binary))"' <<<"$json") + baked=$(jq -r "$filter | select(.installed == true) | \"\(.id) (\(.binary))\"" <<<"$json") if [ -n "$baked" ]; then - fail "the image ships baked default ${noun}s: $(tr '\n' ' ' <<<"$baked")— these must be provisioned into /home/sandbox/.local at boot, not baked" + fail "the image ships baked ${noun}s: $(tr '\n' ' ' <<<"$baked")— these must be installed into /home/sandbox/.local by the CLI, not baked" else - ok "no default $noun is baked into the image ($(tr '\n' ' ' <<<"$ids"))" + ok "no $noun is baked into the image ($(tr '\n' ' ' <<<"$ids"))" fi } if command -v jq >/dev/null 2>&1; then - check_no_baked_defaults harness harness - check_no_baked_defaults tool tool + # Every harness, whatever its kind — none belongs in the image. + check_nothing_baked harness harness '.[]' + # Tools split: kind:"default" is provisioned, kind:"baked-in" is image-level. + check_nothing_baked "default tool" tool '.[] | select(.kind == "default")' else fail "jq is required to read the image's harness and tool catalogs" fi +# The inverse for tools: a kind:"baked-in" tool must actually be present, or the +# check above is passing because the image is simply missing everything. +if command -v jq >/dev/null 2>&1; then + if baked_json=$(run "cd /opt/oh-seed && OH_EXECUTION_TARGET=local oh tool list --json" 2>/dev/null); then + absent=$(jq -r '.[] | select(.kind == "baked-in" and .installed != true) | .id' <<<"$baked_json") + present=$(jq -r '.[] | select(.kind == "baked-in") | .id' <<<"$baked_json") + if [ -z "$present" ]; then + fail "the image's tool catalog declares no kind:\"baked-in\" tool — nothing anchors the image-level half" + elif [ -n "$absent" ]; then + fail "baked-in tools are missing from the image: $(tr '\n' ' ' <<<"$absent")" + else + ok "every baked-in tool is present ($(tr '\n' ' ' <<<"$present"))" + fi + fi +fi + if ((${#failures[@]})); then printf '\nverify-sandbox-image: %d check(s) failed\n' "${#failures[@]}" >&2 printf ' - %s\n' "${failures[@]}" >&2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1394c495..b827f1de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,12 @@ Update policy and release automation live in [`/git`](.claude/skills/git/SKILL.m - Shrink the sandbox image ~540 MB: drop build caches from the baked home seed, stage the seed once via a builder stage, and keep untracked build output out of the build context ([#900](https://github.com/mifunedev/openharness/issues/900)). - **BREAKING:** Stop baking Claude Code, Codex, and Pi into the image; boot installs them into the home mount, so a first boot needs network and runs 60-180s longer ([#904](https://github.com/mifunedev/openharness/issues/904)). - **BREAKING:** Stop baking Herdr and cloudflared into the image; both become `kind: "default"` tools installed into `~/.local/bin` at boot from a pinned, checksum-verified binary ([#906](https://github.com/mifunedev/openharness/issues/906)). +- **BREAKING:** Stop baking OpenCode, DeepAgents, Hermes, and Grok Build into the image; `oh harness install ` installs them into `~/.local` as the sandbox user ([#908](https://github.com/mifunedev/openharness/issues/908)). ### Removed - Remove the `BAKE_HARNESSES` and `AGENTS` build args along with the image bake they gated; the harness catalog is the only source of truth for what gets installed ([#904](https://github.com/mifunedev/openharness/issues/904)). - Remove Cloudflare's apt repository and its bookworm-suite pin from the image; Docker's is now the only third-party apt source ([#906](https://github.com/mifunedev/openharness/issues/906)). +- Remove the four optional-harness build args and the dead `buildArg` catalog field; the `install.*` keys keep working and now drive boot provisioning ([#908](https://github.com/mifunedev/openharness/issues/908)). - **BREAKING:** Retire the `projectRoot` / `OH_PROJECT_ROOT` config knob — the checkout is fixed at `/home/sandbox/harness`, nested inside the home mount ([#898](https://github.com/mifunedev/openharness/issues/898)). ### Added @@ -24,6 +26,7 @@ Update policy and release automation live in [`/git`](.claude/skills/git/SKILL.m - Add `oh-home-mount.sh`, a tier-A probe holding the single-`$HOME`-mount contract: one mount per compose file, the baked `/opt/home-seed`, and the checkout prune that replaces `-xdev` ([#898](https://github.com/mifunedev/openharness/issues/898)). - Assert boot-provisioned harnesses in the boot smoke and reject a baked default harness in `verify-sandbox-image.sh`, so CI exercises the install path ([#904](https://github.com/mifunedev/openharness/issues/904)). - Add `oh tool list --defaults` and generalize the boot provisioner over both catalogs as `provision-defaults.sh` (`OH_PROVISION_DEFAULTS`) ([#906](https://github.com/mifunedev/openharness/issues/906)). +- Fix `oh harness install` hanging on a sudo password prompt inside the sandbox: every harness now installs as the sandbox user, so no install path needs root ([#908](https://github.com/mifunedev/openharness/issues/908)). - Add `skills-task-tool-coupling.sh`, a tier-A probe holding the canonical skill pack and the sandbox in agreement about the Claude-Code-only task tools ([#886](https://github.com/mifunedev/openharness/issues/886)). ### Fixed diff --git a/docs/configuration.md b/docs/configuration.md index 199c967e..25babe7e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -61,10 +61,10 @@ to `oh harness` names: `opencode`, `grok-build`, `deepagents`, `hermes`. | Field | Type | Default | Compose variable | What it does | | --- | --- | --- | --- | --- | -| `install.opencode` | boolean | `false` | `INSTALL_OPENCODE` | Build the OpenCode CLI into the image. | -| `install.grokBuild` | boolean | `false` | `INSTALL_GROK_BUILD` | Build the Grok Build CLI into the image. | -| `install.deepagents` | boolean | `false` | `INSTALL_DEEPAGENTS` | Build the DeepAgents CLI into the image. | -| `install.hermes` | boolean | `false` | `INSTALL_HERMES` | Build the Hermes CLI into the image and enable its runtime wiring. | +| `install.opencode` | boolean | `false` | `INSTALL_OPENCODE` | Install the OpenCode CLI into `~/.local` at boot. `oh harness install opencode` sets it and installs now. | +| `install.grokBuild` | boolean | `false` | `INSTALL_GROK_BUILD` | Install the Grok Build CLI into `~/.local` at boot. `oh harness install grok-build` sets it and installs now. | +| `install.deepagents` | boolean | `false` | `INSTALL_DEEPAGENTS` | Install the DeepAgents CLI into `~/.local` at boot. `oh harness install deepagents` sets it and installs now. | +| `install.hermes` | boolean | `false` | `INSTALL_HERMES` | Install the Hermes CLI into `~/.local` at boot and enable its runtime wiring (skill vendoring, `auth.json`). | | `install.agentBrowser` | boolean | `false` | `INSTALL_AGENT_BROWSER` | Install agent-browser and Chromium (about 1 GB). | ### Access diff --git a/docs/harnesses/deepagents.md b/docs/harnesses/deepagents.md index 9f74fee7..cb924d64 100644 --- a/docs/harnesses/deepagents.md +++ b/docs/harnesses/deepagents.md @@ -44,14 +44,14 @@ install: Or set `INSTALL_DEEPAGENTS=true` in `.devcontainer/.env` (legacy). -Then rebuild/restart the sandbox: +Either way the boot provisioner installs it on the next start — no rebuild: ```bash oh stop && oh sandbox ``` -Open Harness installs the upstream CLI during image build via `uv tool -install` into image-level paths: +Open Harness installs the upstream CLI with `uv tool install` as the `sandbox` +user, which lands it in `~/.local/bin` inside the home mount: ```bash uv tool install deepagents-cli @@ -66,8 +66,9 @@ Verify the install inside the sandbox: deepagents -v ``` -If the command is not found, confirm `INSTALL_DEEPAGENTS=true` is set in -`.devcontainer/.env`, then rebuild with `oh stop && oh sandbox`. +If the command is not found, run `oh harness install deepagents` — it installs +into `~/.local/bin` in the running sandbox and sets `install.deepagents` so a +fresh home mount reinstalls it at boot. No rebuild is involved. ## Authentication and provider keys diff --git a/docs/harnesses/grok-build.md b/docs/harnesses/grok-build.md index c31552c1..d3044d41 100644 --- a/docs/harnesses/grok-build.md +++ b/docs/harnesses/grok-build.md @@ -29,22 +29,22 @@ install: grok_build: true ``` -Or set the legacy build flag in `.devcontainer/.env`: +Or set the legacy flag in `.devcontainer/.env`: ```bash INSTALL_GROK_BUILD=true ``` -Then rebuild/restart the sandbox: +Either way the boot provisioner installs it on the next start — no rebuild: ```bash oh stop && oh sandbox ``` -Open Harness uses the upstream installer during image build, pinned to the version verified when this support was added: +Open Harness uses the upstream installer as the `sandbox` user, pinned to the version verified when this support was added, with the binary directed into the home mount: ```bash -curl -fsSL https://x.ai/cli/install.sh | bash -s 0.2.39 +curl -fsSL https://x.ai/cli/install.sh | GROK_BIN_DIR="$HOME/.local/bin" bash -s 0.2.39 ``` Review-first equivalent for manual inspection: @@ -63,7 +63,7 @@ Verify the install inside the sandbox: grok --version ``` -If `grok` is not found, confirm `INSTALL_GROK_BUILD=true` is set in `.devcontainer/.env` and rebuild. +If `grok` is not found, run `oh harness install grok-build` — it installs into `~/.local/bin` and sets `install.grokBuild` so a fresh home mount reinstalls it at boot. ## Authentication diff --git a/docs/harnesses/hermes.md b/docs/harnesses/hermes.md index e386ddbb..f217257c 100644 --- a/docs/harnesses/hermes.md +++ b/docs/harnesses/hermes.md @@ -10,7 +10,7 @@ skills from experience, scheduled task automation, sub-agent delegation, container sandboxing across multiple backends, and bridges to chat platforms (Telegram, Discord, Slack, WhatsApp, Signal, Email). -Hermes is an **optional image-level runtime** in Open Harness. When enabled (set `INSTALL_HERMES=true` in `.devcontainer/.env`), it sits alongside `claude`, `codex`, +Hermes is an **optional harness** in Open Harness. Install it with `oh harness install hermes` (or set `install.hermes` / `INSTALL_HERMES=true`, which the boot provisioner honours); it then sits alongside `claude`, `codex`, `pi`, `opencode`, and `deepagents` as a sandbox CLI primitive. See the upstream documentation below for canonical facts about Hermes. @@ -54,19 +54,20 @@ Then rebuild/restart the sandbox: oh stop && oh sandbox ``` -The executable is installed during image build, not at container boot, so -an enabled sandbox has `hermes` on PATH immediately: +The executable is installed by `oh harness install hermes`, or at boot on a +fresh home mount when `install.hermes` is true. Once installed it persists in +the home mount, so later boots find it on PATH immediately: ```bash hermes --version ``` -At image build time, Open Harness runs the official installer with setup -and browser installation disabled: +Open Harness runs the official installer as the `sandbox` user with setup and +browser installation disabled, directing it into the home mount: ```bash curl -fsSL https://hermes-agent.nousresearch.com/install.sh \ - | bash -s -- --skip-setup --skip-browser + | HERMES_INSTALL_DIR="$HOME/.local/lib/hermes-agent" bash -s -- --skip-setup --skip-browser ``` Review-first equivalent for manual inspection: @@ -122,10 +123,11 @@ secrets from this directory. the bind-mounted `.hermes/` directory from the checkout. Remove that directory manually if you want a full Hermes project-state reset. -The Hermes binary itself is installed in the image when -`INSTALL_HERMES=true` is set in `.devcontainer/.env`, under the installer's root Linux FHS layout -(`/usr/local/lib/hermes-agent` with a `/usr/local/bin/hermes` launcher). -Disabling the flag on a future rebuild omits the executable; project-local +The Hermes binary is installed into the home mount by `oh harness install hermes` +— at `~/.local/lib/hermes-agent` with a `~/.local/bin/hermes` launcher — and the +boot provisioner reinstalls it on a fresh home mount whenever `install.hermes` is +true. Nothing about it lives in the image. Disabling the flag stops the +reinstall but leaves an existing install in place; project-local state remains in `.hermes/` until removed. ## Common usage @@ -276,7 +278,7 @@ vars — see upstream Hermes documentation for the full list. The sandbox onboarding banner reports Hermes as: -- `❌ not installed` — set `INSTALL_HERMES=true` in `.devcontainer/.env` and rebuild — when the binary is absent from PATH. +- `❌ not installed` — run `oh harness install hermes` — when the binary is absent from PATH. - `✅ installed — run: hermes setup` — when the binary is on PATH but `~/.hermes/auth.json` is absent or empty. - `✅ authenticated` — when `~/.hermes/auth.json` exists and is diff --git a/docs/harnesses/opencode.md b/docs/harnesses/opencode.md index c6bc5bfb..9122304f 100644 --- a/docs/harnesses/opencode.md +++ b/docs/harnesses/opencode.md @@ -4,7 +4,7 @@ title: "OpenCode" # OpenCode -OpenCode is a terminal coding agent that can run interactively or execute one-shot tasks. It is an optional image-level runtime in Open Harness; the default harnesses — Claude Code, Codex, and Pi — are provisioned into `~/.local` at boot instead of being baked into the image. +OpenCode is a terminal coding agent that can run interactively or execute one-shot tasks. It is an optional harness in Open Harness. Like every other harness it installs into `~/.local` rather than the image — `oh harness install opencode`. ## Install (optional) @@ -29,16 +29,16 @@ install: Or set `INSTALL_OPENCODE=true` in `.devcontainer/.env` (legacy). -Then rebuild/restart the sandbox: +Either way the boot provisioner installs it on the next start — no rebuild: ```bash oh stop && oh sandbox ``` -Open Harness installs the upstream npm package globally during image build: +Open Harness installs the upstream npm package into the home mount as the `sandbox` user: ```bash -npm install -g opencode-ai +npm --prefix /home/sandbox/.local install -g opencode-ai ``` Verify the install inside the sandbox: diff --git a/docs/harnesses/overview.md b/docs/harnesses/overview.md index 23bb0eba..a5849e68 100644 --- a/docs/harnesses/overview.md +++ b/docs/harnesses/overview.md @@ -4,16 +4,16 @@ title: "Harnesses Overview" # Harnesses Overview -Open Harness ships with three agent CLIs in the default sandbox image: **Claude Code** (default), **Codex**, and **Pi**. **OpenCode**, **DeepAgents**, **Hermes**, and **Grok Build** are optional image-level installs controlled by the `INSTALL_*` keys in `.devcontainer/.env`. **T3 Code** runs on demand via the `/t3` skill (or directly with `npx t3`) as a browser UI on port 3773, and **Prime Agent** installs on demand with `oh harness install prime-agent` — neither has an `INSTALL_*` key, because neither is ever baked into the image. Inside the sandbox, run `herdr` first, then launch whichever agent you prefer from its panes and switch between them at any time. Reserve tmux for Open Harness's managed/headless cron, gateway, and watchdog infrastructure. +Open Harness provisions three agent CLIs into `~/.local` on first boot: **Claude Code** (default), **Codex**, and **Pi**. **OpenCode**, **DeepAgents**, **Hermes**, and **Grok Build** are optional — install one with `oh harness install `, which also sets its `install.*` key so a fresh home mount reinstalls it at boot. No harness is baked into the image. **T3 Code** runs on demand via the `/t3` skill (or directly with `npx t3`) as a browser UI on port 3773, and **Prime Agent** installs on demand with `oh harness install prime-agent` — neither has an `INSTALL_*` key, because neither is ever baked into the image. Inside the sandbox, run `herdr` first, then launch whichever agent you prefer from its panes and switch between them at any time. Reserve tmux for Open Harness's managed/headless cron, gateway, and watchdog infrastructure. Open Harness is the harness; the **agent** is your call. To go beyond the preinstalled options, install via `npm` / `pip` / `cargo` inside the sandbox or edit the Dockerfile. For Pi+Slack specifically, the recommended path is the `pi-messenger-bridge` npm package — see [Slack integration](../integrations/slack.md). The product surface is one developer, one project, one agent — not racing or stacking multiple CLIs against each other. ## Installing a harness -`oh harness` is the shortest path. It does both halves in one command: it sets -the `.devcontainer/.env` `INSTALL_*` flag so the choice survives the next image build, -**and** installs the CLI into the already-running container so it is usable now. -It never rebuilds or restarts the sandbox. +`oh harness` is the only path, and it does both halves in one command: it sets +the `install.*` key in `oh.json` so a fresh home mount reinstalls the harness at +boot, **and** installs the CLI into the already-running container so it is usable +now. It never rebuilds or restarts the sandbox. ```bash oh harness list # what exists, what is enabled, what is installed @@ -64,7 +64,7 @@ claude --version codex --version pi --version -# Optional image-level CLIs, present only when enabled in .devcontainer/.env: +# Optional CLIs, present only after `oh harness install ` (or its install.* key): opencode --version # install.opencode: true deepagents -v # install.deepagents: true hermes --version # install.hermes: true diff --git a/docs/harnesses/prime-agent.md b/docs/harnesses/prime-agent.md index 0b459caa..01d3426b 100644 --- a/docs/harnesses/prime-agent.md +++ b/docs/harnesses/prime-agent.md @@ -108,7 +108,7 @@ stops all of them. These are deliberate gaps, not oversights: -- **No image-level install.** There is no `INSTALL_PRIME_AGENT` build arg, no compose auth +- **No install.\* key.** There is no `INSTALL_PRIME_AGENT` key, no compose auth volume, and no `install.*` field in `oh.json`. The harness is `on-demand` only. - **No `/spec execute` provider integration.** `/spec execute` uses the active Advisor session and does not launch a provider-specific wrapper; run Prime Agent directly only diff --git a/docs/quickstart.md b/docs/quickstart.md index 5f89d65d..f7d4c20c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -126,7 +126,7 @@ them in place without a rebuild. A first boot on a fresh home mount therefore ne network access and takes a minute or two longer; the sandbox still comes up as a usable shell if the registry is unreachable, and you can retry with `bash .oh/scripts/provision-defaults.sh`. OpenCode, DeepAgents, Hermes, and Grok -Build are optional image-level installs; T3 Code runs on demand via the `/t3` skill +Build are optional installs via `oh harness install `; T3 Code runs on demand via the `/t3` skill or direct `npx`. Authenticate at least one harness before use. > **Simplest cross-provider login — device mode via `/login`.** The most straightforward path