Repository navigation
feat(native): warn value-free on stale env at native exec and audit env keys in native doctor - #323
Merged
Merged
Conversation
…mpt and add the env secrets threat model The `hack global dns activate|deactivate` authorize closures move into createNativeDnsAuthorizer (src/lib/native-domain-dns-authorization.ts) with injectable interactivity, confirmation and administrator prompt. A failed sudo -v now throws NativeDnsAdministratorAuthorizationError (closed outcome "authorization-failed" and failure class; message unchanged). Tests wire the authorizer into the real activation/deactivation flows and host effect boundary and prove: sudo -v runs before every privileged and file effect, a failed prompt is authorization-failed (not denied) and changes nothing, and a decline or a non-interactive run never reaches sudo. Adds docs/security/env-secrets-threat-model.md (HACK-1127), linked from docs/env.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efaults
tests/global-dns-authorizer-wiring.test.ts checks, on the TypeScript
syntax tree of src/commands/global.ts, that handleGlobalDnsActivate and
handleGlobalDnsDeactivate each build exactly one authorizer with
createNativeDnsAuthorizer({ action: <own action>, domain }), pass it to
createNativeDnsHostDependencies, and run their flow with those
dependencies. The only other authorizer allowed is the decline-only
`async () => false` on the deactivate verify-only path.
tests/native-domain-dns-authorization.test.ts now also covers the
production defaults: the default interactivity check requires a stdin
TTY before confirming, and the default confirmation declines an
explicitly non-interactive run before sudo. The administrator prompt
stays a fake, so no test reaches real sudo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uilder `hack init --with`, `hack agent onboard`, `hack agent init` and the `hack-init` MCP prompt now call one builder in src/agents/onboarding-prompt.ts. It runs the corrected repo inventory (bun.lock, nested workspace lockfiles, non-JS runtimes, runtime pins, explicit discovery limits) and renders it before the phases. Each prompt is exactly frame.before + core + frame.after; the core is byte-identical for the same repo and only a fixed, value-free framing line differs. Mode, project name and dev host now follow repo state, so the init handoff always adopts the `.hack/` it runs after; its framing says whether this run scaffolded it. The MCP prompt takes an optional repoRoot; without it nothing is scanned and the core says no inventory ran. MCP inventory failures return closed, value-free errors. HACK-1137 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nv keys in native doctor HACK-1140: native image-project and authored native exec now run the same advisory, value-free stale-env check that #301 added for Compose exec. The foreground up/restart records salted HMAC digests of the env selection it delivered once the run is ready (bound to that run id, one 0600 file per instance under .hack/.internal/native-env-fingerprints/), removes its own record when the foreground ends, and a restart drops the record once the previous run stops, so a failed restart leaves none. Exec resolves the same selection again after the command is admitted and writes one closed warning to stderr naming the service and suggesting `hack restart`. HACK-1127: the env-key doctor diagnostic now also runs in native Doctor inside a project, and public-artifact detection walks nested public, static, dist and similar directories in monorepos (bounded depth/directories/entries, skips node_modules and .git, never follows symlinks, value-free subjects). Doctor check timers are cleared once a check settles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd session guards Pass the exec's cancellation signal, bounded by a five-second timeout, into the authored exec stale-env resolution, and settle the advisory step as soon as that signal aborts, so Ctrl-C returns 130 without waiting for input acquisition and a slow check never holds the command's output. Make the nested public-artifact walk bounds injectable for tests and cover the directory and entry bounds, including the documented 4096-directory default. Assert that a late release of a previous foreground session keeps the record a restart just wrote. Document the two exec paths that do not run the stale-env check yet: an authored project on the Compose backend and a project switched with hack config adopt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Authored .hack/hack.project.json projects now receive the same HACK_* runtime host metadata as Compose-file and native image projects, on the authored native path (up/restart; exec and run inherit) and the authored Compose renderer. The config compiler accepts optional env_metadata.runtime values for declared workloads (closed reserved key set, 64 KiB per value, no NUL) and reports them as public `runtime` bindings below managed values and every authored directive. Host entries never receive them. The values come from the shared runtime-host-metadata builder, fed with the compiler's effective routing hosts. runtime-core adds runtime bindings to public workload environment, never to private delivery. HACK-1263 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re guards Add a prepareCommand-level regression test: authored Compose `up` through the source CLI renders the reserved runtime metadata keys, and HACK_COMPOSE_PROJECT equals the generation store's hack-nc-... project for the default and a --branch instance. A checkout replaced between metadata selection and store open refuses before any Compose effect. Cover the TS wire guards: runtime metadata with non-reserved keys, undeclared workloads or non-text values refuses before the compiler spawns, and host reports carrying runtime bindings are rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…en exec output assertions - Add a command-level test for handleNativeUp with scoped module mocks for start, restart and down: up records the reported env under the image owner and effective branch and releases it (also on failure); restart drops the previous run's record at down (also when down fails), records the new run and releases it; a restart refused before down keeps the running record. - Ctrl-C advisory test: capture output in an ordered log instead of asserting on a restored spy, whose recorded calls mockRestore() clears. - Authored exec ordering test: capture stderr and Bun.write in one ordered log with non-empty command stdout/stderr, so a warning moved after the output fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…no-shell clients - Pin the value-free hack-init MCP catch: an injected builder failure whose message carries a path and a credential value must surface only the closed `<code>: hack-init could not inventory the repo.` text. - Pin the `<dev_host>` placeholder for an existing setup with no dev_host. - Give interactive `hack init` a terminal seam (TTY check, prompts, service wizard, agent-step prompts) and test that its `--with` handoff frames a scaffolded run as `created` and a kept `.hack/` as `existing`. - The /hack-init skill and canonical agent-setup guidance tell no-shell clients to pass `repoRoot` to the hack-init MCP prompt so it includes the repo inventory; renders, plugins, fingerprints and revision regenerated. - Trim repoRoot once so the presence check and the resolved path agree. HACK-1137 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tadata-b14 Conflicted files: none. The two change sets touch disjoint files, so git merged them automatically. Regenerated surfaces after the merge (no resulting diff): - bun run generate:agent-plugins - bun run docs:cli-reference - checked-in renders (.codex/skills/hack-cli, .codex/skills/hack-init, .claude/skills/hack-init, .cursor/rules/hack.mdc) compared byte-equal with their render functions. integration-revision.ts and KNOWN_RENDER_FINGERPRINTS keep the values from wf/e1-authored-metadata-b14; agent-instruction-source and integration-ownership tests pass unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…der-b14 Conflicted files and resolutions: - src/agents/integration-revision.ts: set to bd15ee2d251b, the value tests/agent-instruction-source.test.ts computes for the merged instruction source. - src/agents/integration-ownership.ts: KNOWN_RENDER_FINGERPRINTS is the append-only union of both sides, plus the merged renders requested by tests/integration-ownership.test.ts (hack-cli-skill 88900097f7eb18a752de31bb, cursor-rules 07e49d3c2b05b93b6c49adfd). - plugins/hack/rules/hack.mdc, plugins/hack/skills/hack-branches/SKILL.md, plugins/hack/skills/hack-cli/SKILL.md, plugins/hack/skills/hack-diagnose/SKILL.md, plugins/hack/skills/hack-env/SKILL.md: regenerated with bun run generate:agent-plugins. - .codex/skills/hack-cli/SKILL.md, .cursor/rules/hack.mdc: re-rendered through installCodexSkill/installCursorRules with an explicit project root; byte-equal to the renderers. - examples/basic/AGENTS.md, examples/basic/CLAUDE.md: re-rendered through upsertAgentDocs with an explicit project root. src/agents/instruction-source.ts merged cleanly. .claude/skills/hack-init and .codex/skills/hack-init re-rendered as no-ops. docs/reference/cli.md regenerated with no diff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aleenv-b14 Conflicted files: - src/lib/native-authored-command.ts: kept both sides. Imports keep the stale-env warning resolver (e3) and parseNativeHttpsSelection (e1 via e2). Kept e2's nativeHttpsPort helper and e3's envFingerprintSession helper. startupSelection keeps e3's signature (envFingerprints session, shared run id, onEnvironmentSelected capture, commit on ready) and adds e2's httpsPort: nativeHttpsPort(input.env). Generated surfaces (plugins, docs/reference/cli.md) were regenerated and produced no changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-parents onto next after the previous stacked PR squashed in; next's tree equals 43b4581, which this branch already contains, so the branch tree is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary (HACK-1140 native follow-up, HACK-1127 follow-up; unit E3)
HACK-1140: stale env warning for native exec
#301 added a value-free stale-env warning for Compose
hack exec. Native image-project exec and authored native exec now run the same check.hack up/hack restartrecords salted HMAC-SHA256 digests of the env selection it delivered, using the same digest code as feat(exec): warn value-free when a running service's env changed since start #301 (compose-env-fingerprint.ts). The record is written once the run is ready and is bound to that run id. It lives in.hack/.internal/native-env-fingerprints/<sha256(owner, branch)>.json: one file per instance, mode 0600, written with a temp file and rename. It holds no key name or value. Authored records also store the public--envand--profileselectors.startNativeProject({ onEnvironmentReady }).privateDeliverysends. The newserveNativeAuthoredProject({ onEnvironmentSelected })observer is synchronous and receives digests only.privateDeliverynow shares the projection with exec throughselectNativeAuthoredManagedEnvironment. Its payload is unchanged.downhook and the authoredonStoppedhook. So a failed restart, or a run that never becomes ready, leaves no record, and exec stays silent rather than guessing.WARNING: Env for service "web" changed since it started; this exec uses the env it started with. Run \hack restart` to apply the current env.`... the running service still uses the env it started with. ...Native image exec sends freshly resolved values to the exec itself, so the warning is about the running service.hack restartbecause native restart is whole-project.NATIVE_AUTHORED_EXEC_STALE_ENV_TIMEOUT_MS). The signal reaches input acquisition. The advisory step also settles as soon as that signal aborts, even if the resolution has not finished. Ctrl-C during the check returns 130 at once and writes no output. After the timeout, the command's output and exit code follow with no warning. A failed check also writes no warning.docs/env.mdnow says so:.hack/hack.project.jsonproject on the Compose backend (tryNativeComposeCommand->savedCommand)hack config adopt(tryLegacyComposeAdoptedCommand, exec in the retained containers)HACK-1127: env-key diagnostic in native Doctor and nested public copies
handleNativeDoctornow runs the existing "env key" check when it runs inside a project. Outside a project, native Doctor output is unchanged, with no skip warning.env-key-public-pathnow also finds copies in nestedpublic/,static/,dist/,build/,out/,www/,_site/,.next/and.output/directories, such asapps/web/public/.hack.secret.key.node_modulesand.git, never follows symlinked directories, and passes over unreadable directories.<unprintable-path>/.hack.secret.keywith a generic remedy.runChecknow clears its timeout timer once a check settles. Before this change, a fast check withtimeoutMskept native Doctor alive until the deadline (about 5 s).Docs:
docs/env.md(thehack execrow, the stale-env section, and the env-key table and walk bounds). The CLI surface and agent guidance did not change.bun run docs:cli-referenceproduced no diff, and plugin renders are untouched.Validation (local, macOS arm64; no Docker, real HOME, keychain or sudo)
Rework (head
7afb8048)Fixes for the verifier's findings on
a4497fe9:release(), and is gone only after the restart's own release.findNestedPublicArtifactDirsis exported with an optionallimitstest seam. Production uses the documented constants. New tests:inspectProjectEnvKey: with 4094 fillers,zz/publicis found; one more filler hides ittryNativeAuthoredCommandtests:hack execrow names the covered native families. The stale-env section describes the bound and lists the two uncovered exec paths.Tests
bun test tests/native-exec-stale-env.test.ts: 24 pass (21 before). Covers:runNativeProjectExecwith a stub exec: warning on stderr ahead of output, stdout unchanged, no check for a refused exectryNativeAuthoredCommandwith stubexec,serveandstopseams: exec warning, up records while ready and removes on exit, never-ready writes nothing, a failed restart drops the recordtests/native-project-start.test.ts: 70 pass. The digest is reported once, right after the mapping save and before ready. It matches the prepared (pre-adaptation) env and contains no value. A failing observer never fails startup.tests/native-authored-project-start.test.ts: 41 pass ona4497fe9(see the load note under remaining gaps). With the real managed-input fixture, the observer fires before ready, and its digest matches the deliveredTOKENvalue (unchanged) and differs for an edited one. A selection without managed keys digests an empty set, and a throwing observer never fails startup. The existing delivery-payload assertions still pass.tests/doctor-env-key.test.ts: 31 pass (28 before). Covers nested monorepo copies (apps/web/public,packages/site/dist,apps/docs/.next), skippingnode_modules/.git/symlinks, the depth, directory and entry bounds, and unprintable paths.tests/native-ca-doctor-cli.test.ts: 4 pass, usingprivateCliEnv. Native Doctor in a fixture project reports the env key check with the nested public finding, and the output contains no key text or fixture path. The existing test now runs from an isolated cwd.compose-env-staleness,project-exec-command,project-up-startup-state,native-project-exec,native-project-restart,native-authored-command/-restart/-exec/-oneoff/-logs,native-exec-environment,native-up-runtime-metadata-selection,doctor-*,integration-ownership,cli-spawn-env-isolation.env-command-matrixandskill-references: pass.bun run --cwd packages/cli typecheck: clean.bun run --cwd packages/cli check: 0 errors. The 34 warnings are pre-existing. Changed tests were linted explicitly and are clean.7afb8048(load average 17 to 19):native-exec-stale-env,doctor-env-key,native-project-start,native-ca-doctor-cli,integration-ownership,cli-spawn-env-isolation,native-authored-project-execandnative-authored-project-oneoff: 235 pass, 0 fail.native-authored-command,native-authored-command-dispatch,native-exec-stale-envandnative-authored-project-exec: 155 pass, 0 fail.compose-env-staleness, thedoctor-*suites,project-exec-commandandproject-execution-routing: pass.packages/cli typecheck: exit 0.packages/cli check: exit 0 with 0 errors and the same 34 pre-existing warnings. Biome on the changed files is clean.docs:cli-reference: no diff.releaseforgets without the salt (the mutant that previously survived)beforeOutputgets a fresh signal instead of the exec'sbeforeOutputunwirednode_modulesnot skipped, depth unboundedNegative controls
.hack/stays empty). A refused image exec runs no env resolution.Remaining gaps (host-bound)
handleNativeUpwiring inproject.ts(session creation,onEnvironmentReady, and the restartdowndrop) is three lines of plumbing. The functions it composes are tested, but the wiring itself has no direct test, because exercising it needs the native runtime. An end-to-end check (up, edit env, exec, restart) on a host with the native candidate is still open.tests/native-authored-project-start.test.tsfails intermittently with "Startup fixture settlement unconfirmed" and 5 s timeouts. In the rework run it passed 18 to 24 of 41. The verifier saw the same suite fail in all 3 runs on base8d9dc22eunder the same load. This rework does not touch that file or its code under test. The PR's two new tests in it pass when run alone.8d9dc22e:tests/native-authored-project-recovery.test.ts: 2 no-host recovery casestests/mcp-repeat-install.test.ts: 2 stdio rollback cases🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.