Skip to content

fix: forward security pins and fixes from main to next (HACK-1758) - #545

Merged
roodboi merged 24 commits into
nextfrom
fix/hack-1758-forward-security-pins
Nov 3, 2026
Merged

roodboi merged 24 commits into
nextfrom
fix/hack-1758-forward-security-pins

Conversation

@roodboi

@roodboi roodboi commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Update at 7bd4fa0: merged next with #538 (conflicts in doctor.ts, project-env-config.ts, secret-store.ts and #538's shell-value test). The keychain-disabled refusal is now a SecretStoreKeyUnavailableError, which #538's check depends on. One behaviour change: with the keychain disabled, no key and no store file, a run counts as first use and creates a key file, as on main (#423). On next before this merge it stopped with a refusal, so I removed the one step of #538's test that asserted that stop. The #538 test also restores a preloaded HACK_SECRETS_DISABLE_KEYCHAIN_FALLBACK instead of deleting it. Focused env, secret-store and doctor tests pass on the pinned Bun 1.4.4. Details on HACK-1758.

Update at ede96a8: merged next with #543 (conflicts in secrets.ts and secret-store.ts); the shared keychain now gates first and then uses #543's deadline-bounded calls, with a new test for that order.

Update at 03a0c75: merged next with #436 (one conflict, in doctor's env materialization check, kept both sides); focused env, doctor and MCP tests show no new failures on Bun 1.4.4.

Update at b2ca3cd: review findings at 30f2b1b

This note replaces the older text below where they disagree. next had not moved, so there was nothing new to merge.

  • Relative overrides in MCP sessions: a session now refuses only a relative HACK_HOME. A relative HACK_GLOBAL_CONFIG_PATH resolves against the captured cwd again, which matches child commands because they run in that cwd. The previous note's refusal of that setting was a mistake and is reverted: it made the server refuse what the CLI and its children accept.
  • Guard seam: the fixture case now says it uses the seam. The seam answers only the next read, so it can't stay set for later files, and a new nested test checks that a later file gets the real reader back. The outer test now says it shows detection and reporting, not that the repair (disableNativeProxies) works.

Verified here (private HOME and HACK_HOME, HACK_DAEMON_AUTOSTART_DISABLE=1): bun run typecheck, bun run check, bun run docs:check, gitleaks, and focused tests: 264 pass, 61 skip, 0 fail across 31 files, all *mcp* files included.

Mutation Result
Refuse a relative HACK_GLOBAL_CONFIG_PATH again 2 tests fail
Resolve a relative HACK_GLOBAL_CONFIG_PATH against the process cwd 2 tests fail
Resolve a relative HACK_HOME against the session cwd 3 tests fail
Seam stays set The guard test fails

Not rerun: the whole bun run test suite on this head; hosted CI is the check.


Update at 30f2b1b: review findings at 676bf9b

