Skip to content

release: 6.0.0 - #80

Merged
serialexperimentslainnnn merged 273 commits into
mainfrom
develop
Sep 13, 2026
Merged

serialexperimentslainnnn merged 273 commits into
mainfrom
develop

Conversation

@serialexperimentslainnnn

Copy link
Copy Markdown
Owner

Summary

Promotes develop to main for release 6.0.0 — Claude becomes one with your IDE. Content: pull requests #76 and #79 (the plugin's own MCP servers, Claude God Mode, the live mirror, the IDE tool cards, pull requests and releases through the IDE's account, the guard inside the servers, the restructure, the TypeScript page, the Log view, the coverage suite and the protocol baseline for SDK 0.3.270 / binary 2.1.270, closing #75). Release text: the 6.0.0 block of CHANGELOG.md and RELEASE_NOTES.md.

Replaces #77, closed once develop moved past its head.

Related issue

Closes #75.

Type of change

  • New feature
  • Refactor (no behavioural change)
  • Bug fix
  • Docs / build / CI
  • Security fix

Risk and rollback

Risk: merging publishes 6.0.0 to the Marketplace. The IDE integration is on by default with a documented off switch; the permission surface is unchanged.

Rollback: a user on 6.0.0 stays there until they update; a fix ships as 6.0.1 through the same door.

Checklist

Notes for reviewers

Opened, commented, watched and merged from inside the IDE through the plugin's own pull_request_ops tools.

SessionPersistence takes restore, the open-tab record and the title (rename and the generated one). The forwarders are gone and every caller reaches the owner: cards for pending permissions and edit snapshots, tokens for the counters, diffs.refreshAfterRewind, rollback, lifecycle for isStarting/refreshBootState/dismissLoginCard, turnControl.interrupt, agentScanner.scan, and ClaudeSettings — through the now-public project — for provider, checkpointing and the guard state. The two constants with a single user move there (EXPIRED_TOKEN_NOTICE to ConversationEvents, the control timeout to SessionControlClient).

The detekt baseline is emptied: neither LargeClass nor TooManyFunctions holds on ClaudeSession any more, which was the agreed signal that the split is done.
SessionProcess owns the ClaudeProcess handle, the launch generation, spawning for one generation and what an exit code means; SessionLifecycle keeps the policy — when to start, the gates, the boot state, stop and shutdown. The generation is still bumped at the same points (start, the first line of stop and of shutdown), so a late exit callback is ignored exactly as before. refuseRemoteProject no longer clears a starting flag that is never set when the gates run.
LoginCoordinator keeps the public surface, the route decision and the sign-in state. LoginAttempt (LoginFlows.kt) is the single PTY flow; the card and the dialog are two LoginUi implementations of it, so the two near-identical flows collapse into one. SignInCompletion is the verify-and-vault step. SessionNotifier gains the two notifications with an action that were built inline.

Deltas from folding the dialog flow into the shared one: its code now goes through submitCode, so the 45 s verify watchdog covers it too; the dialog path stores the setup token like the card path always did; and a card sign-in that fails verification no longer raises a balloon on top of the card's own error.
…n the EDT

Pressing Sign in ran resolveEnv() — which sources the user's env script and waits up to 15 s for it — and PtyProcessBuilder.start() on the EDT; verifying a finished sign-in resolved the env on the EDT again. The attempt now starts on the pool and reports back through edt; a cancel that lands while the spawn is in flight terminates the fresh PTY instead of leaving it orphaned, and a second press while one is starting is ignored. EdtAuthContractTest pins both hops.
…reader

