Audit remediation 20261018 follow-up: find-exec placeholder P0, fork taint, lesson-once, guard root, Formatter built-in leg, 1.0.11 bump

This commit is contained in:
John Smith committed 2026-10-18 04:30:25 -04:00
1 parent a8aa58a669
commit 6da3c32412
12 files changed
+1127 -23

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
# `@heretek-ai/opencode-harness` (`v1.0.10`)
# `@heretek-ai/opencode-harness` (`v1.0.11`)
Plugin-first OpenCode v2 in-process harness targeting **Muse 1.3** (`opencode-go/muse-spark-1.3-contributor`): zero-patch built-in tool interceptors (`edit`, `read`, `write`, `patch`, `shell`, `subagent`), post-formatter FNV-1a `<post-edit-anchors>` resync receipts, adaptive `session.hook("retry")` stream/overload recovery, cross-file relative import & named-export contract verification, lexer-safe atomic multi-file `rename_symbol`, Dual Direct + Code Mode (`tools.harness.*`) tool registration, 4-hex FNV-1a hash-anchored file edits, AST structural codemods, PageRank repository mapping, lexer-aware symbol reference & call-site search, blast-radius, circular-dependency & dead-code architecture auditing, all-or-nothing atomic transactions, warm-cached post-mutation diagnostics, stack-frame `<fault-context>` localization, head+tail anti-truncation output compaction, KV-cache-safe context budgeting, automatic circuit-breaker episodic lesson synthesis, role/path/contract/taint permission guards, a doom-loop circuit breaker, and DOM geometry auditing.
+183
View File
@@ -0,0 +1,183 @@
# Stage Report — Audit Remediation 20261018 (`audit-remediation-20261018`)
- **Stage ID:** `audit-remediation-20261018`
- **Branch:** `audit-remediation-20261018` (worktree: `worktrees/audit-remediation-20261018`)
- **Baseline:** `a8aa58a` (`Add drift-guard verification: VERDICT PASS (audit 20261018)`)
- **Source findings:** `AUDIT_REPORT_FINDINGS.md` (audit date 2026-10-18, package `1.0.10`)
- **Scope:** single-session OpenCode v2 in-process plugin ONLY (no WebUI / HTTP /
fleet / worktree-manager / research-engine code — refused by charter).
## Summary
All five code-remediation findings are fixed, regression-pinned, and green
across every gate (`bun run check`, `bun test`, `make check`, `make accept`,
`bun run build`), plus a live host-test run against the pinned upstream
(`anomalyco/opencode@ed772f5`) proving the new built-in-`edit` leg with the
real upstream `Formatter.file`. Package bumped `1.0.10` → `1.0.11` in
`package.json` + `src/version.ts` (+ README title, coupled by test).
**No tags were pushed, nothing was published, no GitHub Release was created —
those are human-only steps listed verbatim in §7.**
## Deliverables per finding
### OH-6.5-find-exec-placeholder (P0) — `find <root> -exec rm -rf {} \;` bypass
- `src/guards/shell-parse.ts`
- New `findPathOperands()` (leading path operands before the first
expression primary) + `findHasWipeRoot()` (any operand is a root-wipe
target: `/`, `/*`, `*`, `~`, `~/*`, `~/…`, `$HOME`, `${HOME}`, …).
- New `findExecPayloadIsPlaceholderWipe()` (`-exec`/`-execdir`/`-ok`/`-okdir`
payload running `rm` + recursive/force flags over the `{}` placeholder,
including wrapper-stripped payloads and `$(…)`/backtick substitution).
- `segmentIsDestructive()` `find` branch: wipe-root + placeholder-wipe
payload → `destructive` (fail-closed). Scoped roots stay `write`;
`find /tmp -name x -delete` and `find /tmp -exec rm -rf / \;` behavior
preserved.
- `src/guards/taint-guard.ts`
- `evaluateTaintGuard()` denies destructive shell while tainted
(`TAINT-DESTRUCTIVE-1`) — the taint layer no longer reads `allow` for a
wipe idiom (permission layer denies first for every role regardless).
- Tests (`test/audit-20261018-remediation.test.ts`, 6 new):
`\;`/`+` × `-exec`/`-execdir`/`-ok`/`-okdir` destructive matrix; `/`/`~`/
`$HOME`/`${HOME}`/`/*` roots; scoped-root write preservation; builder/
architect/director/manager/qa verdicts all `rule-1-destructive`; tainted
`allow`→`deny` at both layers + clean-session layering pins.
### OH-6.5-guard-root-unwired (P3) — `guardConfig.workspaceRoot` default
- `src/index.ts` (`resolveHarnessOptions`): custom `guardConfig` without
`workspaceRoot` now defaults to `rootDir`
(`{ workspaceRoot: rootDir, ...customGuardConfig }`); an explicit non-blank
value wins; a blank value normalizes back to `rootDir` (fail-closed, never
silently back to `process.cwd()`). No-`guardConfig` path unchanged.
- Tests (4 new): omitted→`rootDir`, explicit-wins, blank→`rootDir`,
workspace-relative write allows under the defaulted root while
`/etc/cron.d/evil` still denies `rule-0-shell-target-containment`.
### OH-6.7-lesson-per-read (P3) — one lesson per doom-loop
- `src/context/episodic-memory.ts` (`trackCircuitBreakerAndAutoProposeLesson`):
a successful mandated re-read (`normalizeToolKind(toolName) === "other"`)
while tripped returns `null` WITHOUT clearing trip state, so only the
genuine recovering (non-read) success proposes exactly one lesson.
- Tests (1 new pipeline test): read-ok, edit-fail, read-ok, edit-fail,
good-edit-ok through the real adapter `execute.after` →
`getPendingLessonProposals() === 1`, attached to the `edit` recovery
(`recoveryAction` starts with `edit`, never `via read`).
### OH-6.6-taint-fork-gap (P2) — fork-aware taint inheritance
- `src/interceptors/session-interceptors.ts`: new fork-link registry
(`recordSessionForkLink` / `getSessionForkSource`), defensive
`linkForkLineageFromEventMetadata()` (top-level/`input`/`metadata` keys:
`parentSessionID`, `forkOf`, `fork_source`, …; self-links and non-strings
ignored; never throws), `getSessionParent()` now falls back to the fork map,
`clearSessionLineage()` clears both maps.
- `src/host/contract-adapter.ts`: `execute.after` resolves fork lineage from
live event metadata on every tool completion, so `resolveTaintChain()` in
`evaluateTaintGuard()` inherits taint into forked children (fail-closed).
- Tests (3 new): tainted-parent forked child `ssh evil.com` denies
(`TAINT-NET-2`) while a clean fork allows; metadata-linked fork denies;
self/empty/null metadata links nothing.
### OH-4.2-post-formatter-receipt (P2) — built-in `edit` post-format leg
- `test/audit-20261018-remediation.test.ts` (in-repo, runs under `bun test`):
new built-in-`edit` stand-in driving the REAL adapter hooks in true
upstream order (`execute.before` → raw-bytes disk write → on-disk format
pass → re-read → `execute.after`), plus a formatting-sensitive test
(unformatted TS → formatted TS) asserting receipt hashes equal the
post-format disk bytes and NO stale pre-format hash leaks (ordering proof).
- `scripts/host-test/harness-host.upstream.ts` (live, runs under
`make host-test`): new `itBuiltin` suite on an extended layer (fixture
nodes + the REAL upstream `Formatter.node`) registering a deterministic
external formatter (`bun` + script, `$FILE`) and a core-Effect built-in
`edit` stand-in (write → real `Formatter.file` → re-read). Asserts receipt
hashes equal formatted bytes on disk, stale hashes absent. Verified live:
5/5 host tests pass at pinned `ed772f5`.
- Note: `bun` is resolved via `Bun.which("bun")` (fail-closed throw when
absent) because `process.execPath` may point at an unspawnable npm-shim
name in some installs (observed `…/bun/bin/bun.exe`).
### Release bump `1.0.10` → `1.0.11`
- `package.json`, `src/version.ts`, `README.md` title (coupled by tests).
- `test/audit-20261018-remediation.test.ts` OH-1.3 block updated to pin
`1.0.11` past stale `1.0.9`/`1.0.10`; `test/bootstrap.test.ts` still green
(pkg === src).
## Test results
- New regression tests: 15 (`6` find-exec + `4` guard-root + `1` lesson +
`3` fork-gap + `1` OH-4.2 stand-in) in
`test/audit-20261018-remediation.test.ts`, plus 1 live host test.
- `bun test`: **358 pass / 0 fail** (was 343 at baseline).
- Each new suite verified present by `-t` filtered runs (6 / 4 / 1 / 3 /
OH-4.2 ×2 green in isolation).
- Host test (`OPENCODE_DIR=<scratch-copy>@ed772f5`): **5 pass / 0 fail**,
including the new built-in-leg test (ran solo: 1 pass / 9 expects).
## Gate outputs (worktree root, all exit 0)
- `bun run check` → `tsc --noEmit`, exit 0.
- `bun test` → 358 pass / 0 fail, exit 0.
- `make check` → check + test, exit 0.
- `make accept` → waves 1–7 `ALL GATES GREEN` (incl. zero-stub, scope-refusal,
KV-cache scans), exit 0.
- `bun run build` → `tsc -p tsconfig.build.json`, exit 0; `dist/version.js`
carries `1.0.11`.
- `make host-test` (networked, pinned `ed772f5`): 5 pass / 0 fail.
(Scratch upstream copy at `/dev/shm/ochost`, outside the repo.)
## Self-audit notes
- **Zero stubs:** `grep TODO src/ test/` → zero hits; no `test.skip`/
`describe.skip`/`.only` (one `exit(` false positive inside a command
string); no `|| true` in scripts (only the wave-8 guard that *fails* on
it); the single `catch { return undefined; }` (fork-metadata linker) is a
documented fail-closed total function (unparseable events link nothing),
not a swallowed error.
- **KV-cache invariants:** untouched — no changes under `src/context/`
except the episodic proposal gate (no tag-format, ordering, or
`event.system` changes); `make accept` wave-2/5 KV checks green.
- **Scope refusal:** no WebUI/HTTP/fleet/worktree/research code in `src/`
(`make accept` scope scan green).
- **Red-proof:** new suites assert exact rules/denials that fail if the
corresponding production leg is neutered (find-branch removal,
threshold sabotage, and Builder/Architect allow all go RED by
construction); formal QA sabotage + drift `VERDICT` remain downstream.
- Previous-report files (`docs/reports/wave-audit-20261018-verification.md`,
`audit-20261018-remediation-implementation.md`) intentionally left
untouched as historical record.
## Human follow-ups (NOT executed — forbidden to agents)
Tag, publish, and release `1.0.11` from a green `main` after merge:
```bash
git tag v1.0.11
git push --tags
npm publish --provenance --access public
gh release create v1.0.11 --generate-notes
npm view @heretek-ai/opencode-harness dist-tags.latest # expect 1.0.11
```
This closes OH-1.3-stale-npm-latest (P0) and OH-1.1-unreleased (P1) /
OH-1.1-no-gh-release (P3), which cannot be closed from inside the sandbox.
## Files changed
- `src/guards/shell-parse.ts` (+108): find-path operands, placeholder-wipe
detector, `find`-branch destructive leg.
- `src/guards/taint-guard.ts` (+18/-): `TAINT-DESTRUCTIVE-1` taint-layer leg.
- `src/guards/permission-guard.ts`: untouched (Rule 1 ordering already denies
first — verified by tests).
- `src/index.ts` (+31/-): `guardConfig.workspaceRoot` → `rootDir` default.
- `src/context/episodic-memory.ts` (+12): re-read-is-not-recovery gate.
- `src/interceptors/session-interceptors.ts` (+98/-): fork registry +
metadata linker + fork-aware `getSessionParent`.
- `src/host/contract-adapter.ts` (+7): per-completion fork-link resolution.
- `test/audit-20261018-remediation.test.ts` (+416): 15 regression tests.
- `scripts/host-test/harness-host.upstream.ts` (+271): live Formatter leg.
- `package.json`, `src/version.ts`, `README.md`: `1.0.11` bump.
- `docs/reports/audit-remediation-20261018.md`: this report (new).
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@heretek-ai/opencode-harness",
"version": "1.0.10",
"version": "1.0.11",
"description": "Plugin-first OpenCode v2 in-process harness for Muse 1.3: zero-patch Edit/Read/Patch/Shell/Subagent/Retry interceptors, post-formatter FNV-1a anchor resync receipts, cross-file import/export contract verification, atomic multi-file rename_symbol, Dual Direct + Code Mode (tools.harness.*) tools, Hashline edits, AST codemods, PageRank RepoMap, blast-radius & dead-code architecture auditing, atomic transactions, KV-cache-safe context budgeting, permission/taint guards, and DOM geometry auditing.",
"license": "MIT",
"repository": {
+267 -4
View File
@@ -19,13 +19,53 @@ import { PluginHost } from "@opencode/core/plugin/host"
import { PluginPromise } from "@opencode/core/plugin/promise"
import { PluginHooks } from "@opencode/core/plugin/hooks"
import { Tool } from "@opencode/core/tool"
import { Formatter } from "@opencode/core/formatter"
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
import { LayerNode } from "@opencode/util/effect/layer-node"
import { AISDK } from "@opencode/core/aisdk"
import { Command } from "@opencode/core/command"
import { Config } from "@opencode/core/config"
import { Credential } from "@opencode/core/credential"
import { Database } from "@opencode/core/database/database"
import { FileAccess } from "@opencode/core/file-access"
import { LayerNodePlatform } from "@opencode/util/effect/app-node-platform"
import { AppProcess } from "@opencode/util/process"
import { Bus } from "@opencode/core/bus"
import { FileSystem } from "@opencode/core/filesystem"
import { FSUtil } from "@opencode/util/fs-util"
import { Form } from "@opencode/core/form"
import { Generate } from "@opencode/core/generate"
import { Integration } from "@opencode/core/integration"
import { KV } from "@opencode/core/kv"
import { Location } from "@opencode/core/location"
import { ManagedPolicy } from "@opencode/core/managed-policy"
import { Mcp } from "@opencode/core/mcp/index"
import { Model } from "@opencode/core/model"
import { Npm } from "@opencode/util/npm"
import { Provider } from "@opencode/core/provider"
import { Session } from "@opencode/schema/session"
import { Session as CoreSession } from "@opencode/core/session"
import { SessionMessage } from "@opencode/schema/session-message"
import { Effect, Exit, Scope } from "effect"
import { PersistentPty } from "@opencode/core/persistent-pty"
import { LocationServiceMap } from "@opencode/core/location-service-map"
import { Permission } from "@opencode/core/permission"
import { Reference } from "@opencode/core/reference"
import { Rpc } from "@opencode/core/rpc"
import { Skill } from "@opencode/core/skill"
import { SkillDiscovery } from "@opencode/core/skill/discovery"
import { Vcs } from "@opencode/core/vcs"
import { Watcher } from "@opencode/core/filesystem/watcher"
import { WebSearch } from "@opencode/core/websearch"
import { Worktree } from "@opencode/core/worktree"
import { WorktreeStrategies } from "@opencode/core/worktree/strategies"
import { Effect, Exit, Layer, Scope } from "effect"
import { testEffect } from "../lib/effect"
import { readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { PluginTestLayer } from "./fixture"
import { tempLocationLayer } from "../fixture/location"
import { emptyMcpLayer } from "../fixture/mcp"
const HARNESS_ROOT = process.env.HARNESS_ROOT
if (!HARNESS_ROOT) throw new Error("HARNESS_ROOT must point at the opencode-harness checkout")
@@ -370,9 +410,9 @@ it.live("opencode-harness post-edit receipt matches post-write disk bytes (OH-4.
// TS becomes formatted TS, and the `<post-edit-anchors>` receipt appended
// by `execute.after` must equal the bytes read from disk AFTER the write
// completes — the same ordering the built-in `edit` path relies on after
// upstream `Formatter.file` runs (the built-in leg is pinned at the
// adapter level in `test/audit-20261018-remediation.test.ts` because
// `PluginTestLayer` registers no built-in edit tool).
// upstream `Formatter.file` runs (the built-in leg runs live below in
// the OH-4.2 built-in-leg test with the real upstream Formatter.file;
// this test pins the harness-tool leg of the same ordering invariant).
const probeName = `oh42-format-probe-${Date.now().toString(36)}.ts`
const probeRel = probeName
const probeAbs = join(HARNESS_ROOT, probeName)
@@ -412,3 +452,226 @@ it.live("opencode-harness post-edit receipt matches post-write disk bytes (OH-4.
}
}),
)
// Test layer for the OH-4.2 built-in leg: the shared PluginTestLayer plus
// the REAL upstream Formatter node (AppNodeBuilder wires location-scoped
// nodes; a bare LayerNode.compile cannot bind @opencode/Location). All other
// nodes and replacements mirror the upstream fixture exactly so harness
// wiring behaves identically to the main suite.
const oh42NpmLayer = Layer.succeed(
Npm.Service,
Npm.Service.of({
add: (name) => Effect.succeed({ directory: "", name }),
resolve: (name) => Effect.succeed({ directory: "", name }),
check: () => Effect.succeed(false),
update: (name) => Effect.succeed({ directory: "", name }),
which: () => Effect.undefined,
}),
)
const oh42GenerateLayer = Layer.succeed(Generate.Service, Generate.Service.of({ text: () => Effect.succeed("") }))
const oh42ConfigLayer = Config.testLayer()
const oh42PermissionLayer = Layer.succeed(
Permission.Service,
Permission.Service.of({
close: Effect.void,
ask: (input) => Effect.succeed({ id: input.id ?? Permission.ID.create(), effect: "ask" }),
assert: () => Effect.void,
reply: () => Effect.void,
get: () => Effect.succeed(undefined),
forSession: () => Effect.succeed([]),
list: () => Effect.succeed([]),
}),
)
const Oh42FormatterLayer = AppNodeBuilder.build(
LayerNode.group([
AppProcess.node,
FileSystem.node,
FSUtil.node,
Location.node,
Npm.node,
Credential.node,
Config.node,
Database.node,
FileAccess.node,
Bus.node,
Form.node,
Generate.node,
LayerNodePlatform.httpClient,
Plugin.node,
Agent.node,
AISDK.node,
Provider.node,
Model.node,
Command.node,
Integration.node,
KV.node,
ManagedPolicy.node,
Mcp.node,
CoreSession.node,
PersistentPty.node,
LocationServiceMap.node,
Permission.node,
PluginHooks.node,
Reference.node,
Rpc.node,
Skill.node,
SkillDiscovery.node,
Tool.node,
Vcs.node,
Watcher.node,
WebSearch.node,
Worktree.node,
WorktreeStrategies.node,
Formatter.node,
]),
[
Location.node.replace(tempLocationLayer),
Npm.node.replace(oh42NpmLayer),
Config.node.replace(oh42ConfigLayer),
Mcp.node.replace(emptyMcpLayer),
Generate.node.replace(oh42GenerateLayer),
Permission.node.replace(oh42PermissionLayer),
],
).pipe(Layer.provideMerge(oh42ConfigLayer))
const itBuiltin = testEffect(Oh42FormatterLayer)
itBuiltin.live("opencode-harness built-in edit receipt matches post-Formatter.file disk bytes (OH-4.2 built-in leg)", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const tools = yield* Tool.Service
yield* PluginPromise.fromPromise({
id: harness.id,
setup: (ctx: unknown) => harness.setup(ctx, { rootDir: HARNESS_ROOT }),
}).effect(yield* PluginHost.make(plugins))
const probeName = `oh42-builtin-probe-${Date.now().toString(36)}.ts`
const probeAbs = join(HARNESS_ROOT, probeName)
const oldString = "return title;"
const newString = "return title.trim();"
const expectedFormatted = [
"export function renderCard(title: string) {",
" return title.trim();",
"}",
]
writeFileSync(
probeAbs,
`${["export function renderCard( title:string ) {", oldString, "}"].join("\n")}\n`,
"utf8",
)
// Formatter runtime: the same `bun` running this test (fail-closed when
// unresolvable — `process.execPath` may point at an npm shim name that
// cannot spawn, so it is never used here).
const bunBin = Bun.which("bun")
if (!bunBin) throw new Error("OH-4.2 probe formatter requires `bun` on PATH (fail-closed)")
// Deterministic external formatter process: real bytes rewritten on disk
// through the REAL upstream Formatter.file code path (child-process spawn
// with $FILE replacement), never an in-test copy.
const fmtScript = join(tmpdir(), `harness-probe-fmt-${Date.now().toString(36)}.mjs`)
writeFileSync(
fmtScript,
[
`import { readFileSync, writeFileSync } from "node:fs"`,
`const target = process.argv[process.argv.length - 1]`,
`const raw = readFileSync(target, "utf8")`,
`const out = []`,
`let depth = 0`,
`for (const line of raw.split("\\n")) {`,
` const trimmed = line.trim().replace(/[ \\t]+/g, " ").replace(/\\(\\s+/g, "(").replace(/\\s+\\)/g, ")").replace(/:(?=\\S)/g, ": ").replace(/,(?=\\S)/g, ", ").replace(/\\)\\s*\\{/g, ") {")`,
` if (trimmed === "") continue`,
` if (/^\\}/.test(trimmed)) depth = Math.max(0, depth - 1)`,
` out.push(" ".repeat(depth) + trimmed)`,
` if (/\\{$/.test(trimmed)) depth += 1`,
`}`,
`writeFileSync(target, out.join("\\n") + "\\n")`,
``,
].join("\n"),
"utf8",
)
try {
const formatter = yield* Formatter.Service
yield* formatter.transform((editor) => {
editor.set({
name: "harness-probe-tsfmt",
extensions: [".ts"],
enabled: Effect.succeed([bunBin, fmtScript, "$FILE"]),
})
})
// Built-in `edit` stand-in as a core Effect executor (direct
// `tools.transform` registration, like upstream's own tool tests):
// exact oldString→newString disk write, then the REAL upstream
// Formatter.file, then a disk re-read — mirroring EditTool's
// write → formatter.file → syncTextBom order.
yield* tools.transform((editor) =>
editor.add({
name: "edit",
description: "Built-in edit stand-in for the OH-4.2 post-format receipt probe",
input: {
type: "object",
properties: {
path: { type: "string" },
oldString: { type: "string" },
newString: { type: "string" },
},
required: ["path", "oldString", "newString"],
additionalProperties: false,
},
options: { codemode: false, permission: "edit" },
execute: (input: unknown) =>
Effect.gen(function* () {
const args = input as { path: string; oldString: string; newString: string }
const target = join(HARNESS_ROOT, String(args.path))
const prev = readFileSync(target, "utf8")
if (!prev.includes(String(args.oldString))) {
return yield* new Tool.Error({
message: `oldString not found in ${String(args.path)}`,
})
}
writeFileSync(
target,
prev.replace(String(args.oldString), String(args.newString)),
"utf8",
)
const didFormat: boolean = yield* formatter.file(target)
if (!didFormat) {
return yield* new Tool.Error({
message: `probe formatter did not run for ${String(args.path)}`,
})
}
return {
content: [{ type: "text" as const, text: `Edited ${String(args.path)}` }],
}
}),
}),
)
const snapshot = yield* tools.snapshot()
const r = yield* call(snapshot, "edit", { path: probeName, oldString, newString })
expect(Exit.isSuccess(r)).toBe(true)
const body = textOf(r)
expect(body).toContain("<post-edit-anchors")
const onDisk = readFileSync(probeAbs, "utf8")
expect(onDisk).toBe(`${expectedFormatted.join("\n")}\n`)
for (const line of expectedFormatted) {
expect(body).toContain(`${computeLineHash(line)}| ${line}`)
}
for (const stale of [
"export function renderCard( title:string ) {",
"return title;",
"return title.trim();",
]) {
expect(body).not.toContain(`${computeLineHash(stale)}| ${stale}`)
}
} finally {
rmSync(probeAbs, { force: true })
rmSync(fmtScript, { force: true })
}
}),
)
+12
View File
@@ -1,5 +1,6 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, isAbsolute, relative, resolve } from "node:path";
import { normalizeToolKind } from "../guards/circuit-breaker.js";
import type { EpisodicLesson } from "./context-budget.js";
export interface EpisodicLessonProposal {
@@ -157,6 +158,17 @@ export function trackCircuitBreakerAndAutoProposeLesson(params: {
}
if (ok && ACTIVE_TRIPPED_STATE.has(sessionId)) {
// OH-6.7-lesson-per-read: only a NON-read success breaks the loop. The
// intervention text mandates a re-read of the target state before every
// retry, so a successful mandated re-read (`read`/`file_hash_read` — the
// circuit-breaker `other` kind) is compliance, not recovery: proposing a
// "Recovered via read" lesson on every post-trip re-read emits one
// premature lesson per trip while the edit that actually breaks the loop
// gets none. The trip state is retained so the genuine recovering edit
// still proposes exactly one lesson.
if (normalizeToolKind(toolName) === "other") {
return null;
}
const tripState = ACTIVE_TRIPPED_STATE.get(sessionId)!;
ACTIVE_TRIPPED_STATE.delete(sessionId);
return proposeEpisodicLesson({
+108
View File
@@ -808,6 +808,102 @@ export function findExecPayloads(args: string[]): string[] {
}
return payloads;
}
/**
* Leading path operands of a `find` invocation: tokens before the first
* expression primary (`-…`), negation, or paren. `find` with no path
* defaults to `.` (safe — never a wipe root).
*/
function findPathOperands(args: string[]): string[] {
const roots: string[] = [];
for (const a of args) {
if (a === "") {
continue;
}
if (
a.startsWith("-") ||
a === "(" ||
a === ")" ||
a === "!" ||
a === ","
) {
break;
}
if (a === ";" || a === "+" || a === "{}") {
break;
}
roots.push(a);
}
return roots;
}
/** Whether any leading find-path operand is a root-wipe target. */
function findHasWipeRoot(args: string[]): boolean {
return findPathOperands(args).some((p) => isRootWipeTarget(p));
}
/**
* OH-6.5-find-exec-placeholder: `true` when an `-exec`/`-execdir`/`-ok`/
* `-okdir` payload runs `rm` with recursive+force flags over the `{}`
* placeholder (`rm -rf {}`). Callers gate this on a root-wipe find root so
* scoped uses (`find /tmp -name x -exec rm -rf {} \;`) stay write-class. A
* payload substitution (`$(…)`/backticks) with recursive+force flags is
* destructive because a real shell expands it before `rm` sees its targets.
*/
function findExecPayloadIsPlaceholderWipe(payload: string): boolean {
const tokens = stripLeadingWrapperTokens(
tokenizeSegment(canonicalizeShellCommand(payload)),
);
const subst = /`[^`]*`|\$\(/.test(payload);
for (let s = 0; s < tokens.length; s++) {
if (commandBasename(tokens[s] ?? "") !== "rm") {
continue;
}
let hasRecursive = false;
let hasForce = false;
let hasPlaceholder = false;
let endOfOptions = false;
for (let k = s + 1; k < tokens.length; k++) {
const a = tokens[k] ?? "";
if (!endOfOptions && a === "--") {
endOfOptions = true;
continue;
}
if (!endOfOptions && a === "--no-preserve-root") {
return true;
}
if (!endOfOptions && (a === "--recursive" || a === "-r" || a === "-R")) {
hasRecursive = true;
continue;
}
if (!endOfOptions && (a === "--force" || a === "-f")) {
hasForce = true;
continue;
}
if (!endOfOptions && /^-[rRf]+$/.test(a)) {
hasRecursive = true;
hasForce = true;
continue;
}
if (!endOfOptions && /^-[A-Za-z]+$/.test(a)) {
const letters = a.slice(1);
if (/[rR]/.test(letters)) hasRecursive = true;
if (letters.includes("f")) hasForce = true;
continue;
}
if (!endOfOptions && a.startsWith("-") && a !== "-") {
continue;
}
if (a === "{}") {
hasPlaceholder = true;
}
}
if (hasRecursive && hasForce && (hasPlaceholder || subst)) {
return true;
}
}
return false;
}
export function parseCommandSegments(command: string, depth: number = 0): ParsedSegment[] {
if (!command.trim()) return [];
if (depth > MAX_SHELL_UNWRAP_DEPTH) {
@@ -1049,6 +1145,18 @@ function segmentIsDestructive(segment: string): boolean {
return true;
}
}
// OH-6.5-find-exec-placeholder: `{}` expands to every path under the
// find root, so with a root-wipe root (`/`, `~`, `$HOME`, …) any
// `-exec`/`-execdir`/`-ok`/`-okdir` payload running `rm` with
// recursive+force flags over `{}` is a root wipe: destructive.
// Scoped roots stay write-class (`segmentIsWrite`).
if (findHasWipeRoot(args)) {
for (const payload of findExecPayloads(args)) {
if (findExecPayloadIsPlaceholderWipe(payload)) {
return true;
}
}
}
return false;
}
+15 -3
View File
@@ -2,6 +2,7 @@ import { getSessionParent } from "../interceptors/session-interceptors.js";
import { shellNamedFileTargets } from "./permission-guard.js";
import {
commandBasename,
isDestructiveShellCommand,
parseCommandSegments,
shellArgvFileTargets,
stripLeadingWrapperTokens,
@@ -159,9 +160,9 @@ export function clearTaintState(sessionId?: string): void {
/**
* Resolve the nearest taint records for `sessionId`, walking up the
* `recordSessionParentLink` fork/subagent/compaction chain so child sessions
* inherit their parent's taint (fail-closed: a fork of a tainted session is
* tainted).
* `recordSessionParentLink`/`recordSessionForkLink` subagent/fork/compaction
* chain so child sessions inherit their parent's taint (fail-closed: a fork
* of a tainted session is tainted; see OH-6.6-taint-fork-gap).
*/
function resolveTaintChain(sessionId: string): TaintRecord[] {
const visited = new Set<string>();
@@ -245,6 +246,17 @@ export function evaluateTaintGuard(
const latestSource =
taints[taints.length - 1]?.sourceTool ?? "untrusted-source";
// OH-6.5-find-exec-placeholder: while tainted, a destructive shell command
// (`find / -exec rm -rf {} \;`, `rm -rf /`, …) denies fail-closed at the
// taint layer too. The permission layer denies it for every role first,
// but taint must never read as `allow` for a wipe idiom.
if (actionType === "shell" && isDestructiveShellCommand(commandOrPath)) {
return denyTaint(
"TAINT-DESTRUCTIVE-1",
`Blocked destructive shell command "${commandOrPath}" while session is tainted by untrusted tool "${latestSource}".`,
);
}
if (actionType === "file") {
if (matchesTaintSensitivePath(commandOrPath)) {
return denyTaint(
+7
View File
@@ -56,6 +56,7 @@ import {
calibrateSystemPromptInPlace,
decontaminateCompactionEventInPlace,
handleSessionRetryInPlace,
linkForkLineageFromEventMetadata,
recordSessionParentLink,
type SessionRetryEvent,
} from "../interceptors/session-interceptors.js";
@@ -686,6 +687,12 @@ export function wireOpenCodeV2PromiseContext(params: {
"execute.after",
async (event: OpenCodeV2ToolAfterEvent): Promise<void> => {
const sessionId = event.sessionID || "default";
// OH-6.6-taint-fork-gap: resolve fork lineage via session metadata on
// every tool completion. Host-level forks are invisible to the
// promise-plugin hook surface, so without this the forked child
// would shed its parent's taint on subsequent `evaluateTaintGuard`
// calls. Fail-closed: unparseable events link nothing.
linkForkLineageFromEventMetadata(sessionId, event);
const toolName = (event.tool || "").toLowerCase();
const failed = didToolExecutionFail(event);
const ok = !failed;
+24 -7
View File
@@ -274,19 +274,36 @@ export function resolveHarnessOptions(
"opencode-harness: rootDir must be a non-empty string (fail-closed)",
);
}
const guardConfig: GuardPolicyConfig =
settings.guardConfig !== undefined
? settings.guardConfig
: defaultGuardConfig();
const customGuardConfig =
settings.guardConfig !== undefined ? settings.guardConfig : undefined;
if (
guardConfig === null ||
typeof guardConfig !== "object" ||
Array.isArray(guardConfig.lockedContractGlobs) === false
customGuardConfig !== undefined &&
(customGuardConfig === null ||
typeof customGuardConfig !== "object" ||
Array.isArray(customGuardConfig.lockedContractGlobs) === false)
) {
throw new Error(
"opencode-harness: guardConfig must carry a lockedContractGlobs array (fail-closed)",
);
}
// OH-6.5-guard-root-unwired: a custom guardConfig without workspaceRoot
// used to fall back to process.cwd() inside resolveGuardWorkspaceRoot,
// over-blocking legitimate writes when rootDir !== cwd. Default the root
// to rootDir; an explicit non-blank workspaceRoot wins. A blank value
// normalizes back to rootDir so it cannot silently reintroduce the cwd
// fallback (fail-closed toward the operator's stated root).
let guardConfig: GuardPolicyConfig;
if (customGuardConfig === undefined) {
guardConfig = defaultGuardConfig();
} else {
guardConfig = { workspaceRoot: rootDir, ...customGuardConfig };
if (
typeof guardConfig.workspaceRoot !== "string" ||
guardConfig.workspaceRoot.trim() === ""
) {
guardConfig.workspaceRoot = rootDir;
}
}
const budget =
settings.contextBudgetTokens !== undefined
? settings.contextBudgetTokens
+97 -1
View File
@@ -144,6 +144,101 @@ export function decontaminateCompactionEventInPlace(event: {
const SESSION_PARENT_MAP = new Map<string, string>();
/**
* OH-6.6-taint-fork-gap: host-level session forks (`SessionEvent.Forked` →
* `projectFork`) copy tainted history into the child without emitting a
* promise-plugin hook the harness can observe, so no parent link is ever
* recorded for forks through the `recordSessionParentLink` call sites
* (subagent completion, affinity headers). This registry carries fork
* lineage explicitly: hosts and tests record it via
* `recordSessionForkLink` (or implicitly via
* `linkForkLineageFromEventMetadata`), and both lineage resolution
* (`getSessionParent`) and taint inheritance consult it fail-closed — a
* fork of a tainted session is tainted.
*/
const SESSION_FORK_MAP = new Map<string, string>();
export function recordSessionForkLink(
childSessionID: string,
parentSessionID: string,
): void {
if (
!childSessionID ||
!parentSessionID ||
childSessionID === parentSessionID
) {
return;
}
SESSION_FORK_MAP.set(childSessionID, parentSessionID);
}
export function getSessionForkSource(sessionId: string): string | undefined {
return SESSION_FORK_MAP.get(sessionId);
}
/**
* Fork lineage metadata keys observed on child-session tool events. Checked
* on live tool events because the fork projection itself is invisible to
* the promise-plugin hook surface (fail-closed: only non-empty string
* values distinct from the child session id are honored).
*/
const FORK_SOURCE_METADATA_KEYS = [
"parentSessionID",
"parentSessionId",
"parentID",
"forkOf",
"fork_source",
"forkSource",
"sourceSessionID",
"sourceSessionId",
"forkedFrom",
"forkParentSessionID",
] as const;
/**
* OH-6.6-taint-fork-gap: resolve fork lineage via session metadata on live
* tool events. When a forked child session carries its source session id
* under a known metadata key (top-level, `input`, or `metadata`), record
* the fork link so subsequent `evaluateTaintGuard` calls inherit taint.
* Returns the linked parent session id, or `undefined` when nothing
* linkable is present. Never throws (fail-closed: unparseable events link
* nothing).
*/
export function linkForkLineageFromEventMetadata(
sessionId: string,
event: unknown,
): string | undefined {
try {
if (!sessionId || event === null || typeof event !== "object") {
return undefined;
}
const record = event as Record<string, unknown>;
const holders: Array<Record<string, unknown>> = [record];
for (const holderKey of ["input", "metadata"] as const) {
const holder = record[holderKey];
if (holder !== null && typeof holder === "object") {
holders.push(holder as Record<string, unknown>);
}
}
for (const holder of holders) {
for (const key of FORK_SOURCE_METADATA_KEYS) {
const candidate = holder[key];
if (
typeof candidate === "string" &&
candidate !== "" &&
candidate !== sessionId
) {
recordSessionForkLink(sessionId, candidate);
return candidate;
}
}
}
return undefined;
} catch {
return undefined;
}
}
export function recordSessionParentLink(
childSessionID: string,
parentSessionID: string,
@@ -172,10 +267,11 @@ export function resolveRootSessionAffinity(sessionID: string): string {
export function clearSessionLineage(): void {
SESSION_PARENT_MAP.clear();
SESSION_FORK_MAP.clear();
}
export function getSessionParent(sessionId: string): string | undefined {
return SESSION_PARENT_MAP.get(sessionId);
return SESSION_PARENT_MAP.get(sessionId) ?? SESSION_FORK_MAP.get(sessionId);
}
/**
+1 -1
View File
@@ -2,5 +2,5 @@
* Single source of truth for the harness version. Must equal
* `package.json#version` (asserted by `test/bootstrap.test.ts`).
*/
export const HARNESS_RELEASE_VERSION = "1.0.10";
export const HARNESS_RELEASE_VERSION = "1.0.11";
export const HARNESS_VERSION = HARNESS_RELEASE_VERSION;
+411 -5
View File
@@ -1,7 +1,9 @@
/**
* `test/audit-20261018-remediation.test.ts` — Regression suite for every
* finding in `AUDIT_REPORT_FINDINGS.md` (audit date 2026-10-18, package
* `1.0.9` → `1.0.10` remediation branch).
* finding in `AUDIT_REPORT_FINDINGS.md` (audit date 2026-10-18; follow-up
* remediation round: `1.0.10` → `1.0.11` for the find-exec placeholder,
* guard-root, lesson-per-read, taint-fork, and post-formatter built-in-leg
* findings).
*
* Each `describe` block names the finding ID it pins so a reintroduced
* defect goes RED under the exact finding key:
@@ -12,7 +14,7 @@
* - OH-6.7-edit-breaker-unreachable (P1, unit + adapter pipeline)
* - OH-8.2-install-story (P2), OH-scope-loc-budget (P2)
* - OH-4.2-post-formatter-receipt (P2, built-in edit receipt vs disk)
* - OH-1.3-stale-npm-latest (P0, version bump to 1.0.10)
* - OH-1.3-stale-npm-latest (P0, version bump to 1.0.11)
*/
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -21,13 +23,20 @@ import { join, resolve } from "node:path";
import { spawnSync } from "node:child_process";
import {
clearAllSessionAttempts,
clearPendingLessonProposals,
clearSessionLineage,
defaultGuardConfig,
evaluateBoardGuard,
evaluateSession,
getPendingLessonProposals,
getSessionForkSource,
linkForkLineageFromEventMetadata,
recordAndEvaluateCircuitBreaker,
recordSessionForkLink,
recordToolAttempt,
registerContextBudgetHook,
resetSessionAttempts,
resolveHarnessOptions,
shellAllFileTargets,
shellTargetEscapesWorkspace,
toTrippedDirective,
@@ -36,6 +45,7 @@ import {
} from "../src/index";
import {
classifyShellCommand,
findExecPayloads,
isDestructiveShellCommand,
shellArgvFileTargets,
} from "../src/guards/shell-parse";
@@ -59,6 +69,8 @@ function makeTempDir(prefix: string): string {
beforeEach(() => {
clearAllSessionAttempts();
clearTaintState();
clearPendingLessonProposals();
clearSessionLineage();
});
afterEach(() => {
@@ -67,6 +79,8 @@ afterEach(() => {
}
clearAllSessionAttempts();
clearTaintState();
clearPendingLessonProposals();
clearSessionLineage();
});
describe("OH-6.5-wrapper-laundering: every prefix wrapper denies rm -rf /", () => {
@@ -678,11 +692,12 @@ describe("OH-4.2-post-formatter-receipt: built-in edit receipt reflects post-for
});
});
describe("OH-1.3-stale-npm-latest: release carries the P0 fixes at 1.0.10", () => {
describe("OH-1.3-stale-npm-latest: release carries the P0 fixes at 1.0.11", () => {
test("package.json, src/version.ts, and the entrypoint agree past the stale 1.0.9", () => {
expect(HARNESS_RELEASE_VERSION).toBe(pkgJson.version);
expect(HARNESS_RELEASE_VERSION).toBe("1.0.10");
expect(HARNESS_RELEASE_VERSION).toBe("1.0.11");
expect(HARNESS_RELEASE_VERSION).not.toBe("1.0.9");
expect(HARNESS_RELEASE_VERSION).not.toBe("1.0.10");
});
test("README title tracks the release version", () => {
@@ -690,3 +705,394 @@ describe("OH-1.3-stale-npm-latest: release carries the P0 fixes at 1.0.10", () =
expect(readme).toContain(`(\`v${pkgJson.version}\`)`);
});
});
describe("OH-6.5-find-exec-placeholder: {} placeholder wipe laundering", () => {
test("findExecPayloads unwraps -exec/-execdir/-ok/-okdir payloads verbatim", () => {
expect(findExecPayloads(["/", "-exec", "rm", "-rf", "{}", ";"])).toEqual([
"rm -rf {}",
]);
expect(findExecPayloads(["/", "-execdir", "rm", "-rf", "{}", "+"])).toEqual([
"rm -rf {}",
]);
expect(findExecPayloads(["/", "-ok", "rm", "-rf", "{}", ";"])).toEqual([
"rm -rf {}",
]);
expect(findExecPayloads(["/", "-okdir", "rm", "-rf", "{}", ";"])).toEqual([
"rm -rf {}",
]);
});
test("terminator x starter matrix over a wipe root is destructive", () => {
for (const starter of ["-exec", "-execdir", "-ok", "-okdir"]) {
for (const terminator of ["\\;", "+"]) {
const command = `find / ${starter} rm -rf {} ${terminator}`;
expect(classifyShellCommand(command).class).toBe("destructive");
expect(isDestructiveShellCommand(command)).toBe(true);
}
}
});
test("wipe roots deny; scoped roots stay write-class", () => {
for (const root of ["/", "~", "$HOME", "${HOME}", "/*"]) {
const command = `find ${root} -exec rm -rf {} \\;`;
expect(isDestructiveShellCommand(command)).toBe(true);
}
for (const command of [
"find /tmp -name x -exec rm -rf {} \\;",
"find /tmp -name x -exec rm -rf {} +",
"find . -exec rm -rf {} \\;",
"find /tmp/x -okdir rm -rf {} \\;",
]) {
expect(classifyShellCommand(command).class).toBe("write");
expect(isDestructiveShellCommand(command)).toBe(false);
}
});
test("builder/architect/director/qa verdicts deny with rule-1-destructive", () => {
const command = "find / -exec rm -rf {} \\;";
for (const role of [
"builder",
"architect",
"director",
"manager",
"qa-adversary",
"agency-qa",
]) {
const verdict = evaluateBoardGuard({ role, tool: "shell", command });
expect(verdict.allowed).toBe(false);
expect(verdict.rule).toBe("rule-1-destructive");
}
});
test("preserved taxonomy: scoped delete allows; literal wipe payload still denies", () => {
expect(classifyShellCommand("find /tmp -name x -delete").class).toBe("write");
expect(
evaluateBoardGuard({
role: "builder",
tool: "shell",
command: "find /tmp -name x -delete",
}).allowed,
).toBe(true);
expect(isDestructiveShellCommand("find /tmp -exec rm -rf / \\;")).toBe(true);
const scoped = "find /tmp -name x -exec rm -rf {} \\;";
expect(
evaluateBoardGuard({ role: "builder", tool: "shell", command: scoped }).allowed,
).toBe(true);
expect(
evaluateBoardGuard({ role: "architect", tool: "shell", command: scoped })
.allowed,
).toBe(true);
});
test("tainted session: placeholder wipe denies at both layers (allow-to-deny)", () => {
const session = `oh65-find-taint-${Date.now()}`;
const command = "find / -exec rm -rf {} \\;";
recordUntrustedContentTaint(session, "webfetch");
const tainted = evaluateTaintGuard(session, command, "shell");
expect(tainted.action).toBe("deny");
expect(tainted.ruleId).toBe("TAINT-DESTRUCTIVE-1");
expect(evaluateTaintGuard(session, "rm -rf /", "shell").action).toBe("deny");
expect(
evaluateBoardGuard({ role: "builder", tool: "shell", command }).allowed,
).toBe(false);
clearTaintState(session);
expect(evaluateTaintGuard(session, command, "shell").action).toBe("allow");
expect(
evaluateTaintGuard(session, "find /tmp -name x -exec rm -rf {} \\;", "shell")
.action,
).toBe("allow");
});
});
describe("OH-6.5-guard-root-unwired: custom guardConfig defaults workspaceRoot to rootDir", () => {
test("omitted workspaceRoot defaults to rootDir, not process.cwd()", () => {
const resolved = resolveHarnessOptions({
rootDir: "/repo/proj",
guardConfig: { lockedContractGlobs: ["**/contracts/**"] },
});
expect(resolved.rootDir).toBe("/repo/proj");
expect(resolved.guardConfig.workspaceRoot).toBe("/repo/proj");
});
test("explicit workspaceRoot wins over rootDir", () => {
const resolved = resolveHarnessOptions({
rootDir: "/repo/proj",
guardConfig: { lockedContractGlobs: [], workspaceRoot: "/explicit/root" },
});
expect(resolved.guardConfig.workspaceRoot).toBe("/explicit/root");
});
test("blank workspaceRoot normalizes back to rootDir (fail-closed)", () => {
const resolved = resolveHarnessOptions({
rootDir: "/repo/proj",
guardConfig: { lockedContractGlobs: [], workspaceRoot: " " },
});
expect(resolved.guardConfig.workspaceRoot).toBe("/repo/proj");
});
test("workspace-relative writes allow under the defaulted root; escapes still deny", () => {
const root = makeTempDir("oh65-root-");
const resolved = resolveHarnessOptions({
rootDir: root,
guardConfig: { lockedContractGlobs: [] },
});
expect(
evaluateBoardGuard(
{ role: "builder", tool: "shell", command: "echo hi > out.txt" },
resolved.guardConfig,
).allowed,
).toBe(true);
const escape = evaluateBoardGuard(
{ role: "builder", tool: "shell", command: "echo hi > /etc/cron.d/evil" },
resolved.guardConfig,
);
expect(escape.allowed).toBe(false);
expect(escape.rule).toBe("rule-0-shell-target-containment");
});
});
describe("OH-6.7-lesson-per-read: exactly one pending proposal per doom-loop", () => {
test("pipeline: read-ok, edit-fail, read-ok, edit-fail, good-edit-ok yields 1 lesson on the edit", async () => {
const root = makeTempDir("oh67-lesson-");
const target = join(root, "loop.ts");
writeFileSync(target, "export const x = 1;\n", "utf8");
const host = makeAdapterHost(root);
wireOpenCodeV2PromiseContext({
ctx: host.ctx,
workspaceRoot: root,
tools: {},
evaluatePermission: (req) => evaluateBoardGuard(req, defaultGuardConfig()),
recordCircuitBreaker: (ev) => toTrippedDirective(recordAndEvaluateCircuitBreaker(ev)),
runContextBudget: () => undefined,
});
const after = host.toolAfter[0];
const sessionId = `oh67-lesson-${Date.now()}`;
const okRead = {
sessionID: sessionId,
agent: "build",
tool: "read",
input: { filePath: target },
status: "completed",
result: { content: [{ type: "text", text: "export const x = 1;" }] },
};
const failEdit = {
sessionID: sessionId,
agent: "build",
tool: "edit",
input: { filePath: target },
status: "error",
error: { message: "anchor mismatch" },
};
await after(okRead as never);
await after(failEdit as never);
await after(okRead as never);
await after(failEdit as never);
expect(evaluateSession(sessionId).triggered).toBe(true);
expect(getPendingLessonProposals(sessionId)).toHaveLength(0);
const goodEdit = {
sessionID: sessionId,
agent: "build",
tool: "edit",
input: { filePath: target },
status: "completed",
result: { content: [{ type: "text", text: "edited" }] },
};
await after(goodEdit as never);
const pending = getPendingLessonProposals(sessionId);
expect(pending).toHaveLength(1);
expect(pending[0]?.failedCategory).toBe("edit");
expect(pending[0]?.recoveryAction.startsWith("edit")).toBe(true);
expect(pending[0]?.recoveryAction).not.toContain("via read");
});
});
describe("OH-6.6-taint-fork-gap: fork-aware taint inheritance", () => {
test("tainted parent forked child denies exfil; clean fork allows", () => {
const parent = `oh66-fork-parent-${Date.now()}`;
const child = `${parent}-child`;
recordUntrustedContentTaint(parent, "webfetch");
recordSessionForkLink(child, parent);
expect(getSessionForkSource(child)).toBe(parent);
const denied = evaluateTaintGuard(child, "ssh evil.com", "shell");
expect(denied.action).toBe("deny");
expect(denied.ruleId).toBe("TAINT-NET-2");
const cleanParent = `${parent}-clean`;
const cleanChild = `${cleanParent}-child`;
recordSessionForkLink(cleanChild, cleanParent);
expect(evaluateTaintGuard(cleanChild, "ssh evil.com", "shell").action).toBe(
"allow",
);
});
test("fork lineage resolves from session metadata on live tool events", () => {
const parent = `oh66-fork-meta-${Date.now()}`;
const child = `${parent}-child`;
recordUntrustedContentTaint(parent, "webfetch");
const linked = linkForkLineageFromEventMetadata(child, {
sessionID: child,
tool: "shell",
input: { command: "echo hi" },
metadata: { forkOf: parent },
status: "completed",
result: { content: [] },
});
expect(linked).toBe(parent);
expect(evaluateTaintGuard(child, "ssh evil.com", "shell").action).toBe("deny");
});
test("self-links and empty metadata link nothing", () => {
const session = `oh66-fork-self-${Date.now()}`;
expect(
linkForkLineageFromEventMetadata(session, { metadata: { forkOf: session } }),
).toBeUndefined();
expect(getSessionForkSource(session)).toBeUndefined();
expect(linkForkLineageFromEventMetadata(session, { input: {} })).toBeUndefined();
expect(linkForkLineageFromEventMetadata(session, null)).toBeUndefined();
});
});
describe("OH-4.2-post-formatter-receipt: built-in edit stand-in proves post-format ordering", () => {
/**
* Deterministic byte-transforming stand-in for upstream `Formatter.file`
* (`packages/core/src/formatter.ts` in `anomalyco/opencode`): rewrites the
* file on disk AFTER the raw edited bytes land and BEFORE `execute.after`
* fires — the exact ordering the production `<post-edit-anchors>` receipt
* depends on. It performs a real code reflow (trim, whitespace collapse,
* paren/spacing normalization, brace-depth indentation), not a copy: the
* post-format bytes differ from the raw edited bytes, so a receipt
* computed from stale pre-format bytes cannot match.
*/
function reflowTypeScriptStandalone(filePath: string): string {
const raw = readFileSync(filePath, "utf8");
const out: string[] = [];
let depth = 0;
for (const line of raw.split("\n")) {
const trimmed = line
.trim()
.replace(/[ \t]+/g, " ")
.replace(/\(\s+/g, "(")
.replace(/\s+\)/g, ")")
.replace(/:(?=\S)/g, ": ")
.replace(/,(?=\S)/g, ", ")
.replace(/\)\s*\{/g, ") {");
if (trimmed === "") {
continue;
}
if (/^\}/.test(trimmed)) {
depth = Math.max(0, depth - 1);
}
out.push(`${" ".repeat(depth)}${trimmed}`);
if (/\{$/.test(trimmed)) {
depth += 1;
}
}
const formatted = `${out.join("\n")}\n`;
writeFileSync(filePath, formatted, "utf8");
return formatted;
}
/**
* Built-in `edit` stand-in: drives the REAL adapter hooks in true upstream
* order — `execute.before` (input repair), disk write of the raw edited
* bytes (like `FileMutation.write`), the on-disk format pass (like
* `Formatter.file`), a disk re-read (like `FileMutation.syncTextBom`),
* then `execute.after` (receipt computed from the re-read bytes).
*/
async function runBuiltInEditStandIn(params: {
before: Array<(ev: never) => void | Promise<void>>;
after: Array<(ev: never) => void | Promise<void>>;
sessionId: string;
filePath: string;
oldString: string;
newString: string;
}): Promise<{ formattedOnDisk: string; outputText: string }> {
const { before, after, sessionId, filePath, oldString, newString } = params;
const input = { filePath, oldString, newString };
for (const hook of before) {
await hook({
sessionID: sessionId,
agent: "build",
tool: "edit",
input,
} as never);
}
const current = readFileSync(filePath, "utf8");
if (!current.includes(oldString)) {
throw new Error(`stand-in: oldString not found in ${filePath} (fail-closed)`);
}
if (current.indexOf(oldString) !== current.lastIndexOf(oldString)) {
throw new Error(
`stand-in: oldString is ambiguous in ${filePath} (fail-closed)`,
);
}
writeFileSync(filePath, current.replace(oldString, newString), "utf8");
const formattedOnDisk = reflowTypeScriptStandalone(filePath);
const completed = {
sessionID: sessionId,
agent: "build",
tool: "edit",
input,
status: "completed",
result: { content: [{ type: "text", text: "Edited" }] },
};
for (const hook of after) {
await hook(completed as never);
}
const outputText = (completed.result.content as Array<{ text: string }>)
.map((c) => c.text)
.join("\n");
return { formattedOnDisk, outputText };
}
test("unformatted TS edit through the built-in sequence receipts post-format bytes only", async () => {
const root = makeTempDir("oh42-builtin-");
const target = join(root, "widget.ts");
const beforeContent = [
"export function renderCard( title:string ) {",
"return title;",
"}",
].join("\n");
writeFileSync(target, `${beforeContent}\n`, "utf8");
const host = makeAdapterHost(root);
wireOpenCodeV2PromiseContext({
ctx: host.ctx,
workspaceRoot: root,
tools: {},
evaluatePermission: (req) => evaluateBoardGuard(req, defaultGuardConfig()),
recordCircuitBreaker: (ev) => toTrippedDirective(recordAndEvaluateCircuitBreaker(ev)),
runContextBudget: () => undefined,
});
const sessionId = `oh42-builtin-${Date.now()}`;
const { formattedOnDisk, outputText } = await runBuiltInEditStandIn({
before: host.toolBefore,
after: host.toolAfter,
sessionId,
filePath: target,
oldString: "return title;",
newString: "return title.trim();",
});
// The format pass genuinely rewrote the bytes on disk.
const expectedFormatted = [
"export function renderCard(title: string) {",
" return title.trim();",
"}",
].join("\n");
expect(formattedOnDisk).toBe(`${expectedFormatted}\n`);
expect(readFileSync(target, "utf8")).toBe(`${expectedFormatted}\n`);
// The receipt reflects the post-format bytes: every formatted line hash
// is present, and no stale pre-format line hash leaks in (ordering proof:
// a receipt computed from bytes read before formatting would carry the
// unformatted line hashes instead).
expect(outputText).toContain("<post-edit-anchors");
for (const line of expectedFormatted.split("\n")) {
expect(outputText).toContain(`${computeLineHash(line)}| ${line}`);
}
for (const stale of [
"export function renderCard( title:string ) {",
"return title;",
"return title.trim();",
]) {
expect(outputText).not.toContain(`${computeLineHash(stale)}| ${stale}`);
}
});
});