This note replaces the older text below where they disagree. Merged current next (f94b17ff, including #547) with a merge, not a rebase.

  • ffc49b33 Relative HACK_HOME: captureMcpSessionContext now refuses a relative HACK_HOME or HACK_GLOBAL_CONFIG_PATH with the CLI's error wording. The earlier "MCP sessions still resolve it" note no longer applies. A socket client with such a context loses only its own session. The CLI itself still doesn't check a relative HACK_GLOBAL_CONFIG_PATH; only MCP sessions refuse it.
  • 71975bb6: dropped logs from the commands that record branch usage, in docs/cli.md and src/lib/branch-usage.ts.
  • 30f2b1bf Guard positive case: this is a seam, not a real leak. Under Bun 1.4.4, these routes left no native change that process.env hides: set and delete, loadEnvFile, a Worker, libc setenv, and a replaced env object. So the fixture hands the guard's native reader a changed environment. It tests the boundary check and its report, not a real Bun leak.
  • 30f2b1bf SDK comment: reworded in tests/mcp-tool-result-size.test.ts for the pinned SDK 1.32.
  • RELEASE_PAT: stays on actions/checkout in release.yml and release-prepare.yml (676bf9b7). The earlier note about useblacksmith/checkout receiving it and the 9-step count no longer apply.

Verified here (private HOME and HACK_HOME, HACK_DAEMON_AUTOSTART_DISABLE=1):

  • bun run typecheck, bun run check, bun run docs:check and gitleaks over origin/next..HEAD pass.
  • Focused tests: 325 pass, 61 skip, 0 fail across 35 files, all *mcp* files included. tests/mcp-tool-result-size.test.ts passes (13).
  • A real hack mcp serve start with either relative override exits 1 with the error.
  • Mutation, session rule restored to cwd-relative: 4 tests fail. Mutation, guard's native comparison disabled: the guard test fails.

Read, not run: checkByteCutoff in tests/e2e/scenarios/mcp-logs-compose.ts should still hold. Events are parsed from the capture, so they total about 8 MiB and fit under the 9.5 MiB result limit. No event is dropped, so the reported cutoff stays {bytes, 8 MiB}, and #547 only trims stdout on a code point boundary. Docker wasn't used; hosted CI runs this scenario.

Not rerun: the whole bun run test suite on this head; hosted CI is the check.


Current head: 676bf9b7. It merges next at ddc73a0d and keeps RELEASE_PAT on actions/checkout@v4 in release.yml and release-prepare.yml. main passes that credential to useblacksmith/checkout@v1, a third-party action on a floating tag. Doing the same on next is a security decision of its own, so it isn't part of this port. All other checkout steps ported here stay on Blacksmith. This changes the "RELEASE_PAT now goes through useblacksmith/checkout" point below.


Refs HACK-1758. Forwards the security pins and fixes from main to next, one topic per commit. main is not merged. Each commit was checked by reading next and, where main has a regression suite, by running that suite against unmodified next.

Classification

main commit What it does Result Evidence on this branch
69ed70e9 #457 Bun 1.4.4 and Node 24.22.1 pins Ported, with the exceptions below 2c84aac3: all workflows including prerelease.yml, .tool-versions, mise.toml, packageManager, Dockerfiles, candidate build guards, e2e and pattern fixture tags
b5d3212a HACK-1404 Delete unset env vars instead of assigning undefined Already present tests/helpers/env.ts:12 (restoreEnv, used by 109 test files). No process.env.X = undefined remains, and every restore of a value that may be undefined is guarded. Bun 1.4.4 did change proxy deletes, handled in 2852393c
395351bc #454 Dependabot bun ecosystem Ported f97ca462: .github/dependabot.yml, same as main. GitHub reads this file from the default branch, so it has no effect until next lands
739c4e57 #456 next, MCP SDK, drizzle-orm bumps Ported dd92fae0: MCP SDK 1.26.0 to 1.32.0, drizzle-orm ^0.44.5 to 0.45.2, next 16.2.6 to 16.4.1 in examples/next-app; site/ already has next 16.4.3
75225e00 #513 zod 4.6.7 (GHSA-mrhx-c7xc-m3w5) Ported dd92fae0: zod 4.3.5 to 4.6.7; site/ already has it
4c194a12 #423 Hermetic keychain tests Ported 80abd5fc: src/lib/keychain.ts, tests/preload.ts, tests/keychain-guard.test.ts. The fake is not frozen, because next's tests spy on it
3312658f #526 LogQL escaping and empty Loki follow hint Already present src/lib/logs.ts:62, :91 and src/ui/loki-logs.ts:442, from #417 and #478. main's tests/loki-tail.test.ts passes unchanged on unmodified next
98ab4e70 #527 No global state under the cwd for an empty or relative HOME/HACK_HOME Ported and extended ac12697c: src/lib/home-dir.ts, plus the same bug in next-only code: MCP session capture, launchd access, native AWS environment, setup migrate
65e4d97b #528 Fail and sweep on leaked e2e Docker networks Ported 8a79897a, 32cd2edf: tests/e2e/docker-ownership.ts, harness and run.ts
736544ef #529 Inherit primary local env overlays in linked worktrees Partly present; the gaps are ported next already inherited (resolvePrimaryLocalProjectDir). 24 of the 36 cases in main's test failed on unmodified next. 40d0df6f, e4837dca add worktree-family verification, the env opt-out, fail-closed reads, YAML routing and doctor's obsolete report
83d71ac7 #530 Keep branch usage out of the tracked tree Ported eb30178f, d925c5ef: src/lib/branch-usage.ts; touchBranchUsage is gone
541c611b #520 useblacksmith/checkout on Blacksmith Ubuntu runners Ported f97ca462: 9 steps across ci, release, release-prepare, release-node-runtime-image. Hosted and macOS jobs keep actions/checkout, as on main
1c1cc983 #521 Resize three runners Left out: needs a decision The numbers come from main's single docker-e2e job, and next shards that job. The original PR itself marks the sizes as unbenchmarked, and it adds cost
08e0412a, 2e8dc61d, 91b14bf0, 60f3c0b9, 010880c7, 5e03d47e 4.2.x releases Not applicable Excluded by the issue; next has its own release line
2dd3853b #512 Remove private names from a sample digest Ported, not on the issue's list 25dfed3e: next is public and still carried these names. Drop this commit if you want it handled separately

Pins that stay on the old version

  • Native file and storage helper, oven/bun:1.4.2-slim (src/lib/native-compose-vm-file-protocol.ts:6, src/lib/native-compose-storage-witness-docker-artifact.ts:19, :66). The catalog pins exact image, Bun and libc bytes per platform, and scripts/generate-native-storage-witness-helper.ts says changed artifacts need separate runtime qualification. Moving it is a product decision. Evidence: the helper bundle reproduces byte for byte under Bun 1.4.4, and its source has no Brotli, zlib or fetch use. Bun 1.4.2 is still in that image. CI now pulls both images, and docs/reference/native-config-files.md still names 1.4.2-slim correctly.
  • Frozen benchmark workload: scripts/application-benchmark-protocol.json and scripts/check-application-benchmark-protocol.py freeze this pin as part of the protocol, and scripts/benchmark-native-compose.py defaults to the same image. No Bun from these ships.
  • Records of past runs: the observed client version in src/agents/client-capabilities.ts:501, the docs/versions/ snapshots, scripts/test-shard-durations.json, and comments describing 1.4.2 behaviour. The capability record still says MCP SDK 1.26.0 and needs a new observation.
  • Test fixtures that use 1.4.2 only as sample data.

Behaviour to review

  • A relative HACK_HOME is now an error in the CLI. MCP sessions still resolve it against the captured checkout, because tests/mcp-session-context.test.ts pins that as next's contract.
  • HACK_WORKTREE_INHERIT_LOCAL=0 applies to the Compose path. The graph runner follows only worktree.inherit_local, because its adoption proofs record that setting. docs/cli.md says so.
  • A .git link that cannot be verified, such as a bare-repo worktree, now reads no primary overlays instead of failing the command.
  • Two expectations in main's project-env-worktree-local test keep next's behaviour: the layer guard's refusal for a symlink leaving the project, and the tombstone from env unset --local, which hides an inherited value.
  • tests/global-state-guard.test.ts changed because Bun 1.4.4 now carries a process.env delete into its native environment. That test was written to fail on this upgrade. It now asserts the new behaviour and fails if the old one returns. The guard's native check has no reachable leak under 1.4.4 and stays as a regression check.
  • useblacksmith/checkout now receives RELEASE_PAT in release-prepare.yml and release.yml, as on main. No permissions or secrets changed.
  • The private names removed by 25dfed3e remain in git history. This PR does not rewrite history.

Verified

Local, on macOS arm64 with Bun 1.4.4 from mise. All tests ran with a private temporary HOME and HACK_HOME, HACK_DAEMON_AUTOSTART_DISABLE=1, and a DOCKER_HOST pointing at a socket that does not exist.

  • bun install --frozen-lockfile: pass.
  • bun run typecheck, bun run check, bun run docs:check: pass on the head.
  • gitleaks 8.30.1 over origin/next..HEAD: no leaks.
  • .hack/tasks.json: the method first reproduced the old mise.toml pin and all three package.json script pins. Only mise.toml needed re-pinning, and tests/project-task-catalog.test.ts passes. tests/integration-ownership.test.ts passes with no fingerprint change.
  • Focused tests on the head: 446 pass and 5 fail across 24 files. All 5 are in tests/doctor-fix-noninteractive.test.ts and are described below.

bun run test ran once, before the last three fix commits and before rebasing onto #542: 8505 pass, 127 skip, 27 fail across 598 files.

Failures Cause Also on unmodified next? Now
20 in native-compose-ingress, native-compose-proxy-probe My private DOCKER_HOST Yes, on Bun 1.4.2 and 1.4.4 All 25 tests in those files pass without that variable
3 in global-state-guard Bun 1.4.4 proxy delete change No: they fail on next with 1.4.4, pass with 1.4.2 Fixed in 2852393c, passing
1 each in project-env-local-inheritance, cli-spawn-env-isolation, e2e-docker-ownership This PR's ports No Fixed in e4837dca, 32cd2edf, passing
1 in native-compose-route-claims (Unexpected end of JSON input) Not established. The host was under heavy load and this PR does not touch that code Not reproduced there Passed 5 of 5 focused reruns on this branch

The 5 doctor-fix-noninteractive failures appear only when that file runs alone under plain bun test with this isolation. Unmodified next fails the same 5 the same way, and they passed in the full run.

Not verified

  • Hosted CI, including the Linux jobs and the sharded docker-e2e. No Docker scenario ran locally, so the 1.4.4 and 24.22.1 image tags, docker-ownership against a real engine, and worktree-parallel-up are unproven until CI runs.
  • Docker image builds, the release workflows and Blacksmith checkout behaviour.
  • Whether CVE-2026-111497 is reachable in Hack's use.
  • The whole suite on the final head; CI is the check for that.
  • The issue's other done criteria: a green merged next, a published prerelease, and the published build reporting the new pin.

🤖 Generated with Claude Code


Run Proof on this PR
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

roodboi and others added 13 commits November 2, 2026 14:40
Forward main's 69ed70e to next: every workflow including prerelease.yml
and bun-crash-experiment.yml, .tool-versions, mise.toml, packageManager,
the Dockerfiles, the candidate build guards, and the e2e and pattern
fixture image tags. The pattern fixtures take the 1.4.4-alpine index
digest from the registry; the same lookup reproduces the old 1.4.2 digest.

mise.toml is a reviewed input of .hack/tasks.json, so its pin is updated.
The raw-file method reproduced the unchanged pin first.

The native file and storage helper stays on oven/bun:1.4.2-slim: its
catalog pins image, Bun and libc bytes that need separate runtime
qualification. Helper bytes still reproduce under Bun 1.4.4 and the helper
entry reaches no Brotli, zlib or fetch path. CI pulls both images. The
frozen benchmark workload pin also stays.

b5d3212 needs no port: tests/helpers/env.ts already deletes unset vars.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(HACK-1758)

Forward main's 739c4e5 and 75225e0 where next was below main:
- zod 4.3.5 to 4.6.7 (GHSA-mrhx-c7xc-m3w5)
- @modelcontextprotocol/sdk 1.26.0 to 1.32.0
- drizzle-orm ^0.44.5 to 0.45.2 in packages/db
- next 16.2.6 to 16.4.1 in examples/next-app

site/ already pins next 16.4.3 and zod 4.6.7. Both lockfiles are
regenerated. examples/next-app/bun.lock had still resolved next 16.1.1.
drizzle-orm 0.45 no longer auto-installs its prisma peer, so the prisma
tree leaves the root lockfile, as it already has on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forward main's 4c194a1. Native keychain access goes through one gated
boundary (src/lib/keychain.ts), so HACK_SECRETS_DISABLE_KEYCHAIN_FALLBACK
stops every read, write and delete, and tests/preload.ts replaces
Bun.secrets with an in-memory backend for the whole `bun test` process.

Adapted for next: the preload runs before the state guards, so their
baseline holds the flag. The fake is not frozen because next's tests spy on
it. Tests that use a stubbed keychain open the gate explicitly and restore
it with restoreEnv.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…OME/HACK_HOME (HACK-1758)

Forward main's 98ab4e7 (HACK-1357). An empty or relative HOME, or a
relative HACK_HOME, made Hack resolve the registry, daemon socket and
secret key paths under <cwd>/.hack. src/lib/home-dir.ts now normalizes the
home, refuses a relative HACK_HOME and throws when no absolute home exists.

Beyond main's call sites, next has the same bug in code that main lacks:
MCP session capture, launchd access, the native AWS environment and setup
migrate. Each now uses the helper, with regression tests for the MCP and
launchd paths.

MCP sessions keep resolving a relative HACK_HOME against the captured
checkout: tests/mcp-session-context.test.ts pins that as next's contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forward main's 83d71ac (HACK-1351). Runtime commands in a linked worktree
created .hack/hack.branches.json or rewrote its last_used_at on every up,
down, restart, run, exec and logs, which dirtied git status. Usage now lives
in machine-local .hack/.internal/branch-usage.json. The tracked registry is
written only by `hack branch add` and `remove`, and legacy last_used_at
values are read as a fallback and left byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…HACK-1758)

