Skip to content

Stage 3: end-user setup, PocketIC login e2e, and the dist release pipeline - #152

Merged
aterga merged 32 commits into
mainfrom
claude/secondary-mcp-local-deploy-jpof9i-stage3
Aug 27, 2026
Merged

aterga merged 32 commits into
mainfrom
claude/secondary-mcp-local-deploy-jpof9i-stage3

Conversation

@aterga

@aterga aterga commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Stage 3 of the local-deployment design (docs merged in #148, Stage 1 in #77; stacked on Stage 2 in #150 — merge that first, GitHub retargets this PR automatically): the polish stage — the setup end-user registration command (component 9), the PocketIC end-to-end test of the real login flow (component 3 / Verification), and the dist release pipeline with provenance attestations (components 9/10), plus the crate README with the wallet-grade trust note (component 8).

Related issues

Implements Implementation Stages → Stage 3 of the design docs merged in #148.

Changes

  • imcp2-local setup — one command, no JSON/TOML editing for the user:
    • Detects installed clients and writes each one's own registration: Claude Desktop / Cursor / Antigravity (mcpServers JSON, other keys preserved), Codex (~/.codex/config.toml via toml_edit, comments and formatting preserved), Claude Code (via its own claude mcp add --scope user, which owns that config), Perplexity macOS (UI-only; the steps + JSON to paste are printed).
    • setup --remove undoes exactly those registrations; setup --print only shows the steps; corrupt configs are refused, never overwritten; the first modification of an existing file keeps a one-time .imcp2-bak backup.
    • The binary also grows --help/--version; serving stays the bare no-argument path client registrations invoke.
  • PocketIC end-to-end login (src/e2e_local_login.rs, feature e2e + optional pocket-ic, mirroring the hosted harness's gating): a live II canister issues a genuine canister-signed registration delegation; the login driver binds its real loopback listener; the test plays the browser over real HTTP (pinned page, #4091 allow-list fetch, redeem POST); the driver redeems via mcp_register_v2 on the live II — asserting {"done": true}, the filled session slot, signed-in status with the real principal, "already signed in" on a repeat authenticate, and listener teardown.
  • Release pipeline — dist (cargo-dist 0.31, the ICP CLI's setup): dist-workspace.toml + the generated .github/workflows/imcp2-local-release.yml; five targets (macOS arm64/x64, Linux x64/arm64, Windows x64), shell/PowerShell installers, install-updater = true (one imcp2-local-update upgrades in place), GitHub artifact attestations on every artifact, pinned action SHAs. Tag namespaces stay separate: releases announce only on imcp2-local-v* tags, so the plain v* namespace remains the crates.io publish trigger and a crate release never implicitly ships binaries.
  • crates/imcp2-local/README.md — install, per-client registration, signing in, upgrading, configuration, the wallet-grade security note, and download verification (gh attestation verify … -R dfinity/imcp2).

Deliberately not in this PR (with the design's own reasoning):

  • Apple/Windows code signing + the .mcpb bundle: both need the organization's signing credentials (Apple Developer ID + notarization; Azure Trusted Signing) — the design notes Gatekeeper checks the binary inside an .mcpb, so shipping the bundle before signing exists would produce exactly the blocked double-click the design warns about. The acquisition list and the pipeline hook points are in component 10; until then the installer channels (not subject to the OS gates) are the supported path.
  • The real-browser compatibility gate (automated Chromium/Firefox + manual Safari) and optional keychain persistence remain follow-ups.

Testing

  • cargo build --workspace --all-targets / cargo test --workspace --all-targets — 225 passed, 0 failed (imcp2-local now 16: 5 new setup tests covering the apply→remove round-trip with foreign-key/comment preservation, skip-not-invent detection, print-only mode, corrupt-config refusal, first-run-only backups)
  • The PocketIC e2e was actually run and passes (pocket-ic server 15.0.0 + II release-2026-07-17): the full local login against a live II canister in ~3.8s — and the hosted e2e passes with the same artifacts as a cross-check
  • setup exercised live in this container: --print, apply against a scratch home (correct skips, real claude mcp add execution honoring $HOME, "already exists" refusal surfaced cleanly)
  • dist plan --tag imcp2-local-v0.2.0 plans the full matrix; a real dist build of the Linux artifact produced the archive + updater companion + checksum, and the packaged binary runs
  • cargo check --features e2e --all-targets for both crates; cargo fmt clean on the new files; internal-identifier scan clean over the whole range
  • The generated release workflow triggers only on imcp2-local* version tags (verified pattern), so existing v* crate publishes are unaffected

Checklist


🤖 Generated with Claude Code

https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK


Generated by Claude Code

claude added 22 commits August 25, 2026 16:33
The local half of the design merged in #148: a single-user binary an AI
tool spawns and talks to over stdio, serving the same tool surface as
the hosted server against the same mainnet and production Internet
Identity, with none of the hosted server's OAuth machinery.

imcp2-local (new crate, publish = false; distributed as release
binaries per the design's Stage 3):
- login driver: `authenticate` mints a FRESH session id + registration
  key, binds a transient loopback listener on 127.0.0.1:0, and returns
  the id.ai connect link in-band (plus a best-effort `open::that`
  browser launch), never blocking on the callback. The listener serves
  exactly three routes — the pinned fragment-reading callback page
  (shared with hosted, same CSP + hardening headers), the slim redeem,
  and the #4091 allow-list (CORS-readable, no-store) — and is torn down
  on redeem, on the 10-minute handshake timeout, or when a fresh flow
  replaces it. The redeem mirrors the hosted order (validate state,
  parse before claiming, single-flight claim, one mcp_register_v2
  call) minus the OAuth tail: success answers {"done": true} and the
  page renders "Signed in — you can close this tab" in place.
- wrapper handler: the two local-only login tools live on LocalServer's
  own #[tool_router]; tools/list is the merged surface (26 core + 2),
  tools/call dispatches to the login router first and forwards
  everything else (calls, resources, get_tool) to the inner IcTools
  handler verbatim. get_info identifies as imcp2-local and appends the
  sign-in guidance to core's instructions.
- main: mainnet agent (IMCP2_IC_URL override; IMCP2_FETCH_ROOT_KEY only
  honoured for loopback endpoints — the local-replica test
  configuration), production II (II_URL_PROD/II_CANISTER_ID_PROD
  overrides), management identity derived at the hosted server's origin
  by default (IMCP2_MANAGEMENT_ORIGIN) so the same anchor keeps the
  same controller principal across deployments, IMCP2_NO_OPEN, all
  logging to stderr (stdout is the JSON-RPC channel).

imcp2-core (the two seams Stage 1 left for the login driver):
- SessionSource::Singleton now holds a SessionSlot — a shared,
  REPLACEABLE holder, not a fixed id: the II contract requires a fresh
  session key after Unauthorized, so each successful login writes a
  fresh session id and reauthentication is a same-process step.
- the pinned callback page's script gains the {"done": true} success
  arm (hosted keeps answering `redirect`), with a `.done` terminal
  state in connect.css that drops the spinner without the error line.
- regression guard: the core router must never carry the login tools
  (it IS the hosted tools/list).

Tests: 218 pass workspace-wide (9 new in imcp2-local, 3 new in core),
including a real MCP round-trip over an in-process duplex pipe (merged
tools/list, login lifecycle through tools/call, forwarding into core
tools) and browser-played listener tests (pinned page + CSP, allow-list
verbatim-match + CORS, redeem refusal paths staying retryable). The
built binary answers initialize/tools/list over real stdio. No behavior
change to the hosted server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…le-redeem guard

- authenticate/auth_status now declare the object-rooted outputSchema
  and attach matching structuredContent (one AuthOutput shape for the
  whole sign-in lifecycle), preserving the all-tools MCP contract the
  core surface enforces.
- auth_status reports a live pending handshake BEFORE a live grant,
  matching begin's precedence: after authenticate(refresh=true) the
  truthful answer is "a replacement sign-in is waiting", not
  "signed in".
- The redeem's commit is no longer last-writer-wins: each flow carries
  an epoch, and a stale redeem (claimed, then outlived by the watchdog
  while its mcp_register_v2 call ran) is refused when a NEWER pending
  or completed sign-in exists — while a late success with nothing newer
  around still lands.

Tests: 220 pass workspace-wide (2 new: pending-outranks-grant status,
stale-redeem supersession) with structured-content assertions added to
the MCP round-trip and the outputSchema check extended to the merged
surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…eline

The polish stage of the local-deployment design (docs in #148, the
binary in the Stage 2 PR), in three pieces:

imcp2-local setup (component 9). One command registers this binary with
the AI tools installed on the machine — Claude Desktop, Cursor, and
Antigravity through their mcpServers JSON; Codex through
~/.codex/config.toml edited with toml_edit so the user's comments and
formatting survive; Claude Code through its own CLI (user scope);
Perplexity's UI-only steps are printed. `setup --remove` undoes exactly
those registrations, `setup --print` only shows the steps, corrupt
configs are refused rather than overwritten, and the first modification
of an existing file keeps a one-time .imcp2-bak backup next to it. The
binary also grows --help/--version; serving stays the bare no-argument
path clients invoke.

PocketIC end-to-end login (component 3 / Verification). The hosted
crate's e2e harness, extended to the local flow: a live Internet
Identity canister in PocketIC issues a genuine canister-signed
registration delegation; the login driver binds its real loopback
listener; the test plays the browser over real HTTP (pinned page,
#4091 allow-list fetch, redeem POST) and the driver redeems via
mcp_register_v2 against the live II — asserting {"done": true}, the
filled session slot, the signed-in status with the real principal, and
the listener teardown. Gated exactly like the hosted harness (cargo
feature `e2e` + II_WASM/POCKET_IC_BIN at runtime). Verified here for
real: both the new local e2e and the hosted e2e pass against the same
pocket-ic 15.0.0 + II release-2026-07-17 artifacts.

Release pipeline (components 9/10). dist (cargo-dist 0.31, the ICP
CLI's setup): five platform targets, shell/PowerShell installers,
install-updater = true (one `imcp2-local-update` upgrades in place),
GitHub artifact attestations on every artifact, and pinned action SHAs.
Releases announce on `imcp2-local-v*` tags only (tag-namespace), so the
plain v* namespace stays the crates.io publish trigger and a crate
release never implicitly ships binaries. Rehearsed locally: `dist plan`
for the full matrix and a real `dist build` of the linux artifact
(archive + updater companion + checksum; the packaged binary runs).
Apple Developer ID + notarization and Windows Authenticode hook into
this same pipeline once the organization's signing credentials exist —
until then the installer channels (which the OS gates don't cover) are
the supported path, as the design lays out.

Plus crates/imcp2-local/README.md: install, per-client registration,
signing in, upgrading, configuration, the wallet-grade trust note, and
download verification (gh attestation verify).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…local-deploy-jpof9i-stage2

# Conflicts:
#	crates/imcp2-core/src/tools.rs
…laude/secondary-mcp-local-deploy-jpof9i-stage3
The epoch guard fixed only the local half of the stale-redeem race: II's
mcp_register_v2 REPLACES the anchor's previous grant, so a timed-out
flow's slow registration landing after a newer flow's left II honoring
the old session while the guarded slot kept naming the new one — every
subsequent tool call would come back Unauthorized. A local commit-time
check cannot undo the remote side effect.

Replace the guard with what the race actually needs: a registration
mutex that serializes each redeem's mcp_register_v2 AND its local
commit, across flows. The last registration to settle at II is the live
grant, and under the lock the commit order equals the settle order, so
the slot always names the session II actually honors — in both
interleavings of an old in-flight registration and its replacement. The
per-flow single-flight claim stays (it stops page double-submits); the
epoch machinery goes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…laude/secondary-mcp-local-deploy-jpof9i-stage3
The design's loopback hardening requires it (bind 127.0.0.1 explicitly
AND validate the Host header, the local analogue of the hosted server's
allowed-hosts list), and the listener accepted any Host: a page on an
attacker's hostname that rebinds to 127.0.0.1 could reach the three
routes with `Host: attacker.example`. Every URL this flow hands out
carries exactly one authority — this listener's 127.0.0.1:<port> — so
a middleware now rejects anything else (exact string match, like II's
own allow-list check) before routing. Even without it the routes are
largely inert to a rebinder (/redeem requires the connect state and a
chain targeting the in-process X); this closes the door outright.

Tested with raw-socket requests: a rebound hostname and even
localhost:<port> bounce with 403; the one advertised authority passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…laude/secondary-mcp-local-deploy-jpof9i-stage3
The pinned page's inline script used single-character names (t, cls, c,
r, d); source stays readable — minification, if ever wanted, is a build
transformation, not a source style.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…laude/secondary-mcp-local-deploy-jpof9i-stage3
…lver

The SessionSource enum put deployment knowledge (bearer vs singleton)
inside core's tool layer. Replace it with an injected resolver: core
only asks "which already-validated session does this call act as?", and
each binary owns its authentication end to end, in its own package —
the hosted crate's require_token middleware validates the bearer and
its resolver reads the stashed AuthedSession back (both now defined in
imcp2's auth.rs again); the local binary's login flow fills its
SessionSlot (now defined in imcp2-local's login.rs) and hands core the
slot-reading resolver. Core drops its `http` dependency along the way —
the request-extension read was the only use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…laude/secondary-mcp-local-deploy-jpof9i-stage3
…h the opener

- login.rs (~1000 lines) becomes the login/ module: mod.rs (the driver
  and its state machine), slot.rs (the SessionSlot), routes.rs (the
  loopback listener's three routes + Host validation), tests.rs.
- CONTACT -> CONTACT_EMAIL, saying what kind of string it is.
- The browser opener uses open::that_detached: open 5.x already nulls
  the launcher's stdio either way (so nothing could interleave with the
  JSON-RPC stream), but detached additionally double-forks the launcher
  away off macOS, so it can never hold a thread or a handle on us.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…blind to it

Component 7 now says what the reviewed implementation does: IcTools
asks one injected SessionResolver "which already-validated session is
this call?" and knows nothing else — not the transport, not how callers
are validated, not whether the deployment is single- or multi-user.
Each binary owns its authentication end to end in its own package (the
hosted bearer middleware + AuthedSession in imcp2's auth.rs; the local
login flow's SessionSlot in imcp2-local), and the doc spells out why
the seam is a value-returning callback rather than a middleware slot in
core: real authentication middleware needs its transport, so the
middlewares live with their transports and core receives only their
outcome. Core's dependency profile loses `http` accordingly, and the
concise doc's component 7 matches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…laude/secondary-mcp-local-deploy-jpof9i-stage3
…is PR

- The local binary now runs the same 60-second session reaper the
  hosted server does: expired grants, abandoned sign-in keys, and their
  cached delegations leave memory promptly instead of accumulating over
  a long-lived process (below MAX_PENDING_CONNECTS nothing else scans
  the map).
- The register -> slot/grant commit -> listener-shutdown success path
  now has an in-repository test ON THIS PR: the PocketIC end-to-end
  login (a live II canister issues a genuine canister-signed
  registration delegation; the test plays the browser over real HTTP
  against the real listener) moves here from the follow-up branch,
  gated exactly like the hosted crate's harness (cargo feature `e2e` +
  II_WASM/POCKET_IC_BIN at runtime). Run here and passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…laude/secondary-mcp-local-deploy-jpof9i-stage3

# Conflicts:
#	Cargo.lock
#	crates/imcp2-local/Cargo.toml
…ockfile

The merge commit landed with both branches' e2e module declarations and
an unresolved Cargo.lock — committed without the verification gate
actually running. Full suite + the live PocketIC e2e verified green on
this tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
The reaper removes an expired session's server-side state, but the slot
kept resolving its id — session-requiring tools would then bypass their
"needs an authenticated session" guidance and surface internal errors
(or re-create a keyless session via ensure_session). The slot now
stores the grant expiration II returned and reports NO session the
moment the grant lapses, so unauthenticated is immediate and accurate —
the reaper's 60-second cadence frees memory, it does not gate
authentication. auth_status keeps its Expired reporting via the
driver's retained Grant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…laude/secondary-mcp-local-deploy-jpof9i-stage3
@aterga
aterga marked this pull request as ready for review August 27, 2026 12:27
@aterga
aterga requested a review from a team as a code owner August 27, 2026 12:27
@aterga
aterga changed the base branch from claude/secondary-mcp-local-deploy-jpof9i-stage2 to main August 27, 2026 12:30
Comment thread crates/imcp2-local/src/setup.rs Resolved
@aterga
aterga requested a balanced review from Copilot August 27, 2026 12:35
claude added 2 commits August 27, 2026 12:38
The module header credited comment preservation via toml_edit, which is
not what distinguishes the file edit from `codex mcp add`. The actual
reasons: directory-keyed detection reaches installs whose codex binary
is not on PATH, removal stays idempotent, and the format works on Codex
versions predating the `codex mcp` subcommand.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

Copilot AI 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.

Pull request overview

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

Suppressed comments (3)

crates/imcp2-local/src/setup.rs:468

  • A first invocation of setup --remove can modify an existing JSON config without creating the promised one-time backup; backup_once is called only by the apply path. Create the backup before this write so removal cannot delete a pre-existing imcp2 registration without a recovery copy.
    std::fs::write(path, format!("{:#}\n", root))
        .with_context(|| format!("write {}", path.display()))?;

crates/imcp2-local/src/setup.rs:550

  • The Codex removal path also skips backup_once, so running setup --remove before apply deletes an existing table without the backup promised for the first config modification. Back up the parsed file immediately before writing the removal.
    std::fs::write(path, doc.to_string()).with_context(|| format!("write {}", path.display()))?;

.github/workflows/imcp2-local-release.yml:206

  • The global artifacts produced here (installers and checksums) are uploaded and released, but only build-local-artifacts has an attestation step. This contradicts the PR's claim of attestations on every artifact. Either grant this job id-token/attestations permissions and attest its upload files, or narrow the release guarantee to platform archives.
      - id: cargo-dist
        shell: bash
        run: |
          dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json

Comment thread crates/imcp2-local/src/setup.rs Resolved
Comment thread crates/imcp2-local/src/setup.rs Resolved
Comment thread crates/imcp2-local/README.md Resolved
…moval, tighter verify

- The one-liner path is always single-quoted (POSIX and PowerShell
  literal quoting), so shell metacharacters in a path never expand.
- A refused `claude mcp remove` counts as a failure unless the output
  says the registration was absent.
- Removal now also takes the one-time pre-imcp2 backup before its first
  modification of a config file.
- README: the verify command pins --signer-workflow, and the maintainer
  flow names the protected-tag ruleset for imcp2-local-v* tags as the
  control over what ships.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
Copilot AI review requested due to automatic review settings August 27, 2026 13:05

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.

Suppressed comments (7)

Previously missed (2) — in code that hasn't changed since the last review.

crates/imcp2-local/src/setup.rs:89

  • A non-UTF-8 executable path is accepted here, but every generated registration later uses display()/to_string_lossy(), replacing bytes with U+FFFD and registering a nonexistent command while reporting success. Reject paths that cannot be represented as UTF-8 before constructing Env.
        let exe = exe.canonicalize().unwrap_or(exe);

crates/imcp2-local/README.md:37

  • --remove does not necessarily “undo exactly” what setup added: it intentionally removes the reserved imcp2 entry by name even if that entry predated setup or was later changed, and it does not restore the backup. Describe the name-keyed removal semantics so users do not mistake this for exact restoration.
imcp2-local setup --remove   # undo exactly that

.github/workflows/imcp2-local-release.yml:18

  • The workflow grants contents: write to every job, including the PR/tag planning job that executes a downloaded installer and repository-controlled dist configuration. Only the publishing job should receive release-write authority; default the workflow to contents: read and grant contents: write explicitly to host.
permissions:
  "contents": "write"

crates/imcp2-local/src/setup.rs:457

  • This truncates the live client config in place. A crash or write failure can leave invalid JSON (and newly created files have no backup); a concurrent client read can also observe partial content. Serialize to a sibling temporary file and atomically replace the destination, and use the same persistence helper in the JSON removal and both TOML paths.
    std::fs::write(path, format!("{:#}\n", root))
        .with_context(|| format!("write {}", path.display()))?;

crates/imcp2-local/src/setup.rs:483

  • The removal path also truncates the live JSON config in place, so interruption can leave the client unable to parse any of its registrations. Use the same cross-platform atomic replacement helper as the apply path.
    std::fs::write(path, format!("{:#}\n", root))
        .with_context(|| format!("write {}", path.display()))?;

crates/imcp2-local/src/setup.rs:537

  • The Codex apply path truncates config.toml in place, so a crash or write failure can corrupt the user's entire Codex configuration. Persist through the same sibling-temp-and-atomic-replace helper as the JSON paths.
    std::fs::write(path, doc.to_string()).with_context(|| format!("write {}", path.display()))?;

crates/imcp2-local/src/setup.rs:570

  • The Codex removal path likewise truncates the live TOML file in place and can leave it partially written. Route this write through the shared atomic replacement helper as well.
    std::fs::write(path, doc.to_string()).with_context(|| format!("write {}", path.display()))?;

Comment thread crates/imcp2-local/src/setup.rs Resolved
…8 path check

- All four live-config writes (JSON/TOML, apply/remove) go through one
  persist() helper: sibling temp file + rename, so a crash or full disk
  never leaves a client's config truncated.
- The one-time backup copies to a temp and renames into place, so a
  partial copy can never pass as the valid backup on later runs.
- setup refuses a non-UTF-8 binary path up front instead of lossily
  registering a command that does not exist.
- README quickstart no longer says --remove 'undoes exactly' — it
  removes the imcp2 registrations by name.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
Copilot AI review requested due to automatic review settings August 27, 2026 13:11

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

crates/imcp2-local/README.md:113

  • The release matrix includes Windows, whose cargo-dist archive is a .zip, so this .tar.xz command cannot verify the Windows download. Use a format-neutral placeholder (or show both extensions) so every documented platform can follow the verification instructions.
gh attestation verify imcp2-local-<target>.tar.xz -R dfinity/imcp2 \
  --signer-workflow dfinity/imcp2/.github/workflows/imcp2-local-release.yml

crates/imcp2-local/src/setup.rs:557

  • A valid Codex entry written as an inline table ([mcp_servers] imcp2 = { command = "...", args = [...] }) is not returned by as_table_mut(), so this branch replaces the whole entry and silently drops args, env, and other settings. Handle both regular and inline TOML tables when updating command.
    match servers.get_mut(SERVER_NAME).and_then(|i| i.as_table_mut()) {
        Some(entry) => {
            entry.insert("command", toml_edit::value(exe.to_string_lossy().as_ref()));
        }

Comment thread crates/imcp2-local/src/setup.rs Resolved
…ntries, .zip note

- persist() carries the destination's permissions onto the temp before
  the rename, so a 0600 config never comes back umask-default.
- The Codex upsert merges into an inline imcp2 = { ... } entry too,
  instead of replacing it wholesale; the round-trip test now seeds one
  with env and asserts survival by parsing the result.
- README verify example notes Windows archives are .zip.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
Copilot AI review requested due to automatic review settings August 27, 2026 13:17

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

crates/imcp2-local/src/setup.rs:46

  • Because each flag overwrites mode, setup --print --remove removes registrations even though --print promises that nothing is written and the usage marks these options as mutually exclusive. Reject conflicting modes instead of letting the last flag win.
            "--remove" => mode = Mode::Remove,
            "--print" => mode = Mode::Print,

Comment thread crates/imcp2-local/src/setup.rs Resolved
…g flags rejected

- persist() canonicalizes the destination first, so a symlinked config
  (dotfiles managers) gets its target updated instead of the link being
  replaced by a regular file.
- setup rejects --print together with --remove instead of letting the
  last flag win.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
Copilot AI review requested due to automatic review settings August 27, 2026 13:21

Copilot AI 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.

Pull request overview

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

Suppressed comments (3)

Previously missed (2) — in code that hasn't changed since the last review.

crates/imcp2-local/src/setup.rs:490

  • When retargeting an existing entry, preserving args can leave this registration unusable. imcp2-local serves only with no arguments, so arguments from a previous command are passed to the new binary and make main reject them as unknown. Remove the owned invocation's stale args field while retaining compatible additions such as env.

This issue also appears on line 572 of the same file.

            entry.insert(
                "command".to_string(),
                serde_json::Value::from(exe.to_string_lossy().as_ref()),
            );

crates/imcp2-local/src/setup.rs:516

  • Apply mode explicitly treats an empty JSON config as an empty object, but remove mode parses the same file as JSON and fails. Thus setup --remove reports a failure even though an empty config cannot contain the registration. Handle blank content as the same clean “nothing to remove” case used for a missing file.
    let text = std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
    let mut root: serde_json::Value = serde_json::from_str(&text)
        .with_context(|| format!("{} is not valid JSON", path.display()))?;

crates/imcp2-local/src/setup.rs:576

  • The Codex merge likewise preserves a prior server's args. After changing command to imcp2-local, Codex will still launch it with those arguments, but this binary only serves on its no-argument path and exits on any unknown argument. Clear args in both regular and inline tables when retargeting the entry.
            if let Some(entry) = item.as_table_mut() {
                entry.insert("command", toml_edit::value(exe_text.as_ref()));
                true
            } else if let Some(entry) = item.as_inline_table_mut() {
                entry.insert("command", toml_edit::Value::from(exe_text.as_ref()));

Per review: use `codex mcp add`/`remove` when a codex recent enough to
have the mcp subcommand is on PATH (probed per run); fall back to the
direct, atomic config.toml edit for IDE-only installs with no binary on
PATH and for older CLIs. The fallback respects $CODEX_HOME. The vendor
invocation shares the claude-path refusal contract via one helper
(absent-on-remove is benign, everything else fails the exit code), and
two tests drive a fake codex script through both halves.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
Copilot AI review requested due to automatic review settings August 27, 2026 13:35
@aterga
aterga requested a review from sea-snake August 27, 2026 13:38

Copilot AI 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.

Pull request overview

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

Comment thread crates/imcp2-local/src/setup.rs Resolved
Comment thread crates/imcp2-local/src/setup.rs Resolved
…ent-removal line

- persist() refuses a config symlink whose target cannot be resolved
  instead of silently replacing the link with a regular file.
- A vendor-CLI removal refused as absent now reports 'nothing to
  remove' rather than a refusal, matching the zero exit.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
Copilot AI review requested due to automatic review settings August 27, 2026 13:41

Copilot AI 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.

Pull request overview

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

Suppressed comments (3)

Previously missed (2) — in code that hasn't changed since the last review.

crates/imcp2-local/src/setup.rs:133

  • is_file() also accepts non-executable PATH entries. On Unix, a stale non-executable codex file is treated as an installation; its probe then fails and this command falls back to creating ~/.codex/config.toml, while the equivalent Claude case reports a registration failure. Filter extensionless PATH candidates by executable permission before returning them.
        let candidate = dir.join(name);
        if candidate.is_file() {
            return Some(candidate);

crates/imcp2-local/src/setup.rs:408

  • This misses a valid per-user macOS installation in ~/Applications/Perplexity.app, so an installed client is reported as absent and its connector instructions are omitted in apply mode. Check both the system and per-user Applications directories.
            target: if env.applications_dir.join("Perplexity.app").exists() {
                Target::Manual
            } else {
                Target::NotDetected("Perplexity.app is not in /Applications".into())

crates/imcp2-local/src/setup.rs:512

  • This preserves only the metadata represented by std::fs::Permissions; on Windows that is just the read-only flag, not the file's DACL. Because the temporary file inherits the directory ACL and then replaces the destination, an existing config with a restricted ACL can become accessible to additional users even though MCP configs may contain API keys. Use a Windows replacement API that preserves the destination DACL (or explicitly copy its security descriptor) before replacing it.
    if let Ok(meta) = std::fs::metadata(path) {
        if let Err(e) = std::fs::set_permissions(&tmp, meta.permissions()) {
            let _ = std::fs::remove_file(&tmp);
            return Err(e).with_context(|| format!("carry permissions onto {}", tmp.display()));
        }

@aterga
aterga merged commit 09ac565 into main Aug 27, 2026
13 checks passed
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.

4 participants