-
Notifications
You must be signed in to change notification settings - Fork 1k
fix(config): portable exclusive creation for config temps and clearer init publication recovery #3941
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
Merged
Merged
fix(config): portable exclusive creation for config temps and clearer init publication recovery #3941
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
b2ae869
docs(devlog): plan the C track config/init stack
lidge-jun f6df95b
fix: use portable exclusive config temp creation
x3M3x 55c00c4
test: guard atomic temp writes against Bun/Windows ENOENT
x3M3x e2231bb
chore(config): record atomic-write carry attribution
lidge-jun 5df6573
docs(devlog): record the atomic-write carry evidence
lidge-jun f3ca280
fix(config): use portable exclusive creation for the initial config temp
lidge-jun 2b0e02f
fix(init): explain configuration publication recovery
parkjs101 f704339
chore(config): record init-guidance carry attribution
lidge-jun 5821ccf
docs(devlog): drop trailing blank lines from the C track roadmap
lidge-jun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
28 changes: 28 additions & 0 deletions
28
devlog/_plan/260907_init_publication_guidance/010_implementation.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # Issue #3893: implementation plan | ||
|
|
||
| Satisfy-spec work, triggered by issue #3893 and the request to implement separate draft PRs. Goal: actionable first-run publication diagnostics. Non-goals: changing file writes, permissions, replacement/cleanup guarantees, or adding a filesystem fallback. Stop after a verified draft PR; unresolved platform checks are reported, never marked passed. Escalate if resolving the issue requires weakening publication guarantees. This file records the plan and eventual evidence. | ||
|
|
||
| Class C2: diagnostic propagation and user documentation. One independent branch from 522ce5f8c; no branch dependencies or orchestration state changes. | ||
|
|
||
| File map: | ||
| - MODIFY src/config/initialize.ts: add an optional hardeningFailed flag to constructor options; select a fixed privacy-safe permission diagnostic when hardening throws. Track the flag around the existing harden call only, and pass it in the existing error options. Append supported-location guidance to denied-link diagnostics. All I/O order and cleanup remain identical. | ||
| - MODIFY tests/config/config-mutation-lock.test.ts: inject a harden failure and prove write/link never happen, target remains absent, no residue remains, and raw error details do not appear. Assert all five denied-link codes provide recovery guidance while retaining uncertain-publication state. Partial-write errors must not be mislabeled as permission failures. | ||
| - MODIFY tests/service/init-eof.test.ts: use its existing child bootstrap seam to inject publication errors during the real CLI wizard; verify exit=1, diagnostics and residue warnings, no configuration/backup damage or integration prompts. | ||
| - MODIFY docs-site/src/content/docs/getting-started/quickstart.md and structure/02_config-and-codex-home.md: explain supported locations, inspection before retry, separate permission and link failures, and fresh-install OPENCODEX_HOME examples. Existing translations reviewed for contradictions. | ||
|
|
||
| Optional constructor input chain: created by the publication function; consumed by Error.message; no config serialization, migration, or persistent state. Existing constructor calls keep their meaning. | ||
|
|
||
| Verification: focused config/init tests read the real publication and CLI code; typecheck includes src; privacy scan; required docs-site build. Baseline focused run: 35 pass, 3 skip, 2 fail (Windows file-symlink privilege: symlinkSync EPERM and dependent missing-residue assertion). No baseline failure will be hidden by changing tests. New regression checks must pass. Windows-native filesystem support remains bounded by the host. | ||
|
|
||
| Audit: direct O_EXCL and replacement fallbacks rejected because they change complete-file/no-replace guarantees. Reuse the existing error and test seams; no new diagnostic module. Guidance never prints raw cause text or candidate bytes. | ||
|
|
||
| ## Verification before draft publication | ||
|
|
||
| - `bun install --frozen-lockfile`: passed; lockfile unchanged. | ||
| - New diagnostics were observed failing before implementation: 9 failures across the focused hardening/link/CLI fault cases. After implementation: 9 passed. | ||
| - `bun test tests/config/config-mutation-lock.test.ts tests/service/init-eof.test.ts`: 38 passed, 3 skipped, 2 failed. The same two tests failed on unchanged 522ce5f8c: file-symlink creation is denied on this Windows host, and the swapped-symlink test then lacks its expected residue. New recovery tests pass; no skips or weakened assertions were added. | ||
| - `bun run typecheck`: passed. | ||
| - `bun run privacy:scan`: passed. | ||
| - `cd docs-site; bun install --frozen-lockfile; bun run build`: passed, 425 pages. Translated quickstarts contain no conflicting recovery/fallback instructions. | ||
| - CLI fault scenarios verify exit=1, distinct permission/link messages, uncertain-publication/residue warnings, backup preservation and no integration prompts. Partial-write errors keep the generic diagnostic. | ||
| - No physical non-NTFS filesystem support is claimed. Maintainer review remains required; this is a draft handoff. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| # 000_plan.md — C track: config file + init as a manual PR stack | ||
|
|
||
| ## Objective | ||
|
|
||
| Land the C triage track (config-file and init surfaces) on `dev` as one manual, | ||
| dependency-ordered branch chain whose tip carries every layer. The track has two | ||
| existing contributor pull requests plus one gap discovered while planning: | ||
|
|
||
| | Layer | Source | Surface | | ||
| |---|---|---| | ||
| | wp1 | PR #3900 by @x3M3x | `src/config/atomic-write.ts` Bun/Windows ENOENT | | ||
| | wp2 | new (this unit) | `src/config/initialize.ts` sibling numeric flag | | ||
| | wp3 | PR #3896 by @parkjs101 | `ocx init` publication recovery guidance (closes #3893) | | ||
|
|
||
| wp2 exists on its own merit, not as glue. `publishInitialConfigNoReplace` still | ||
| opens its temp file with the numeric spelling that Bun miscompiles on Windows, | ||
| so shipping wp1 alone leaves first-run config publication exposed to the same | ||
| `ENOENT`. #3900 never touches `initialize.ts`; the file overlap is between wp2 | ||
| and wp3 only. | ||
|
|
||
| ## Constraints (owner-stated, this session) | ||
|
|
||
| - **No local product suite.** No `bun run test`, `bun run typecheck`, | ||
| `bun run build`, or install. Every such check is recorded **NOT RUN**. | ||
| - **Push with `--no-verify`** on every layer. | ||
| - **CI on the tip only.** Verified mechanism in `040`: the lower layers are | ||
| pushed as branches but **no pull request is opened for them** until the tip | ||
| has landed. `.github/workflows/ci.yml` triggers on `pull_request: {}` with no | ||
| draft filter, so opening a lower PR would start CI; draft status suppresses | ||
| nothing. | ||
| - **Green tip merges; the rest resolve.** When the tip's exact head SHA is green | ||
| against a current `dev` base, merge the tip, then resolve the source PRs and | ||
| close issue #3893. | ||
| - **Original authors are preserved** with full `Co-authored-by: Name <email>` | ||
| trailers that survive the squash (AGENTS.md "Landing another author's work"). | ||
|
|
||
| ## Build order | ||
|
|
||
| ``` | ||
| codex/c-track-init-guidance → the ONLY pull request (base dev) ← wp3 tip | ||
| codex/c-track-initialize-flag → branch only, no PR ← wp2 | ||
| codex/c-track-atomic-write → branch only, no PR ← wp1 | ||
| ──────────────────────────────── dev | ||
| ``` | ||
|
|
||
| Each branch is based on the one below, so the tip's tree is the cumulative | ||
| result. wp2 sits between the carried PRs because wp3 inserts a line directly | ||
| after the `openSync` call that wp2 rewrites; constructing wp2 first means that | ||
| adjacent-hunk overlap is resolved once while carrying wp3. This is a chosen | ||
| construction order for a single conflict resolution, not a semantic | ||
| prerequisite — either change could be written first. | ||
|
|
||
| ## Scope boundary | ||
|
|
||
| IN: the three layers above, their regression tests, the docs/structure text that | ||
| #3896 already carries, and this devlog unit. | ||
|
|
||
| OUT: `#3838`/`#3917` adapter work, any other triage track, release promotion, | ||
| `main`/`preview`, and any behavioral change to hard-link publication, ACL | ||
| hardening, or credential storage beyond the flag spelling. | ||
|
|
||
| ## Verifiers | ||
|
|
||
| Local product gates are forbidden this session, so acceptance rests on | ||
| repository CI against the tip plus read-only inspection. | ||
|
|
||
| | Claim | Evidence | Status | | ||
| |---|---|---| | ||
| | Layers carry original authorship | `git log --format='%(trailers:key=Co-authored-by)'` on the tip, then on the landed commit | to run (read-only) | | ||
| | wp2 removes the numeric spelling | `rg 'constants\.O_' src/config/initialize.ts` on the pushed tip tree | to run (read-only) | | ||
| | Carried content really landed | tip tree vs. each source PR's pinned patch, then landed-merge tree comparison (see `040`) | to run (read-only) | | ||
| | Layers build and pass | repository CI on the tip head SHA, base `dev` | tip only | | ||
| | Local suite / typecheck / build | — | **NOT RUN** (owner instruction) | | ||
|
|
||
| ## Terminal outcome | ||
|
|
||
| DONE requires: tip CI success on its exact head SHA against a current `dev` | ||
| base, tip merged into `dev` proven by fetched ancestry and tree comparison, | ||
| source PRs resolved with credit intact, and #3893 closed. | ||
72 changes: 72 additions & 0 deletions
72
devlog/_plan/260908_c_track_config_init_stack/010_layer1_atomic_write.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| # 010_layer1_atomic_write.md — wp1: carry PR #3900 | ||
|
|
||
| Source: [PR #3900](https://github.com/lidge-jun/opencodex/pull/3900) by @x3M3x, | ||
| head `744eb644028492784446fe9f0f73813d5d1fe59f`, two commits | ||
| (`52c749561`, `744eb6440`). | ||
|
|
||
| Branch: `codex/c-track-atomic-write`, base `dev`. | ||
|
|
||
| ## Problem | ||
|
|
||
| `src/config/atomic-write.ts` builds its exclusive-create flags numerically. | ||
| Bun on Windows misreads that combination and drops the creation bit, so every | ||
| private temp write fails with `ENOENT`: `ocx start`, management-API config | ||
| saves, and OAuth credential refreshes all route through these two writers. | ||
|
|
||
| ## Change (MODIFY, carried unmodified from #3900) | ||
|
|
||
| `src/config/atomic-write.ts` | ||
|
|
||
| ```diff | ||
| - const descriptor = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600); | ||
| + const descriptor = openSync(path, "wx", 0o600); | ||
| ``` | ||
|
|
||
| Applied in both `writePrivateTempFile` and `writePrivateTempFileAsync`; the now | ||
| unused `constants` import is dropped. | ||
|
|
||
| `tests/windows/windows-secret-acl.test.ts` gains the source-oracle guard | ||
| "atomic secret temp writer portability", asserting exactly two portable calls. | ||
|
|
||
| ## Semantics note (audit correction) | ||
|
|
||
| The PR description calls `"wx"` exactly `O_WRONLY | O_CREAT | O_EXCL`. Node and | ||
| Bun actually map it to `O_WRONLY | O_CREAT | O_EXCL | O_TRUNC`. It is | ||
| **behaviorally** equivalent here rather than bit-identical: exclusive creation | ||
| rejects an existing path, so `O_TRUNC` can never truncate one. `0o600` remains | ||
| a separate mode argument and still applies. Recorded so a later reader does not | ||
| inherit the imprecise claim. | ||
|
|
||
| ## Security review (independent, read-only) | ||
|
|
||
| No blocking finding. Exclusivity is preserved for every caller of the default | ||
| wrappers, which include OAuth `auth.json`, Codex account credentials, service | ||
| API tokens, `config.json`, and `ocx.pid`. Ownership is marked only after a | ||
| successful create, so no new pre-existing-temp or symlink-following path opens. | ||
| Windows ACL ordering (create → own → harden → identity check → write → close) | ||
| is untouched. | ||
|
|
||
| ## Authorship | ||
|
|
||
| Both commits are cherry-picked with `-x`, so each retains | ||
| `x3M3x <amroeid1999@gmail.com>` as its git author and records the source SHA: | ||
|
|
||
| | Carried commit | Source commit | | ||
| |---|---| | ||
| | `6a0abcf90` fix: use portable exclusive config temp creation | `52c7495618f18f2847b7f9468421442c1c573da1` | | ||
| | `24a078d80` test: guard atomic temp writes against Bun/Windows ENOENT | `744eb644028492784446fe9f0f73813d5d1fe59f` | | ||
|
|
||
| A squash landing keeps only the squash message and drops per-commit authors, so | ||
| `b1a7f111c` adds the `Co-authored-by: x3M3x <amroeid1999@gmail.com>` trailer to | ||
| the branch. That trailer must be carried into the tip PR squash message and | ||
| re-read on the landed commit. | ||
|
|
||
| ## Verification | ||
|
|
||
| An independent read-only audit of the built branch confirmed the carried | ||
| source-and-test diff is byte-identical to #3900 pinned patch (2,176 bytes), | ||
| that both `-x` annotations and the original author survive, that the trailer | ||
| parses through `git interpret-trailers`, and that no other `src/` file changed. | ||
|
|
||
| Repository CI on the stack tip only. Local suite, typecheck, and build: | ||
| **NOT RUN** (owner instruction). |
69 changes: 69 additions & 0 deletions
69
devlog/_plan/260908_c_track_config_init_stack/020_layer2_initialize_flag.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # 020_layer2_initialize_flag.md — wp2: sibling flag in initialize.ts | ||
|
|
||
| New work in this unit. Branch `codex/c-track-initialize-flag`, base | ||
| `codex/c-track-atomic-write`. | ||
|
|
||
| ## Why this layer exists | ||
|
|
||
| wp1 fixes the two writers in `atomic-write.ts`, but | ||
| `publishInitialConfigNoReplace` in `src/config/initialize.ts` still opens its | ||
| temp file with the same numeric spelling. Independent inspection confirms the | ||
| identical Bun/Windows exposure: first-run `ocx init` fails before writing or | ||
| publishing `config.json`, leaving `publication = "not-published"` and | ||
| `hardLinkUnavailable = false`, so the CLI prints "Initial config publication | ||
| did not finish." and exits 1. | ||
|
|
||
| It also sits between the two carried PRs deliberately. #3896 inserts a line | ||
| immediately after this `openSync` call, so building wp2 first means the | ||
| adjacent-hunk overlap is resolved once, while carrying #3896 in wp3. This is a | ||
| chosen construction order rather than a semantic prerequisite: #3900 does not | ||
| touch this file at all, and either change could be written first. | ||
|
|
||
| ## Change (MODIFY) | ||
|
|
||
| `src/config/initialize.ts` | ||
|
|
||
| ```diff | ||
| import { | ||
| - closeSync, constants, fchmodSync, fstatSync, linkSync, lstatSync, | ||
| + closeSync, fchmodSync, fstatSync, linkSync, lstatSync, | ||
| openSync, unlinkSync, writeFileSync, | ||
| } from "node:fs"; | ||
| @@ | ||
| - fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600); | ||
| + fd = openSync(temp, "wx", 0o600); | ||
| ``` | ||
|
|
||
| `constants` is referenced only by the import and that one expression, so the | ||
| import entry is removed with it. | ||
|
|
||
| ## Change (MODIFY, regression) | ||
|
|
||
| `tests/windows/windows-secret-acl.test.ts` gains a sibling source-oracle test | ||
| next to the wp1 guard, asserting exactly one portable call in | ||
| `src/config/initialize.ts`. | ||
|
|
||
| ## Contract preserved | ||
|
|
||
| The no-replace publication contract does not depend on the numeric spelling: | ||
| hardening, `verifyPrivateTemp`, the single `linkSync` publication with its | ||
| `EEXIST`/`collided` and `hardLinkUnavailable` handling, and the | ||
| descriptor-owned `removeOwnedTemp` cleanup are all unchanged. | ||
|
|
||
| ## Out of scope: the same pattern under `src/lab/` | ||
|
|
||
| An independent scan found three more exclusive opens sharing this combination: | ||
| `src/lab/ledger/store.ts:153` and `:185` (recovery mutex, ledger lock) and | ||
| `src/lab/public/private-file.ts:209` (private publication temp). They deserve the | ||
| same portability follow-up, but Lab is an opt-in subsystem off the core request | ||
| path, so they stay out of this track rather than widening a config-surface fix. | ||
|
|
||
| Two further matches are not exclusive opens and must not be swept in: | ||
| `src/codex/native-main-lock-file.ts:89` and `src/lab/fabric/scratch.ts:416`. The | ||
| read/write sites in `src/lab/artifacts/secure-fs.ts` need individual treatment because | ||
| `"wx"` would drop read access. | ||
|
|
||
| ## Verification | ||
|
|
||
| Repository CI on the stack tip only. Local suite, typecheck, and build: | ||
| **NOT RUN** (owner instruction). |
65 changes: 65 additions & 0 deletions
65
devlog/_plan/260908_c_track_config_init_stack/030_layer3_init_guidance.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # 030_layer3_init_guidance.md — wp3: carry PR #3896 (stack tip) | ||
|
|
||
| Source: [PR #3896](https://github.com/lidge-jun/opencodex/pull/3896) by | ||
| @parkjs101 (Joonsuh Park), head `fc78bc37d419576061b995281baf39c46655eaa9`, | ||
| closes [#3893](https://github.com/lidge-jun/opencodex/issues/3893). | ||
|
|
||
| Branch: `codex/c-track-init-guidance`, based on `codex/c-track-initialize-flag`. | ||
| This is the stack tip and the only pull request opened for this unit. | ||
|
|
||
| ## Problem | ||
|
|
||
| `ocx init` already separates denied hard-link publication from a generic | ||
| failure, but a required permission-hardening failure falls into the generic | ||
| message, and neither message tells the user what to do next. The user cannot | ||
| tell why publication stopped or where to move `OPENCODEX_HOME`. | ||
|
|
||
| ## Change (carried from #3896, rebased onto wp2) | ||
|
|
||
| - `src/config/initialize.ts`: a `hardeningFailed` flag set immediately before | ||
| the hardening call and cleared immediately after it returns — the assignments | ||
| surround `hardenInitialConfig`, which runs after `openSync`. The flag selects | ||
| a distinct message; `InitialConfigPublicationError` takes the matching option, | ||
| and both messages gain `OPENCODEX_HOME` recovery direction. | ||
| - `docs-site/src/content/docs/getting-started/quickstart.md`: inspection before | ||
| retry, preserving existing configuration, choosing a supported location. | ||
| - `structure/02_config-and-codex-home.md`: records the diagnostic distinction. | ||
| - `tests/config/config-mutation-lock.test.ts`, `tests/service/init-eof.test.ts`: | ||
| permission, link, and cleanup faults, privacy-safe messages, backup | ||
| preservation. Both files already exist in the test-layout registries, so no | ||
| registry entry is added. | ||
| - `devlog/_plan/260907_init_publication_guidance/010_implementation.md`: carried | ||
| as-is; on terminal closure that unit moves to `_fin/`. | ||
|
|
||
| The rebase keeps wp2's `openSync(temp, "wx", 0o600)` and both `hardeningFailed` | ||
| assignments around the hardening call. | ||
|
|
||
| ## Review (independent subagent audit, read-only, this session) | ||
|
|
||
| Reviewed at `fc78bc37d`, all six files. No blocking finding: | ||
|
|
||
| - The flag cannot be left incorrectly true. Write, verify, link, and close | ||
| failures all occur after it is cleared (`src/config/initialize.ts:95-115`, | ||
| `:129-134`). A throwing injected `io.harden` test seam would select the same | ||
| message, which is a seam edge rather than a production defect. | ||
| - File I/O ordering, the no-replace guarantee, and private permissions are | ||
| unchanged (`:38-43`, `:98-130`). | ||
| - The new messages are fixed text naming the `OPENCODEX_HOME` variable; they | ||
| interpolate no real paths, bytes, or filesystem error text, and raw errors | ||
| stay in the `cause` the CLI does not print (`src/cli/init.ts:262-267`). | ||
|
|
||
| This is a static agent review, not the maintainer security review or the | ||
| approval required by `MAINTAINERS.md`; those are recorded separately in `040`. | ||
|
|
||
| ## Authorship | ||
|
|
||
| Carried with `Co-authored-by: Joonsuh Park <trckstr4422@gmail.com>`, the identity | ||
| on the source commit. The trailer must appear in the **squash message** of the | ||
| landed commit, and is verified on the landed commit rather than only on the | ||
| branch. | ||
|
|
||
| ## Verification | ||
|
|
||
| This tip is the only layer that triggers repository CI, and its exact head SHA | ||
| must be green against a current `dev` base. Local suite, typecheck, and build: | ||
| **NOT RUN** (owner instruction). |
Oops, something went wrong.
Oops, something went wrong.
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.
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.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the two markdownlint findings in the plan.
Add
textto the branch-diagram fence. Rewrite the issue-number sentence so it does not begin with#3896. This removes MD040 and MD018 without changing the plan content.Proposed documentation fix
Static analysis reports MD040 at Line 39 and MD018 at Line 56.
Also applies to: 56-56
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 39-39: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Source: Linters/SAST tools