Forward main's 65e4d97 (HACK-1346). Fixture names are recorded as owned
in memory and in a per-PID journal. A Docker scenario that leaves an owned
network now fails, the harness sweeps by exact ID and never prunes, run.ts
sweeps on SIGINT, SIGTERM and SIGHUP and reclaims journals from dead runs,
and downBestEffort uses the real `<name>--<branch>` project name.

Adapted for next: run.ts keeps its launchd canary and import.meta.main
guard, and the new test restores its env var for next's state guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Next already inherited primary local overlays in linked worktrees, but
main's 736544e regression suite found these gaps, 24 of its 36 cases
failing before this change:

- verify the Git worktree family before reading a primary checkout, so a
  redirected .git file cannot pull in another checkout's local overlays
- add the HACK_WORKTREE_INHERIT_LOCAL=0 opt-out, checked before any Git work
- only unreadable or malformed project config blocks inheritance, not a
  field error elsewhere in a well-formed file
- reject non-regular inherited files, and fail closed on unreadable or
  unparsable ones with a path-only error
- route inherited-only worktrees to the YAML env commands
- have doctor report a materialized .env whose inputs are all gone as obsolete

Two of main's test expectations keep next's behaviour: the layer guard's
refusal message for a symlinked default file, and the tombstone a local
unset leaves so it can hide an inherited value. The graph runner keeps
following only worktree.inherit_local, since its adoption proofs record that
setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…CK-1758)