The whole read loop sat inside one runCatching, so an exception from a listener callback (onToken writing to a locked PasswordSafe) ended the reader: the code prompt that followed was never parsed, and the flow then reported the exit code as a clean success. Each callback is now isolated and logged at warn, the reader keeps going, and scanning stops once the URL, the prompt and the token have all been seen instead of re-parsing the accumulated output on every chunk. The PTY spawn is injectable so the reader has a test.
cancel() destroyed the PTY and the reader then reported the killed process's non-zero exit as a failed login: the card flipped from idle to an error after the user had cancelled, and the dialog route showed a 'Login failed' balloon right after 'Login canceled'. A cancelled flow now reports nothing.
SessionListing owns 'Open previous session': the newest files with title, first prompt, branch and timestamp. It reads each line through SessionTranscriptReader.parseRecord — the one JSONL line parser, now exposed — instead of carrying its own Json instance, and still stops at the first line that completes the three fields. EntryDTO and SessionRef move to their own file.
ProtocolParser keeps the top-level and system tables and the two status notices. MessageParsers takes assistant, user and stream_event; ControlParsers takes the control frames and the rate-limit event, with control_request going through a subtype table like everything else, so a new control subtype is a row and not another branch in the one function that grows per feature. unwrapToolError had no caller outside the parser and is private.
Msg is its own sealed hierarchy (ui/jcef/Msg.kt) and JcefBridge only parses. SessionControl gains three sealed sub-families — Vuln, Navigation, Onboarding — so the router's else-chain onto three boolean handlers is gone: every when is exhaustive and a new message that nobody handles is a compile error, not a warn at runtime. ChatBridgeRouter is the seven-arm dispatch; each family has a Bridge* handler in ui/. The whitelist and suspension arithmetic moves to GuardWhitelists and SecuritySuspensions as pure operations on State; the live half of a settings-menu toggle is the SettingsMenuApply table; the panel registry and its broadcasts are LivePanels; VulnPromptedActions lives in vuln/ next to what it quotes; pooled payloads go through JcefHost.execBuilt, which skips a disposed browser.

A settings toggle now learns whether the key is known by applying it to the live state once, then persists through update {}, so the block handed to update is pure on State.
The card's file_path comes from the tool input, and View diff read it synchronously on the EDT with no containment check and no size cap: a card naming a file under the home directory opened it in a diff, and a large one froze the UI while it was read. DiffPresenter.readCurrent decides — inside the project root, under 1 MB, absent files diff against nothing — and the read happens on the pool; openDiff is then handed the text it already has.
… the page are their own files

PageAssembly builds the document and its CSP (appNames and CSS_PARTS live there now, which is where the frontend test loader and the guard-view contract read them). PageRoute is the ladder's pure part. SchemePageServer is the custom-scheme registration and its resource handler. PageDelivery is the ladder itself: the watchdogs, promotion, the loopback server and which rung has been proven. JcefHost keeps the browser, the JS query, exec and its queue, the deferred blocks and the load and navigation handlers. util/Edt.kt gains edtNow — run inline on the EDT, hop otherwise — which replaces the host's private copy.
The replacement chat for a closed last tab is built into a component that is not on screen yet. JBCefOsrComponent.addNotify is what creates the browser, and the 2.5 s ready watchdog was armed in deliver(), before that — so on a cold CefApp, or right after disposing the previous browser, the deadline fired before the first navigation could start and promote() walked the ladder. Its last two rungs, INLINE and NOTICE, loaded through loadHTML, which navigates to file:///jbcefbrowser/… and is refused by the host's own navigation guard: ready stayed false, the queue never drained, no script ran — no composer, no tabs, 'loading' forever — until Open previous session built a browser on a warm CefApp that met the deadline.

The watchdog is now armed on the main frame's first onLoadStart, when a browser exists to load; the two dead rungs and the RemoteDevNotice page they served are gone (PageRoute is SCHEME → LOOPBACK → nothing); exec runs nothing in a disposed host and the unreachable JcefHost.dispose() is folded into the Disposer registration; and ChatTabsPanel.close opens the replacement in a finally, so a teardown that throws still leaves a chat on screen.
… switch on

GitAction.behaviour is sealed — InitRepository, Prompt(text), Ide(actionId), Host(run) — and perform() is one exhaustive when over it. The two when(action.id) blocks that duplicated the catalogue as arms (and answered a new row with 'No prompt is wired') are gone: a row now says what it does where it is declared, and a row without a behaviour does not compile. Kind and ideActionId are derived so the catalogue's readers and its test keep their vocabulary. GitInit owns the one process the plugin spawns and the VCS mapping that follows; IdeActionInvoker owns the platform-action invocation. IdeActionApiContractTest follows both.
refresh() was reentrant only by a flag: a second caller arriving while a snapshot was being collected had its callback dropped, and the re-run answered the first caller twice. With two chats open, the one that asked second never repainted its Git card after a branch change. Callers now queue; each is answered by a collection that started after it asked, exactly once.
Four files in main and three tests still carried prose: the mechanism each one held (what UntrustedState disarms and why, why the trust gate leaves the binary paths out, why session-scoped suspensions are keyed per project, what the two reachability scans can and cannot see) now sits in the package maps next to the file it describes. One line in ClaudeProcess was the orphaned half of a sentence the previous sweep cut.
SettingsMenuRows builds the JSON the page draws; JcefSettingsMenu keeps the key vocabulary and applies a toggle to State. Both were one 300-line object with two unrelated reasons to change.
…cefHost

