-
Notifications
You must be signed in to change notification settings - Fork 0
ci(host-filesystem): prove host-install slice on Ubuntu, macOS, and Windows #787
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
66f2a77
5420782
789a531
b189762
c84c6a4
a8163c1
e443ca9
61b9201
52db175
1b4cace
d85454c
5f9c953
6e270b9
a377fbb
2b0875c
64ad3a2
4d1b15d
89377e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "agent-bundle": patch | ||
| --- | ||
|
|
||
| On Windows, `createProjectContext` now judges existing project paths by on-disk identity so 8.3 aliases and native config paths no longer raise `AB7001`; missing paths resolve the nearest existing ancestor (so a dangling leaf under an escaping symlink still fails closed) instead of throwing `ENOENT`. After that containment gate, the configuration file and every recorded source input must still exist (`ENOENT`); prebuilt hook and payload paths that are allowed not to exist yet stay valid. Source snapshots record the same identity path `createProjectContext` uses, so a development rebuild no longer fails with `AB7101` when `path.resolve` spelling differs from the realpath project identity, and a successful `agent-bundle dev` rebuild publishes `artifact.available` from the epoch it just built so host-install attaches even when a prepare snapshot digest and compiler project revision disagree. `agent-bundle prepack` launches npm's `npm-cli.js` through `process.execPath` and ignores a pnpm `npm_execpath`. Development host install publishes Windows directory junctions with absolute realpath targets and replaces an existing junction by moving the previous pointer aside; development epoch publication opens existing regular files with write-capable non-truncating `r+` on Windows before `fsync` so FlushFileBuffers is not refused as `EPERM` (`AB7100`), while directory FlushFileBuffers `EACCES`/`EINVAL`/`EPERM` stays best-effort and genuine file-sync failures still refuse publication; and a declared state root whose parent is a file records as unproven (`EEXIST`) instead of failing install. (#787) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,8 +23,8 @@ Verify-equivalent leg on whatever Node is currently active, with the repo's | |
| normal local worker derivation. It skips the Node matrix and the | ||
| examples/release/micro-eval gates, so it is a fast signal, not a merge gate. | ||
|
|
||
| Docs-only PRs skip the hosted Verify, examples, release-gates, micro-eval, and | ||
| host-install-proofs jobs. Docs-only means changes under `docs/` or `agent-patterns/`, changeset | ||
| Docs-only PRs skip the hosted Verify, examples, release-gates, micro-eval, | ||
| host-install-proofs, and host-filesystem jobs. Docs-only means changes under `docs/` or `agent-patterns/`, changeset | ||
| markdown (`.changeset/*.md`), or top-level markdown. Nested markdown elsewhere | ||
| is treated as code. Pushes to `main` never use this skip. The allowlist and | ||
| fail-open listing checks are implemented by `scripts/classify-docs-only.mjs` | ||
|
|
@@ -148,6 +148,46 @@ the `claude -p` session proofs (`AGENT_BUNDLE_HOST_INSTALL_CLAUDE_SESSION`, | |
| (`AGENT_BUNDLE_PACKED_NATIVE_{CLAUDE,CODEX}_SMOKE`). Those still run only in | ||
| the opt-in `native-host-smoke` workflow on a signed-in runner. | ||
|
|
||
| ## Host-install filesystem OS matrix | ||
|
|
||
| The published `agent-bundle` package has no `os` field: it is a | ||
| platform-generic npm package, and `dev --install-host` already has Windows | ||
| junction publish, atomic rename/rollback, ownership manifests, path-casing | ||
| comparisons, and per-OS host config locations. Primary CI still runs Verify, | ||
| examples, release-gates, and the pinned-CLI host-install proofs on | ||
| `ubuntu-latest` only. Cloning that suite onto three OSes would multiply the | ||
| PR critical path. | ||
|
|
||
| Hosted CI therefore adds one extra job, `host-filesystem`, on | ||
| `ubuntu-latest`, `macos-latest`, and `windows-latest` (Node 22.19, engines | ||
| floor — three cells, not a Node × OS product). It builds once and runs | ||
| `pnpm test:host-filesystem` (`rstest.host-filesystem.config.ts`, which does | ||
| not build the Workbench e2e example payload): | ||
|
|
||
| - `dev-host-install.test.ts` (Claude/Codex legs skip when those CLIs are | ||
| absent; this job does not install them) | ||
| - `dev-host-install-manager.test.ts` (rollback and stale ownership) | ||
| - `install.test.ts` / `uninstall.test.ts` (receipt ownership) | ||
| - `durable-fs.test.ts` (atomic publish, Windows directory fsync) | ||
| - `internal-child-resolution-policy.test.ts` (packaged child resolution, #769) | ||
| - `packed-install-bin.test.ts` (packaged installer bin from a consumer cwd) | ||
| - `rstest-worker-isolation.test.ts` (canonical TMPDIR; macOS `/tmp` → `/private/tmp`) | ||
|
Comment on lines
+171
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This list says it enumerates the tests run by AGENTS.md reference: AGENTS.md:L96-L99 Useful? React with 👍 / 👎. |
||
|
|
||
| That is the slice that can actually diverge by OS. macOS matters because | ||
| Claude, Codex, and Cursor authors commonly develop there; Windows is in the | ||
| matrix because the code already has `win32` branches, not because the rest | ||
| of the suite is claimed green there. A green `Host filesystem (windows-latest)` | ||
| job is evidence for this slice only. | ||
|
|
||
| To run the same slice locally after `pnpm build`: | ||
|
|
||
| ```sh | ||
| pnpm test:host-filesystem | ||
| ``` | ||
|
|
||
| The local gate does not fan this across OSes — one machine can prove one OS. | ||
| Hosted macOS and Windows results are the qualification for those runners. | ||
|
|
||
| ## Node provisioning | ||
|
|
||
| The runner introduces no new tooling. For each hosted runtime line | ||
|
|
@@ -233,10 +273,17 @@ then treat a repeat as a real signal. | |
| does not install host CLIs into its legs, so run those proofs by hand with | ||
| the commands above when a change touches adapter emission, the installers, | ||
| or the proof suites. | ||
| - **host-filesystem** is the three-OS host-install/filesystem slice (see | ||
| [Host-install filesystem OS matrix](#host-install-filesystem-os-matrix)). | ||
| The local gate can run `pnpm test:host-filesystem` on the current OS after | ||
| `pnpm build`; it cannot prove macOS or Windows from a Linux machine. | ||
| - **native-host-smoke** needs signed-in Claude/Codex CLIs and is opt-in even | ||
| on hosted CI. | ||
| - **Environment skew**: hosted runners are `ubuntu-latest`. Hosted Workbench | ||
| browser suites launch Playwright's bundled Chromium — pinned by the | ||
| - **Environment skew**: Verify, examples, release-gates, micro-eval, and the | ||
| pinned-CLI host-install proofs stay on `ubuntu-latest`. The | ||
| `host-filesystem` job is the exception: `ubuntu-latest`, `macos-latest`, | ||
| and `windows-latest`. Hosted Workbench browser suites launch Playwright's | ||
| bundled Chromium — pinned by the | ||
| Playwright version in the lockfile and selected with | ||
| `AGENT_BUNDLE_PLAYWRIGHT_CHANNEL=chromium` (read by | ||
| `packages/workbench/tests/support/workbench-e2e.ts`) — so the browser under | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import { existsSync, realpathSync } from 'node:fs'; | ||
| import { delimiter as defaultDelimiter, dirname, join } from 'node:path'; | ||
|
|
||
| const npmCliJs = 'npm-cli.js'; | ||
|
|
||
| /** Filesystem and environment the resolver consults. Tests inject a fake. */ | ||
| export interface NpmCliResolutionIo { | ||
| readonly delimiter?: string; | ||
| readonly env: Readonly<Record<string, string | undefined>>; | ||
| readonly execPath: string; | ||
| readonly exists: (candidate: string) => boolean; | ||
| readonly realpath: (candidate: string) => string; | ||
| } | ||
|
|
||
| const isNpmCliJs = (candidate: string): boolean => candidate.endsWith(npmCliJs); | ||
|
|
||
| /** Official Node layouts relative to a `bin/` or install-prefix directory. */ | ||
| const officialNpmCliCandidates = (directory: string): readonly string[] => [ | ||
| join(directory, 'node_modules', 'npm', 'bin', npmCliJs), | ||
| join(directory, '..', 'lib', 'node_modules', 'npm', 'bin', npmCliJs), | ||
| join(directory, '..', 'node_modules', 'npm', 'bin', npmCliJs), | ||
| ]; | ||
|
|
||
| const realpathIfNpmCli = (io: NpmCliResolutionIo, candidate: string): string | undefined => { | ||
| if (!io.exists(candidate)) return undefined; | ||
| try { | ||
| const real = io.realpath(candidate); | ||
| return isNpmCliJs(real) ? real : undefined; | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| }; | ||
|
|
||
| const candidatesFromDirectory = (io: NpmCliResolutionIo, directory: string): readonly string[] => { | ||
| const found: string[] = [...officialNpmCliCandidates(directory)]; | ||
| // Unix nvm: `bin/npm` → `../lib/node_modules/npm/bin/npm-cli.js`. | ||
| // Windows: `npm.cmd` / `npm.ps1` do not realpath to npm-cli.js; the | ||
| // official layouts above still find the JS entry beside the shim. | ||
| for (const shim of ['npm', 'npm.cmd', 'npm.ps1', 'npm.exe'] as const) { | ||
| const resolved = realpathIfNpmCli(io, join(directory, shim)); | ||
| if (resolved !== undefined) found.push(resolved); | ||
| } | ||
| return found; | ||
| }; | ||
|
|
||
| /** | ||
| * Locates a real on-disk `npm-cli.js`. Never assumes `npm` is a resolvable | ||
| * package from `createRequire(import.meta.url)` — a pnpm-managed Node has no | ||
| * such dependency from this module — and never treats `npm_execpath` as npm | ||
| * when it points at pnpm or a cmd shim. | ||
| */ | ||
| export const resolveNpmCliJs = (io: NpmCliResolutionIo): string => { | ||
| const execDir = dirname(io.execPath); | ||
| const delimiter = io.delimiter ?? defaultDelimiter; | ||
| const pathEnv = io.env['PATH'] ?? io.env['Path'] ?? io.env['path'] ?? ''; | ||
| const prefix = io.env['npm_config_prefix'] ?? io.env['NPM_CONFIG_PREFIX']; | ||
| const candidates: (string | undefined)[] = []; | ||
|
|
||
| const execpath = io.env['npm_execpath']; | ||
| if (execpath !== undefined && isNpmCliJs(execpath)) candidates.push(execpath); | ||
|
|
||
| candidates.push(...candidatesFromDirectory(io, execDir)); | ||
| if (prefix !== undefined && prefix.length > 0) { | ||
| candidates.push(...officialNpmCliCandidates(prefix)); | ||
| candidates.push(...candidatesFromDirectory(io, join(prefix, 'bin'))); | ||
| } | ||
| for (const entry of pathEnv.split(delimiter)) { | ||
| if (entry.length === 0) continue; | ||
| candidates.push(...candidatesFromDirectory(io, entry)); | ||
| } | ||
|
|
||
| for (const candidate of candidates) { | ||
| if (candidate !== undefined && isNpmCliJs(candidate) && io.exists(candidate)) { | ||
| return candidate; | ||
| } | ||
| } | ||
| throw new Error(`Unable to resolve npm-cli.js from ${io.execPath}`); | ||
| }; | ||
|
|
||
| /** Resolves `npm-cli.js` from the running Node and process environment. */ | ||
| export const resolveProcessNpmCliJs = (): string => | ||
| resolveNpmCliJs({ | ||
| env: process.env, | ||
| execPath: process.execPath, | ||
| exists: existsSync, | ||
| realpath: realpathSync, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changeset is a declarative, implementation-heavy account beginning with “now judges” and detailing internal flags, fsync behavior, and error handling, rather than an imperative release summary. Rewrite it in imperative voice around the user-visible commands and behavior while retaining the relevant diagnostic codes and
(#787)suffix.AGENTS.md reference: AGENTS.md:L124-L125
Useful? React with 👍 / 👎.