Forward main's 395351b and 541c611. Jobs on Blacksmith Ubuntu runners use
useblacksmith/checkout@v1 with the same inputs; jobs on hosted and macOS
runners keep actions/checkout, as on main. No permissions or secrets change.

Main's 1c1cc98 runner sizing is left out: its measurements come from
main's single docker-e2e job, and next shards that job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ACK-1758)

Forward main's 2dd3853. The sample digest listed entity names and a contact
address from a private knowledge base. This commit is outside the HACK-1758
list; it is carried because next is public and still had the names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runtime commands no longer write hack.branches.json, so the note in this
test no longer describes what `up` does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bun 1.4.2 did not carry a process.env delete into its native environment, so
a deleted proxy kept routing later fetch calls. Bun 1.4.4 does. The sentinel
test that was written to fail on this upgrade now asserts the new behaviour,
and fails if the old one returns. restoreEnv keeps its empty assignment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…CK-1758)

An inherited local overlay loop must keep next's closed, value-free
refusal, so the layer guard runs before validatePrimaryLocalFile. The ported
test now spawns the CLI with privateCliEnv, as next's isolation scan requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Next's test files share one process, so a path saved by an earlier file
outlived a later HACK_E2E_OWNERSHIP_DIR and the journal went elsewhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Nov 2, 2026

Copy link
Copy Markdown

HACK-1758

roodboi and others added 2 commits November 2, 2026 14:46
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Handing the release credential to a third-party action on a floating tag is a
security decision of its own, so it isn't part of this port. Other checkout
steps stay on Blacksmith.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@blacksmith-sh

