Skip to content

fix(airlock): stop enable/disable rewriting network.json - #133

Merged
GordonBeeming merged 3 commits into
mainfrom
gb/fix-airlock-network-json-overwrite
Aug 13, 2026
Merged

fix(airlock): stop enable/disable rewriting network.json#133
GordonBeeming merged 3 commits into
mainfrom
gb/fix-airlock-network-json-overwrite

Conversation

@GordonBeeming

Copy link
Copy Markdown
Owner

Summary

Toggling airlock destroyed config in two separate ways, both reported in #129.

The toggle rewrote the whole file. SetEnabledInJson deserialized network.json into NetworkConfig, flipped Enabled, and serialized it back over the top, so comments, key order, indentation and any key the class doesn't model were lost. A file containing JSON comments threw an unhandled JsonException instead. This came in with c0c1a83, which replaced a surgical true/false swap with a typed round-trip.

The enabled value is now spliced in place using Utf8JsonReader token offsets. A CurrentDepth == 1 check stops an enabled nested inside a rule from matching, which the old string-scanning code couldn't do, and the document is validated up front so a file broken further down isn't rewritten into a half-fixed state.

Separately, --disable-airlock silently emptied a project's ruleset. With rules only in ~/.config/copilot_here/network.json, running it inside a project created .copilot_here/network.json with allowed_rules: []. Local config replaces global outright, so re-enabling left the project running with no rules at all and nothing in the output said so. Creating a local file now seeds it from the global one, and the command reports that it did.

Bad JSON also names the file and exits 1 rather than throwing a stack trace, and the reader accepts comments and trailing commas to match how people actually edit the file.

Test plan

  • 605/605 unit tests green against origin/main plus this branch. 12 are new: round-trip preservation, comments, an enabled nested inside a rule, a missing enabled key, an empty root object, malformed JSON, and the global-to-local seeding.
  • Round-trip against the NativeAOT binary. A hand-edited file with a comment, a custom top-level key and a custom key inside a rule, then --disable-airlock followed by --enable-airlock: diff reports it byte-identical to the original.
  • The reported setup: rules in the global config only, --disable-airlock inside a project. The new local file carries both global rules instead of [].
  • Malformed JSON: names the path, exits 1, leaves the file untouched.
  • dotnet publish -c Release -r osx-arm64 clean, no trim or AOT warnings from the changed code.

Closes #129

Toggling airlock read the whole file into NetworkConfig, flipped one bool and
serialized it back over the top, so comments, key order and any key the class
doesn't model were destroyed. A file containing JSON comments threw instead.
The enabled value is now spliced in place using Utf8JsonReader offsets, with a
depth check so an "enabled" nested inside a rule can't be matched, and the
document is validated up front so a file broken further down isn't rewritten.

Separately, --disable-airlock created an empty local network.json when only a
global one existed. Local config replaces global entirely, so that silently
dropped the project out of its ruleset. Creating a local file now seeds it from
the global one and the command says it did.

Bad JSON reports the path and exits 1 instead of throwing a stack trace, and
the reader accepts comments and trailing commas to match how people edit it.

Closes #129
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@GordonBeeming, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9292cc80-139f-4cf9-82fc-9ddaef4d9cbf

📥 Commits

Reviewing files that changed from the base of the PR and between d59cd5e and 402cd22.

📒 Files selected for processing (9)
  • README.md
  • app/Commands/Airlock/DisableAirlock.cs
  • app/Commands/Airlock/DisableGlobalAirlock.cs
  • app/Commands/Airlock/EnableAirlock.cs
  • app/Commands/Airlock/EnableGlobalAirlock.cs
  • app/Commands/Airlock/NetworkConfig.cs
  • app/Commands/Airlock/_AirlockCommands.cs
  • app/Commands/Airlock/_AirlockConfig.cs
  • tests/CopilotHere.UnitTests/AirlockConfigTests.cs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@GordonBeeming

Copy link
Copy Markdown
Owner Author

@codex review

@GordonBeeming

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75977addc8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/Commands/Airlock/_AirlockConfig.cs Outdated
Comment thread app/Commands/Airlock/_AirlockConfig.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Airlock enable/disable behavior so network.json is no longer round-tripped through a typed model (which previously dropped comments/formatting/unknown keys), and prevents --disable-airlock from accidentally shadowing global rules with an empty local file.

Changes:

  • Update Airlock toggling to splice/insert the root "enabled" value in-place using Utf8JsonReader offsets (preserving comments, ordering, indentation, unknown keys, and BOM; validating JSON first).
  • Seed newly-created local .copilot_here/network.json from global ~/.config/copilot_here/network.json and surface an outcome so the CLI can report what happened.
  • Add targeted unit tests for preservation, comment handling, nested enabled, missing enabled, malformed JSON, and global-to-local seeding; update README to document the behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/CopilotHere.UnitTests/AirlockConfigTests.cs Adds new unit tests covering toggle preservation, malformed JSON handling, and global-to-local seeding outcomes.