PageLoadHandler holds the main-frame failure verdict and reports three outcomes: started, arrived, missed. installNavigationGuards is the pair of CEF handlers that refuse any navigation off the plugin's own page. JcefHost keeps the browser, the queue and the deferred blocks. PageStateRecoveryContractTest follows the handler.
… session

The two forSession factories take the lambda plumbing out of ClaudeSession, which is left with declarations and its verbs. The injected-lambda constructors stay, because the tests drive both through them.
GuardFixture holds the base policy and the read/bash/edit builders; GuardProbe is the base class the guard tests extend for v/rule/why against their own policy. The same five helpers were copied into 27 files. No assertion changes: every test keeps the policy it had, spelled as basePolicy().copy(...) where it differed from the base.
Thirteen names promised an ASK, a card, a third-party distinction or a per-caller behaviour that the suite deleted releases ago while the assertion underneath said DENY. The verdicts are untouched; the names now say what is asserted, so the next reader does not trust the sentence over the code. SecurityRuleFamiliesTest also joins the shared fixture, the one file the previous commit missed.
…ALLOW'

Eight fuzz tests asserted only that a vector did not pass, so a vector caught by the wrong rule — whitelistable under a different label, reported under a different reason — read as green. Each now asserts DENY and the rule (or, where the bases mix families on purpose, the small set of rules they belong to). The unreadable-script fuzz pins PRIVILEGE_ESCALATION for its sudo variant, because the command families are judged before the script is opened; that order is the guard's, and the test says so.
…each

Sixteen assertions across four files repeated the guard's own reason wording as a literal, so a change to one word broke sixteen tests that were not about wording. GuardReasonWords holds each load-bearing fragment once; the production strings stay literal, because they are the copy the user reads. Where an assertion was really about which rule answered, it already asserts the rule.
Twenty assertion pairs asserted the same input twice against the same policy,
left over from the trusted/untrusted split that no longer exists; the second
copy could never fail on its own. The fuzz variant that aliased one payload
under two names goes with them, as does a test whose two lines were already
asserted verbatim elsewhere. The stronger-rule test now pins the rule instead
of a verdict two other tests already pin. No expectation changes.
GuardCardMandatoryTest stubs the guard's verdict and drives PermissionBroker
with it: card, refusal, bypass row, per-command approval. It runs no rule, so a
name suggesting it proves cards are mandatory under the real guard overstated
it. The stubbed verdict is the right seam for the broker and stays; only the
name changes.
It reached a private field of GuardPaths and asserted its maximum pool size sat
in 1..32 for a value of 8: a test of an implementation detail with a range wide
enough never to fail. The invariant it stood for, that a hung mount cannot
freeze evaluate() or pay a timeout per path, is what the budget and timeout
tests beside it already assert against observable behaviour.
The refactor split left three references that never compiled, uncaught because
the tree was written ahead of the gate: GuardWhitelists.all returned a Set where
its signature promised a List, JcefChatPanel referenced SettingsMenuRows without
importing it, and JcefAccountData read auth status off ClaudeSession directly
when it now lives on lifecycle.auth. Main sources compile again.
Case folding in GuardPaths.under read System.getProperty(os.name) from a frozen
val inside an object, so Windows and macOS containment were unreachable on Linux
CI and the drive-rooted branch had no test. The choice becomes Policy.caseInsensitivePaths, threaded through under() and supplied by SettingsSensitivePolicy as SystemInfo.isWindows || isMac. The Linux default is false, which reproduces the os.name behaviour on the CI host exactly, so no verdict changes.

WindowsVectorCorpus and GuardWindowsContainmentTest exercise the branch: case-folded drive roots, a UNC project root, backslash-home normalisation, and one parameterised vector per rule family in Windows spelling, each pinning the rule the guard gives today.
The model list moved to session.catalog in the Phase A split; the integration
test still called the removed session.modelOptions(), so the test tree did not
compile. It now asserts over catalog.models, which is where the binary-reported
models live.
The script-launch scanner split a command on the bare pipe character, so a
clobber redirect (a greater-than immediately followed by a pipe) was torn in
two. Its target became the first word of a second segment, matched as a
launched executable, and refused as SCRIPT_EXECUTION when it is really a file
write. With the pipe no longer split out of the clobber operator, the target
stays in the verb's segment and the write is judged by ShellFileWrites, which
already understood the operator. Denied before, denied now, under the rule that
fits. Found by the hardened mutation fuzzer.
LOCAL_ASSIGN required a separator immediately before the variable name, so the
cmd assignment form with a quoted name left the variable unbound: the name sits
behind the opening quote. A later percent-reference to it was then something the
guard could not follow, and an ordinary Windows one-liner drew an
UNRESOLVED_VARIABLE card. The pattern now also binds a name introduced by set
with an optional quote. This can only remove a name from the unresolved set,
never add a threat: a bound value is still judged by every rule. A variable
nothing here defines still cards, as the test holds.
… the guard sees an own call flat