This comment has been minimized.

roodboi added a commit that referenced this pull request Nov 2, 2026
Fixes the cause of HACK-1777, which blocks merging #545.

## Problem

MCP SDK 1.32 closes a stdio client's transport when its read buffer
passes 10 MiB. Hack's 8 MiB capture budget bounds raw child output only.
The result repeats that output as `content` text and as
`structuredContent`, log results add `data.events`, and JSON escaping
grows each copy. A large result could end the client's whole session.

## Change

`boundMcpToolResult` (`src/mcp/result-budget.ts`) runs at the single
exit of the `tools/call` dispatcher, so it covers every tool. It limits
the serialized result to `MCP_TOOL_RESULT_LIMIT_BYTES` (9.5 MiB of
UTF-8), which leaves 512 KiB under 10 MiB for the JSON-RPC envelope and
the start of a following message in the client's last read. A result
within the limit is returned unchanged.

Over the limit, data goes in this order:

1. **`content` text.** It repeats `structuredContent` with extra
escaping, so it buys the most and loses the least. It becomes a view of
at most 256 KiB, in the same JSON shape, with its own `outputTruncated`
and cutoff. It is not removed outright, because some clients read only
this channel.
2. **Log tools: `stdout`, then whole events from the end.** `stdout`
repeats the events. **Other tools: `data`, then the end of `stdout`.**
Omitting `data` matches what a capture cut already does.
3. **The end of `stderr`.** Its first 16 KiB are protected from step 2.

What a caller sees:

- A cut never splits a code point. Sizes are measured with the real
serializer, in bytes.
- `structuredContent` that was cut has `outputTruncated: true` and a
fixed note ending `stderr`.
- `structuredContent` that fits after step 1 is complete and has no
flag. Only the text view says it was cut.
- When events are removed from a successful collection, `data.cutoff` is
`{bound: "bytes", limit: 9961472}` and `data.count` is the number kept.
`data.stop_reason` still says why collection stopped.
- A backend failure stays a failure with a null cutoff.
- `ok` and `exitCode` still describe the command. A command that exited
0 stays `ok` when its result is cut.

Docs updated: `docs/reference/mcp-tools.md`,
`docs/reference/mcp-logs.md`, `docs/guides/agent-first-setup.md`,
`tests/e2e/README.md`, and the comments in `src/mcp/logs-contract.ts`.
No agent guidance changed, so nothing was regenerated.

### Please check these choices

- **Text-only clients get less.** A client that reads only `content` now
sees 256 KiB of a large result, not all of it. `structuredContent` keeps
the most because the 8 MiB capture must still fit, so the text cannot
share the budget equally. Clients on `structuredContent` are unaffected.
- **Two channels can differ.** On a cut result the text view has fewer
events than `structuredContent`, and its cutoff limit is `262144`. Each
channel is truthful about itself, but they are no longer the same JSON.
- **The note can appear in `stderr` of an env tool** that normally has
an empty one on success. The note is fixed text and carries no values.

## Verified

Local runs on Bun 1.4.2 with a private temporary HOME and HACK_HOME and
`HACK_DAEMON_AUTOSTART_DISABLE=1`, at this head.

**New test, `tests/mcp-tool-result-size.test.ts`:** 13 pass. It drives
the real server over stdio with a real SDK client on default limits,
using a stub `HACK_MCP_COMMAND`. A `tee` records the bytes the client
reads, so the size assertion holds on any SDK version. Coverage:

- log snapshot and log tail above 8 MiB, with emoji and characters JSON
escaping expands
- a complete log stream that goes over the message limit
- a non-log tool above the capture limit, and one under it that exits 0
but expands when escaped
- a text-only cut, and a small result that must stay unchanged
- the session staying usable after each result, with the SDK validating
against `outputSchema`
- unit cases: budget edges around surrogate pairs, an oversized first
event, backend failure, the stderr floor, a result with no typed
payload, and an unexpected oversized field

**Client-only control**, with the server from unfixed `origin/next` on
SDK 1.26 and only the client changing:

| Client SDK | Server | Result |
| --- | --- | --- |
| 1.26 | unfixed | OK, 33.7 MB result |
| 1.32 | unfixed | `MCP error -32000: Connection closed` |
| 1.32 | fixed | OK, 9,961,406 bytes, 1012 events, cutoff `bytes` /
8388608 |

**SDK 1.32 on client and server:** the #545 tree in a scratch copy, with
this fix applied, passes 13 of 13.

**Mutations:**

| Mutation | SDK | Result |
| --- | --- | --- |
| Remove the bound from the dispatcher | 1.26 | 6 fail; the size
assertion sees lines up to about 68 MB |
| Remove the bound from the dispatcher | 1.32 | 6 fail; `Connection
closed` in the output |
| Measure string length instead of bytes | 1.26 | 6 fail |

**`checkByteCutoff` in `tests/e2e/scenarios/mcp-logs-compose.ts`:**
unchanged, including its 8 MiB limit assertion. Reparsed events can't
outgrow the 8 MiB capture unless the source JSON uses unusual number
spellings, so canonical events fit the budget and the capture limit
stays the one reported. The new test makes the same checks with a stub:
a complete ordered prefix of whole events, and no split code point.