README.md Documents non-merge behavior, seeding from global on first local creation, and preservation guarantees during toggles.
app/Commands/Airlock/NetworkConfig.cs Aligns JSON source-gen options with hand-edited config reality (comments + trailing commas).
app/Commands/Airlock/_AirlockConfig.cs Implements byte-preserving toggle logic, seeding behavior, and introduces AirlockToggleOutcome.
app/Commands/Airlock/_AirlockCommands.cs Adds shared toggle runner with consistent success/error reporting and seeded-from-global messaging.
app/Commands/Airlock/EnableAirlock.cs Switches to shared toggle runner and reports rules path.
app/Commands/Airlock/DisableAirlock.cs Switches to shared toggle runner and reports rules path.
app/Commands/Airlock/EnableGlobalAirlock.cs Switches to shared toggle runner and reports rules path.
app/Commands/Airlock/DisableGlobalAirlock.cs Switches to shared toggle runner and reports rules path.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread app/Commands/Airlock/_AirlockCommands.cs
Comment thread app/Commands/Airlock/_AirlockConfig.cs Outdated
Address Copilot + Codex feedback on PR #133 round 1:

- FindRootEnabledValue returned the first root "enabled" occurrence, but
  System.Text.Json's deserializer (used by Load) resolves duplicate keys to
  the last one. A hand-edited file with a duplicate root "enabled" could
  report success while Load() still saw the stale value. Keep scanning and
  splice the occurrence Load() will actually read.

- InsertRootEnabled's empty-object branch replaced every byte between the
  braces, including a comment-only object's comment, since JsonCommentHandling.Skip
  doesn't surface comments as tokens. Only use the whole-range replace for a
  truly empty `{}`; otherwise insert ahead of the existing content so
  comments (or any other whitespace) survive.

- Same method always inserted a newline+indent even when the file had no
  newline anywhere, reformatting a deliberately single-line network.json
  onto multiple lines. Detect the no-newline case and insert inline instead.

- RunToggle's JsonException handler claimed the file "isn't valid JSON" even
  when the JSON was syntactically valid but shaped wrong (e.g. "enabled"
  holding an object). Drop the specific claim and let ex.Message carry the
  actual reason.
@GordonBeeming
GordonBeeming requested a lite review from Copilot August 13, 2026 16:09
@GordonBeeming

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1dd61f991b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/Commands/Airlock/_AirlockConfig.cs
…omma

The round-1 fix for comment/whitespace preservation in InsertRootEnabled's
EndObject branch narrowed the "replace the whole gap" path to only the
truly-empty {} case, but let every other EndObject case fall through to the
insert-with-comma path. That path is only correct when a real property
follows the insertion point - an EndObject means nothing does, so the comma
made the file strict-JSON-invalid (our own AllowTrailingCommas=true reader
tolerated it, which is exactly why the suite didn't catch it).

Split the EndObject branch three ways instead: truly empty (synthesize the
whole line, no comma), whitespace/comments only (insert ahead of the content,
no comma), and a real property follows (insert with comma, unchanged).

Tests now parse every insert-path result with AllowTrailingCommas=false so a
malformed splice can't hide behind our own reader's leniency again. Added
cases for a truly-empty {} and a whitespace-only {\n} object - the latter is
the one that actually regressed and had no prior coverage. Also added a test
distinguishing the shape-error path (enabled holding an object) from a syntax
error, closing the gap the error-message fix didn't have a test for yet.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (2)

app/Commands/Airlock/_AirlockConfig.cs:277

  • Newline detection assumes that the presence of any '\r' implies CRLF ("\r\n"). For files using lone CR line endings (legacy Mac style), this will insert "\r\n" and won’t preserve the existing newline style as intended. Consider detecting CRLF vs CR vs LF based on the first newline sequence encountered.
    if (body.IndexOf((byte)'\n') >= 0 || body.IndexOf((byte)'\r') >= 0)
    {
      newline = body.IndexOf((byte)'\r') >= 0 ? "\r\n" : "\n";
      indent = DetectIndent(body, nextTokenStart);

app/Commands/Airlock/_AirlockConfig.cs:197

  • if (existing is var (start, length)) is a subtle pattern to read correctly with a nullable tuple return type; it can look like it would always match. Using an explicit nullable tuple pattern (or existing.HasValue) would make the null behavior obvious and reduce the chance of future regressions during refactors.
    var existing = FindRootEnabledValue(body);
    if (existing is var (start, length))
      return Splice(json, bomLength + start, length, replacement);

@GordonBeeming
GordonBeeming requested a lite review from Copilot August 13, 2026 16:15
@GordonBeeming

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 402cd22891

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (2)

app/Commands/Airlock/_AirlockConfig.cs:140

  • SetEnabledInJson creates the target directory before attempting to parse the source JSON. If the operation fails (e.g., seeding from a malformed global config), this can still create a new .copilot_here/ directory even though the command reports that no changes were written. Consider deferring directory creation until after JSON parsing succeeds (e.g., create the directory immediately before writing the output file).
    var dir = Path.GetDirectoryName(path);
    if (!string.IsNullOrEmpty(dir))
      Directory.CreateDirectory(dir);

app/Commands/Airlock/_AirlockConfig.cs:199

  • The nullable tuple check if (existing is var (start, length)) is hard to read and can be misinterpreted as an always-true var pattern. Using an explicit nullable-tuple positional pattern (or an explicit null check) makes it clearer that the splice only happens when an existing root enabled value was found.
    var existing = FindRootEnabledValue(body);
    if (existing is var (start, length))
      return Splice(json, bomLength + start, length, replacement);

@GordonBeeming
GordonBeeming marked this pull request as ready for review August 13, 2026 16:28
@GordonBeeming
GordonBeeming merged commit d3b1bb3 into main Aug 13, 2026
33 checks passed
@GordonBeeming
GordonBeeming deleted the gb/fix-airlock-network-json-overwrite branch August 13, 2026 16:30
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.

leaving/entering airgap overwrites network.json

2 participants