The guard reads a tool call's path and content keys at the top level,
which is where the native tools carry them; an own call carried them one
level down, under args, so a write judged through the meta-tool did not
look like a write to it. OwnTools.guardInput lifts the arguments beside
the tool name before either gate (the server's GuardGate and the
session's broker) evaluates the call, so both shapes get the same
verdict. Independently, the tools that create or move files now refuse a
destination outside the project root, as delete_file and the VCS write
tools already did: write_file, create_file, replace_text, insert_text,
move_file, file_from_template and worktrees add. GuardGateTest pins the
nested write with a probe that reports the target missing.
CodeQL on the release pull request: the per-session socket directory was
created owner-only, but its parent under the system temp directory took
the default permissions, so other local users could list the session
ids. The parent is now created owner-only and, when an older build left
it open, tightened on the next start.
…instead of opening the browser

The GitHub plugin replaces the list with the request's details inside
the same content, so the second open of the same number found no list
row and fell back to the browser. When no row is found, the tool now
looks for the component of the Pull Requests window whose context
enables Refresh Pull Request and fires it there (shown=refreshed); the
browser is only for an IDE without the view.
A Unix socket takes the process umask at bind, which the plugin does not
control; the owner-only directory around it was the only thing keeping
other local users from connecting. The endpoint now sets the socket to
rw------- right after bind, so the file defends itself whatever the
umask, and ServerEndpointTest pins it.
…still without the focus

ToolWindow.show marks the window visible but leaves another window
selected on the same side, so 'show me the pull request' answered shown
while the user kept looking at Bookmarks. Every reveal of a tool window
now activates it with autoFocusContents=false: the window takes its
place in front like a selected tab, the keyboard focus stays where it
was, and the focus contract keeps refusing activate(..., true).
…n it is already open

View Pull Request opens the request as an editor tab named by its
number; asked again while that tab sat behind others, the tool refreshed
the details panel instead of putting the tab in front, which is what
'show me the pull request' means. The tool now selects the open tab
first (openFile without focus, like a source file), opens it from the
list otherwise, and answers none rather than refreshing anything.
…without its defaults

The models were only ever decoded, so the encoders the serialization
plugin generates never ran and the package sat under the branch floor.
Each model is now encoded and decoded back, full and empty, under the
protocol's Json and under one that omits defaults; the usage window and
rate-limit helpers get their branch cases too.
OsvScanner reached OsvHttp through a plain call, so nothing could stand
in for the network and its 45 lines were never run. The call is now an
injected function, OsvHttp.post by default, and the test drives the
batch, the hydration, the silences, a malformed answer, cancellation at
both points and the cut above the hydration cap.
…udit reads it first

EnvScriptLoader.load ran a shell nobody tested, and SourceScriptAudit
had no test at all. A temporary script is now sourced on POSIX and its
export comes back; a blank, missing or exiting script yields nothing;
the audit passes a harmless script, names a destructive one and ignores
what it cannot read.
…ugh a real control client

SessionQueries and RemoteControl were only reached through the live
session. They now run against SessionControlClient with a fake
scheduler: each ask writes its subtype and decodes its reply on the EDT,
a stopped session answers nothing, the usage reply feeds the quota
warnings, and Remote Control records state, error and the notice for
every outcome. The protocol round-trip test takes the formatter's shape.
…d the gate probes the stand-in binary

The login coordinator built its attempt and the attempt built its PTY,
so no test could stand between them. Both take a factory now, the real
ones by default. A scripted process plays the binary's own output: the
url and the code prompt reach the ui, the pasted code reaches the pty, a
verified login restarts the chat, a failed one carries the binary's
wording, cancelling forgets the attempt, and another provider is told to
use its key. The gate's probe, renewal short-cuts and the dialog login
ui are driven against fake-claude and the test dialog manager.
…d tool window

A Pull Requests tool window with a list of rows is registered in the
headless fixture: the request's row is selected and yields the list's
context, an unknown number yields none, the window opens with and
without focus, and an open timeline tab is selected while a missing one
is reported.
…un against the headless IDE

A target's parsing and the contexts it yields (a file with its editor,
PSI and caret element, a directory, the project), the reveal of a file
and of a registered tool window and its content, an action dispatched
and a toggle flipped through IdeActions, and every verb of the card
places against the windows the fixture has and lacks.
…eveal test stops depending on test order

NoticeNarrator had no test of its own: status notices, notifications
loud and quiet, denied permissions with their best reason, recalled
memories, persisted and failed files, plugin installs by status, both
refusal shapes, informational notices by level and the logged rest. The
reveal test no longer asserts on windows another fixture may have
registered in the shared light project.
…nd the coverage gate measures what can run

MarketplaceGateway takes its fetch as a parameter, the HTTP one by
default, so the update rows, their defaults, a dead endpoint and a
reply that is not a list are all tested; GitHubAvailability runs
headless with and without the plugin. Two corrections to what kover
measures: the uiTest source set was read as production code and showed
up as a package at zero, and GitHubGateway joins the gateways that need
a live plugin, an account and the network, on the same grounds as the
database and OSV ones. No floor moves.
…code-for-jetbrains into feature/release_6.0.0
…nary 2.1.270

checkDrift flagged four control subtypes the SDK now declares and the
plugin did not acknowledge: get_hooks_listing, list_permission_rules,
reload_output_styles and update_settings. None of them backs a plugin
feature yet, so they join KNOWN_SUBTYPES the way list_models did instead
of getting a request builder nothing calls; the reachability contract
rejects a member without a caller.

The SDK also stopped declaring generate_session_title, get_plan,
get_workspace_diff and side_question. The plugin still sends all four
(session titles, the Plan view, the workspace diff and /btw), so they
stay known until the binary refuses them at runtime.
@serialexperimentslainnnn

Copy link
Copy Markdown
Owner Author

This Release is being driven by Claude Code Native Plugin.

Comment thread src/main/kotlin/dev/lain/claudejb/controller/mcp/IdeMcpService.kt Dismissed
@serialexperimentslainnnn

Copy link
Copy Markdown
Owner Author

CodeQL java/local-temp-file-or-directory-information-disclosure on IdeMcpService.kt:78 — dismissed as a false positive.

The query treats Files.createFile/createDirectory/createDirectories with a PosixFilePermissions attribute as sanitised and flags the one-argument forms. The only one-argument sink on this flow is the non-POSIX branch of SocketHome.create.

  • POSIX (Linux, macOS): the parent claude-ide-mcp and the session directory are created rwx------ (attribute plus setPosixFilePermissions, which fails closed if another user pre-created the parent), the token file rw-------, the socket rw------- after bind. Nothing under the temp directory is readable by other local users.
  • The flagged branch runs on Windows only, where java.io.tmpdir is the per-user %LOCALAPPDATA%\Temp with the profile's ACL, not a shared directory; and it is the second candidate after PathManager.getTempPath(), used only when the IDE's own temp path does not fit the Unix socket path limit.

An explicit owner-only ACL for that branch is tracked for 6.0.1.

… target IDE lacks

The develop-to-main release PR went red on Plugin verifier with all eight
targets Compatible: PyCharm has no com.intellij.modules.java, the
optional dependency behind the uast domain, and the IntelliJ Platform
Gradle plugin (2.16.0, 2.18.1 alike) parses the verifier's output by the
"Missing dependencies" heading without telling an optional gap from a
mandatory one. With FailureLevel.MISSING_DEPENDENCIES on, every optional
dependency a target lacks is a failed verification.

The protection that level gave, a mandatory dependency the IDE cannot
satisfy means the plugin never loads, moves to
PluginDependenciesContractTest: every non-optional <depends> must be a
platform module every IntelliJ-based IDE ships, every optional one names
an existing config-file, and the level stays out of failureLevel until
the Gradle plugin ships its "missing mandatory" verdict marker.

verifyPlugin now passes locally with the Java dependency declared: 8 of 8
Compatible, 42 experimental API usages, no deprecated or internal API.
…y layout

The release PR to main failed Build plugin with 121 legitimate entries
listed as unexpected: the allowlist admitted only flat jcef/*.js|html
and jcef/css/*.css, and since 6.0.0 the web app lives in directories by
family (controllers, core, models, views, css/views/...). The pattern
now accepts subdirectories while still demanding the js, html and css
extensions, so a stray map, doc or fixture under resources/jcef keeps
failing the build. Checked against the local 6.0.0 zip: 0 unexpected
entries, 126 jcef files, all js, html or css.
@serialexperimentslainnnn
serialexperimentslainnnn merged commit 221cde5 into main Sep 13, 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.

2 participants