**Gates:**

- `bun run --cwd packages/cli typecheck` passes.
- Biome passes on changed code files.
- Focused MCP tests pass: 73 across `mcp-tool-result-size`,
`mcp-lifetime`, `mcp-logs-contract`, `mcp-inspect-tools`, `mcp` and
`mcp-session-context`.
- `bun run docs:check` passes.
- gitleaks finds no leaks over `origin/next..HEAD`.

## Not verified

- **`mcp-logs-compose` itself was not run locally,** because it needs
Docker. Its logic was read and matched with a stub, not executed. The
ticket's done condition requires it to pass on SDK 1.32 defaults, so it
needs a `docker-e2e` run on the merged result with #545. It can't prove
that on this branch alone, because this branch has SDK 1.26.
- **Full suite and hosted CI:** the full suite was not run locally. See
this PR's checks for CI.
- **Real clients** other than the SDK (Claude Code, Codex and others)
were not measured.
- **Concurrent large results:** the 512 KiB margin is reasoned from how
`ReadBuffer` drains complete lines, not tested with parallel calls.
- **Published prerelease:** the ticket also requires this on `next` and
in a prerelease, which happens after merge.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Dimitri Kennedy <344842+roodboi@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
roodboi and others added 9 commits November 2, 2026 16:06
…-security-pins

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…K-1758)

The server resolved these against the session cwd, but it passes the raw env
to child commands, and they now refuse a relative HACK_HOME. Tool calls then
failed after the server had read a different registry. A session now refuses
both overrides with the CLI's error. A socket client with such a context
loses only its own session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-1758)

recordBranchUsage is called for up, down, restart, run, exec and branch add;
nothing calls it for logs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…HACK-1758)

Under Bun 1.4.4 no process.env route found leaves a hidden native change:
set and delete, loadEnvFile, a Worker, libc setenv, a replaced env object. The
fixture therefore presents a changed native environment through a small seam
in the guard's reader. This tests the boundary check and its report, not a
real Bun leak.

Also reword tests/mcp-tool-result-size.test.ts: the pinned MCP SDK 1.32 has the
10 MiB read limit that its comments treated as optional.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Child commands run in the session cwd and accept a relative
HACK_GLOBAL_CONFIG_PATH, so refusing it stopped the server in a setup where
the CLI works. The session resolves it against the captured cwd again, which
matches them. It still refuses a relative HACK_HOME, as children do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The seam stayed set for later files, which left the native check blind there.
It now answers the guard's next read only, and a nested test checks that a later
file gets the real reader back. The fixture name and the outer test now say
that this covers detection and reporting, not the repair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-security-pins

Brings in #436. Conflict in the doctor env materialization check: keep its
refusal for an unusable layer entry, and keep inspecting a legacy-format
project that has recorded materialization state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-security-pins

Brings in #543 and 9057298. secrets.ts and secret-store.ts conflicted because
each side wrapped the native keychain. The shared keychain now gates first and
then calls #543's deadline-bounded nativeKeychain, so every user gets both, and
keychainKeyAccessError stays as #543 wrote it. A new test covers the order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps this branch's keychain-disabled check after key bootstrap, behind the shared
guard. That refusal now throws SecretStoreKeyUnavailableError from PR 538, which the
shell-value check depends on.

With the check after bootstrap, a run with no key and no store is first use and
creates a key file, keychain or not. PR 538's recovery test asserted that step
stopped, which held only through the old early refusal, so that step is removed. Its
afterEach now restores the preloaded keychain setting for the state guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi
roodboi merged commit 7a6f7dd into next Nov 3, 2026
23 checks passed
@roodboi
roodboi deleted the fix/hack-1758-forward-security-pins branch November 3, 2026 04:47
roodboi added a commit that referenced this pull request Nov 3, 2026
Keeps both import additions in the e2e fixture: this PR's fixture git
environment and next's gitignore markers and Docker ownership record.

Three git spawns that arrived on next (#545 and the HACK-1709/1711
follow-ups) built their own git environment, which this PR's scan refuses.
They now use fixtureGitEnv. The legacy-scope test keeps its private HOME;
its commits now carry the shared fixture identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi added a commit that referenced this pull request Nov 3, 2026
… deadline (#499)

**Update at 3e85b9b (second `next` merge, at c019124):** this brings
in #557, #564, #562 and #563. The only conflict was at the end of
`tests/native-compose-adoption-routing-generation.test.ts`. This PR adds
live caller budget cases there, and #563 adds the HACK-1359 mutation
deadline refusal; both are kept. Typecheck and Biome pass on Bun 1.4.4.
The five adoption generation test files and the compose-env-at-rest
tests pass (235/0). The file-owner tests need a built config compiler;
once it was built with Rust 1.97.1, they passed on their own (29/0).

**Update at 04abddb:** merged `next` at 1df527b (#545, #419, #337).
The one conflict was in `tests/native-authored-project-start.test.ts`:
#419 renamed the stand-in owner cap to `STAND_IN_CAP_MS`, sized from the
case ceiling. Both sides are kept, and the failed-case test's Bun
timeout now uses the named fixture margin, which is the same 5000 ms.
Typecheck and Biome pass on Bun 1.4.4.

- `tests/native-compose-adoption-generation.test.ts` run on its own
passes: 123 pass, 0 fail.
- In one earlier 4-file run at a host load average of about 7–9, two of
its cases failed. Both pass when rerun on their own. One of them refused
with `E_CONFIG_INVALID` for a cancellation, tracked as HACK-1810 and
noted on HACK-1458.
- HACK-1458's 10 loaded runs have not been done.

> **Current state (bd55b86, merges `next` at e5d907d).** This head
adds the local Oct 27 to 31 recovery work that was never pushed: live
adoption transaction deadlines, retained routing fixture budgets,
retiring proven pre-ready host processes after cancellation, a faster
startup compiler stand-in, and a per-case failure cleanup for stand-in
host groups.
>
> Checked at this head: `tsc` passes, and 233 tests pass with 0 failures
across the adoption generation and routing files, authored project
start, and the fixture helper tests. These ran on a host at load 8 to
10, once, with no added load generator.
>
> Not done: the ticket's 10 consecutive loaded runs of the affected
files have not been run at this head, and the failed Oct 26 verification
has not been re-run. An independent read-only review by `claude-hd` (the
Codex route is limited until Nov 4) read the code at c520acc and did
not run it. It recommended merging, with four low findings. The mux
cleanup finding is fixed at bd55b86, proven by two mutations, and the
delta after that review has not been reviewed. The others are in
HACK-1801. It raised two decisions for the owner: pre-Ready teardown
stops host processes without running the authored down hooks, and
teardown runs the compiler again before checking for Removed, which can
slow cancellation. The Evidence and Gaps sections below describe earlier
heads.

Refs HACK-1458 (unit G106). Product fix for the four product clocks that
kept HACK-1367's loaded runs from passing. Test-side budgets stay with
#419 (G52/G105); this PR adds new cases at the end of the two shared
test files and edits none of #419's hunks.

## Root cause

1. **Public adoption dispatch.** `tryLegacyComposeAdoptedCommand` opened
its adoption store without a deadline, so every store inspection phase
(one probe owner) got the default 15 s aggregate clock, whatever
`HACK_COMPOSE_STARTUP_TIMEOUT_MS` allowed. A slow engine expired it as
`E_NATIVE_COMPOSE_PROBE_TIMEOUT`; `translate()` turned that into
`E_LEGACY_ADOPTION_STATE`, and the command reported `E_CONFIG_INVALID`
("state is invalid, unsafe or changed"). The command's own deadline only
started after `loadActive`, so inspection before it was never bound by
the caller.
2. **Authored compiler calls.** `acquireNativeExecutionInputs` never
passed a compiler timeout, so each compiler invocation (and the later
`assertFresh`/value checks) kept the transport's fixed 10 s default,
even with minutes of startup budget left.
3. **Source5 cancellation after readiness hang.** Reproduced: after
`onReady` aborts, the cancel's `down.before` re-acquires inputs with the
hard signal. That acquisition also kept the fixed 10 s compiler clock.
When one compiler call took longer, `down.before` failed at `acquire`
(no hook ran), `onStopFailure` fired, and the start retained the live
owner and host processes, waiting for a hard cancel that the case never
sends. With a scratch copy whose compiler sleeps 11 s after readiness,
the case hung for its whole 420 s budget, with hook events
`down.before:acquire` then `down.before:stop-operation` and no down
hook, the same symptoms as the loaded report.
4. **Failed authored cases leave host process groups running.** This is
a product path, two of them:
- (i) Startup inputs check freshness under the startup signal. Once a
cancel or the startup deadline (its timer aborts the same signal) ends
startup after the host processes started and before reservation,
`stopUnstartedHostProcesses` could never pass `hostProcesses.stop()`'s
freshness check (it re-reads inputs with the aborted signal), so the
groups stayed up. A loaded local run of a new case showed it directly:
startup expired after `host-process-ready.json` was written, the start
failed, and the mux window group was still alive afterwards.
- (ii) `createNativeAuthoredHostProcesses().start()` had no failure
path. A failure after the launch intent (cancel, deadline or refused
check during the launch) left the mux session running behind an intent
that recovery never converts into authority.

Failures after reservation still retain host processes on purpose, with
complete owner/ready/state records that `hack down` recovery stops
(`retireNativeAuthoredHostProcesses`). Ending those in a test is test
cleanup (#419).

## Fix

- **Adoption** (`native-compose-adoption-generation.ts`,
`native-compose-adoption-command.ts`):
- The store takes an optional caller `deadline`. With one, each
inspection phase gets `legacyAdoptionProbePhaseMs`: the remaining time,
capped at `LEGACY_ADOPTION_PROBE_PHASE_MAX_MS` (60 s). An expired
deadline refuses as `E_LEGACY_ADOPTION_DEADLINE`, and an unclassified
failure after a probe timeout or past the deadline becomes
`E_LEGACY_ADOPTION_DEADLINE`, not `E_LEGACY_ADOPTION_STATE`. A store
without a deadline is unchanged.
- The command starts one deadline (`HACK_COMPOSE_STARTUP_TIMEOUT_MS`)
before it opens the store and uses it for inspection, the retained
operation, the plain container runner and `ps` inspection.
`E_LEGACY_ADOPTION_DEADLINE` maps to `E_STARTUP_TIMEOUT`. The message
stays the store's fixed text, so the closed `legacy-deadline` diagnostic
token is unchanged.
- **Compiler** (`native-config-compiler.ts`,
`native-project-validation.ts`, `native-execution-inputs.ts`): named
`NATIVE_CONFIG_COMPILER_MIN_TIMEOUT_MS` (1) and
`NATIVE_CONFIG_COMPILER_MAX_TIMEOUT_MS` (60 000),
`nativeConfigCompilerTimeoutWithin(remainingMs)`, and an optional
`compilerTimeoutMs` getter that every compiler invocation of a selection
reads, including later freshness checks.
- **Authored start** (`native-authored-project-start.ts`):
- Startup compiler calls use the remaining startup deadline (floor 1 ms,
ceiling 60 s). Calls outside startup (down-hook acquisition, checks
after ready) have no startup deadline and take the 60 s ceiling, so a
slow host no longer fails a graceful stop.
- Before stopping unstarted host processes, teardown re-acquires inputs
under the hard signal and refuses a changed selection, like the down
hooks do.
- **Host processes** (`native-authored-host-processes.ts`): a start that
fails after its launch intent calls the controller's `abort()`, which
ends the session through the ownership token it created. Owner
bookkeeping is cleared only when every launched group was recorded and
is proven absent within 5 s. Otherwise it stays retained.
- Docs: `docs/reference/native-compose-adoption.md`,
`docs/reference/native-authored-commands.md`.

## Tests

New cases, each proven by mutation (product change reverted in a scratch
copy):

| Case | File | Clock | Mutation result |
| --- | --- | --- | --- |
| `HACK-1458: public dispatch inspection outlives the former fixed 15 s
phase clock while the caller deadline remains` | adoption generation |
injected `Date.now` jump of 16 s after the first engine probe, 120 s
deadline | base: `E_CONFIG_INVALID` "state is invalid, unsafe or
changed" |
| `HACK-1458: an expired public dispatch deadline ends inspection as a
startup timeout, not invalid config` | adoption generation | injected 6
s jump, 5 s deadline | base: returns `0` (inspection ignored the expired
deadline and started the container) |
| `HACK-1458: a startup compiler call may outlast the compiler's 10 s
default while the startup deadline remains` | authored start | real: one
`compile` sleeps 10.5 s | wiring reverted: `stage inputs
(E_COMPILER_TIMEOUT)` after 11.4 s |
| `HACK-1458: Source5 cancellation after readiness settles when its down
acquisition compiler is slow` | authored start | real: first compiler
call after readiness sleeps 10.5 s | pending (running) |
| `HACK-1458: cancellation after host processes start and before
reservation stops them` | authored start | real: second `frontend-plan`
held until cancelled | teardown refresh removed: `process-order` is
`started` only and the host group stays alive |
| `HACK-1458: cancellation during the host process launch ends the
launched group` | authored start | real: first `list-panes` delayed 1 s
| `abandonLaunch` made a no-op: the owned mux session (`mux.json`) stays
|

## Evidence

In progress: 10-consecutive loaded runs of both whole files are running
locally (M5, Bun 1.4.2, private HOME/HACK_HOME/DOCKER_CONFIG, keychain
fallback disabled, unreachable DOCKER_HOST, added CPU load stopped in a
trap), on this branch plus #419's test-side budgets. Hosted CI at the
exact head is the full-suite evidence. This section will be updated with
results.

## Gates

- gitleaks 8.30.1 `dir` over a `git log -p origin/next..HEAD` export: no
leaks.
- Root `bun run check` (includes `privacy:check`): passed. `bun run
--cwd packages/cli typecheck`: 0 errors. Biome on the changed files: no
new warnings (the 8 existing complexity warnings in
`native-compose-adoption-generation.ts` remain).
- The CLI surface and consumer agent guidance did not change, so
`docs:cli-reference` and plugin regeneration do not apply.
- `origin/next` merged at 246f1b4.

## Gaps

To be completed with the evidence.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/hack-dance/codesmith/hack/pr/499?autoLogin=true&ref=codesmith_pr_footer"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v3.svg"><img
alt="View with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png"
width="162" height="28"></picture></a> <a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1795466280&installation_model_id=17053&pr_number=499&ref=codesmith_pr_footer&repository=hack-dance%2Fhack&return_to=https%3A%2F%2Fgithub.com%2Fhack-dance%2Fhack%2Fpull%2F499&signature=6606080a151b569805ac7899cdf5e25b54266947167b30eaa5ddade6721505a5"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png"
width="190" height="28"></picture></a>
<sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you
need.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dimitri Kennedy <344842+roodboi@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant