Repository navigation
feat(mcp): add a zero-execution action flight plan - #1380
Mihir-Rabari wants to merge 46 commits into
Conversation
|
Thanks Mihir-Rabari — the isolated feature commit I’m converting this PR to draft because the submitted head is not safe to merge as-is:
Please rerun the focused |
c93ffcd to
20a3778
Compare
|
Addressed the review on the current dev base. The branch now contains the isolated flight-plan cherry-pick plus a focused fix commit (20a3778), with no unrelated migrations. The forecast marks only PersistedPolicyUnreadable as blocking; regression tests cover both write-failure fault types and the unreadable fault. The status item now gates on all registered tools, with a test for an all-blocked registry and the empty state. Local Gradle tests could not start because this checkout lacks cached JxBrowser, SQLite JDBC, and Compose icon artifacts; CI is running against the pushed head. |
bdcf620 to
bc9aa3b
Compare
|
Final update: CI on the rebased head 37d3821 passed the macOS, Ubuntu, and Windows build-test jobs, code quality, compatibility, database, contracts, version, and shell checks. The branch is ready for another review. |
37d3821 to
ff1b71f
Compare
|
Rebuilt on current dev (bc9aa3b): the branch now carries only the isolated flight-plan series, 10 files (+672/-2) covering the McpFlightPlan model, dialog, launcher, view, the activity-log hook, and focused tests. The unrelated 139-file aggregate diff is gone. Fresh CI is starting on ff1b71f; I will post the full matrix when it completes. |
|
Fresh CI on ff1b71f: build-test (macOS/Ubuntu/Windows), code quality, compatibility, database, contracts, version, and shell checks all passed. Ready for re-review. |
Added McpApprovalScope enum and related functions for approval flags and labels. Implemented McpApprovalDialog composable to handle user interactions for tool approval requests.
…ApprovalCountdown.kt) Implement McpApprovalDialog for AI agent tool approval with various scopes and user interactions.
…to-dev Sync main into dev
risa-labs-inc#1594) * fix(cli): keep an & in `boss <anything>` from running a second command boss.bat's smart detection tested the argument with `echo %arg% | findstr`. cmd substitutes %arg% into the line before it parses the line, so an & in the argument ended the echo and ran the rest as its own command, once per detection line. A file named R&D.txt routed nowhere, and `boss "x&echo side-effect>marker.txt"` wrote marker.txt. The "could not determine type" echoes read %arg% unquoted too. The URL and TLD checks are now quoted string tests (a prefix substring and a case-insensitive substring replacement), with no subshell, and the error echoes quote the argument. Routing is otherwise unchanged. test-boss-bat-urlencode.ps1 gains a source-shape check that :detect_and_route reads %arg% only inside quotes, which the Ubuntu leg enforces and which fails on the old script, plus live cmd.exe probes: the payload creates no marker, R&D.txt routes to boss://file with its full path, URL and domain detection give the same answers, and a mutation check shows the old findstr line does run the payload. Follows up risa-labs-inc#1347, whose empty-path half risa-labs-inc#1059 already fixed. * boss.bat: drop literal ampersands from REM comments Reword the two REM lines so they don't contain a bare & (flagged in review as a possible command split). Comment-only change. --------- Co-authored-by: fluck-boss <62e43a68001@mail.instinct.com>
…lways Allow (risa-labs-inc#1613) * fix(mcp): ask before a destructive shell command even under a saved Always Allow A saved ALLOW on run_command (or any shell tool) went straight to AUTO_ALLOWED, and the risk evaluator only ran on the ASK path to label the dialog. So one "Always Allow" for a harmless command auto-ran every later call, including rm -rf, git push --force and mkfs, which the evaluator itself rates CRITICAL. - McpToolRegistryCore.invoke now passes the saved policy through askBeforeDestructiveShell: an ALLOW on a shell tool drops to ASK when the call's own args rate CRITICAL. Every shell call already rates HIGH, so CRITICAL is the line that keeps "Always Allow" meaning "don't ask for routine calls". The args evaluated are the ones executed, and shell tools are identified by the evaluator's own normalization (the new DefaultMcpRiskEvaluator.isShellTool). A provider-wide "Trust This Plugin" ALLOW is covered too. Non-shell tools, DENY and ASK are unchanged. - The destructive-command matcher decides this now, so it reads each command of a chain for the flag shapes real calls take: rm with any recursive flag in any order or path (rm -fr, rm -r -f, /bin/rm -R, sudo rm --recursive), del/erase/rd/rmdir /s, Remove-Item -Recurse, and git push with a force flag anywhere after push. Runs of whitespace are collapsed. Every existing pattern still matches. - WorkspaceMcpToolProviderTest's newline check now uses a benign second line: its old `rm -rf /` payload is stopped earlier, at the approval gate, under that test's provider-wide ALLOW. Fixes risa-labs-inc#1577 * test(mcp): pin newline-chained and operand-first recursive rm as CRITICAL Add additional cleanup commands to the risk evaluation test. * fix(mcp): split shell chains before collapsing whitespace, read rm flags after operands * fix(mcp): escalate on >= CRITICAL so a higher tier stays fail-closed --------- Co-authored-by: fluck-boss <62e43a68001@mail.instinct.com>
…ot win (risa-labs-inc#1027) (risa-labs-inc#1622) * fix(browser): take ResolvedHostsStore save snapshot under saveLock `recordLoaded` captured `hosts.toList().sorted()` on the calling thread and only acquired `saveLock` inside the launched save. Two recordLoaded calls landing close together both captured the same snapshot, raced for the lock, and whichever won LAST overwrote the newer in-memory state with the older snapshot - silently dropping the host added by the first save. Moving the snapshot inside `saveLock.withLock` keeps the in-memory set (already a ConcurrentHashMap.newKeySet) as the source of truth while making the on-disk write reflect everything the caller could possibly have added by the time the lock was taken. Fixes risa-labs-inc#1307 Co-authored-by: Claude <noreply@anthropic.com> * fix(ci): ktlintFormat ResolvedHostsStore and its test * test(browser): make ResolvedHostsStore tests fail on the pre-fix race; correct comments The regression tests each ended with a fresh full-snapshot saveNowBlocking(), which masked the out-of-order write they were meant to catch, so all passed on the old call-site-snapshot code. Rework them to observe what the launched production saves actually leave on disk via a new test-only awaitPendingSaves() seam (no forced full write): a two-writer 'older never wins' test and a concurrent burst, both of which fail on the pre-fix snapshot and pass with the snapshot taken under saveLock. Also correct three production comments that overclaimed: the ConcurrentHashMap toList() is a weakly-consistent iteration, not a stable snapshot; there is no debounce; and saveNowBlocking is not the production write path (it runs inline). --------- Co-authored-by: choksi2212 <manaschoksiwork@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
risa-labs-inc#1527) * fix(terminal): queue concurrent send writers instead of fail-fast spin TerminalSession.send's tryLock-fail path now queues writers on a kotlinx.coroutines Mutex for a bounded 5s wait (injectable via inputQueueTimeoutMillis); callers that outwait the bound are shed with RESOURCE_EXHAUSTED carrying a google.rpc RetryInfo retry-delay via StatusProto instead of an instant spin-inducing failure. The queue wait suspends rather than parking a Dispatchers.IO thread, so a stalled pipe cannot pin the pool under load, and coroutine cancellation abandons a queued write instead of letting it through after the caller gave up; cancellation surfaces as CANCELLED. * fix(terminal): keep the NUL check on environment variable names Added additional null character check for environment variable values. --------- Co-authored-by: Raghav Bansal <273537979+Raghav-Bansal-15@users.noreply.github.com> Co-authored-by: fluck-boss <62e43a68001@mail.instinct.com>
Co-authored-by: kshivang <kshivang@users.noreply.github.com>
…abs-inc#563) * fix(wizard): skip a plugin only when it is actually installed, not merely recorded The first-run wizard's skip guard called DynamicPluginManager.isInstalled, which is pluginStates.containsKey. An entry is not a usable plugin. installPlugin registers a DISABLED entry for a jar it rejected as binary-incompatible and then deletes that jar, so the wizard announced "already installed, skipping" for a plugin that was not there, added its id to installedIds, and finished reporting success for something it never installed. The user picked it during setup and did not get it. This is the trap AGENTS.md already records for the dependency prompt, at a call site that was not converted with it. The same file was already using the correct predicate after the loop, for the dependency report. It now uses PluginDependencyResolution.installedAndOnDisk, the codebase's single definition: LOADED, or the jar exists and the plugin is not recorded as binary incompatible. Both clauses carry their weight here. Requiring only an entry is the bug above. Requiring only the jar would refuse to skip a running plugin whose file has moved, because neither PluginJarReconciler nor the updater repoints the manager's jarPath, and reinstalling it fails with "already loaded". Recomputed per call rather than hoisted, since the loop installs plugins and a snapshot taken before it would go stale exactly when a later entry depends on an earlier one. Six tests, written around the cases where the two predicates disagree, since the cases where they agree would pass either way. The last one pins the disagreement as a set difference, so a future change to either definition shows up as a changed disagreement rather than silently converging. 4405 composeApp tests pass. ktlint and detekt clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wizard): reject unusable install results before reporting success * fix(wizard): refuse to overwrite an installed plugin, rather than roll back Takes the bounded alternative offered in review, and collapses four commits of my own CI repairs into the change they were repairing. **Refuse an occupied destination.** Nothing already at the installed path is replaced. The previous attempt moved over it and rolled back on failure, and that grew a tail of its own failure paths: a backup that will not rename, a promotion that throws between backup and install, a restore that fails silently, a fixed-name backup that can destroy the only original left by an earlier failure. Each could lose the artifact the rollback existed to protect, and each needed an injected file-operation seam to test. Refusing has no tail: nothing is overwritten, so nothing needs undoing. It also makes cleanup unambiguous, because everything at the destination afterwards belongs to this call. **The signature travels with the jar.** `RemotePluginRepository` writes the sidecar beside the DOWNLOAD path, so promotion is when it has to move. Moving only the jar left every signed store install loading unsigned while its signature sat at the staging name. `SignedArtifact` moves and deletes the jar with whichever sidecar exists, carrying both suffixes the loader knows. **Both downloads stage.** The Store path used a fixed `-downloading.jar`, which a directory scan reads as a plugin and which two installs of one id collide on. Both paths now use a unique name whose marker breaks the `.jar` suffix, with one constant defining it so writer and reader cannot drift. `isPluginResident` answers what `isInstalled` cannot: that reports whether an entry exists, while `disablePlugin` leaves a DISABLED plugin resident in the loader, which is the case that made this reachable at all. 11 tests over real files and real moves: an occupied destination refused with its bytes intact and the loader never reached, a resident plugin refused before anything moves, the signature arriving with its jar, a failure removing both and leaving no orphan sidecar, a throwing loader not escaping the cleanup, and the staging name being unique, unscannable and reversible. Rebased onto current dev, which has now been rewritten four times today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wizard): make promotion all-or-nothing, and refuse the whole footprint Review found that refusing an occupied JAR does not make promotion indivisible, and it was right. `SignedArtifact.moveTo` moved the jar and then its sidecar, and `stageAndInstall` called it BEFORE the try/cleanup block. If the second move failed - the final `.sig` path being a nonempty directory is enough, and a permission change between the check and the move will do it too - the jar was left at its scannable installed name with its signature stranded at the staging name, nothing cleaned it up, and the occupied-destination refusal then blocked every retry. Three changes: **Promotion is inside the failure boundary.** `moveTo` records each path as it lands and deletes exactly those on failure, so a partial promotion leaves the destination as it was found. Deleting rather than moving back is deliberate: every path was established free beforehand, so deleting cannot destroy an artifact this call did not create, whereas a move back has a failure of its own that would leave the same mess. What is lost is a download, which the retry re-fetches. The loader is never reached. **The refusal covers the whole footprint**, not just the jar. A stale `.sig` beside a jar removed by hand is still somebody else's file, and promoting an unsigned download next to it would leave an old signature asserting bytes it never vetted - which PluginSignatureSidecar's own contract calls out as the thing never to do, and which is worse than no signature because the loader reads it as present-and-invalid. Refusing also means every path this call writes was established free, which is what makes the cleanup precise rather than indiscriminate. The message names the obstructing file so the user knows what to remove. **The destination-ownership claim is corrected, not restated.** The check and the move are two operations, so it holds against what is already on disk, not against a second installer racing this one. `ATOMIC_MOVE` cannot close that gap: on POSIX it is rename(2), which replaces the target silently - verified with a probe on this platform rather than assumed. Promotion therefore uses a plain non-replacing move, so a racing installer collides with FileAlreadyExistsException and this call reverts, instead of the two overwriting each other silently. Serialising installs is the only thing that would make the race impossible, and that does not belong in this function; the KDoc says so rather than implying a guarantee that is not there. Three regression tests over real files and real moves: a blocked destination sidecar (loader not called, no jar at the installed name, the obstruction and its contents untouched, and the retry succeeds once it is removed); an unsigned download meeting an old sidecar; and a promotion onto an occupied path failing rather than replacing it. Reported-by: kshivang Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(test): call the two-argument assertContains, not the Iterable overload `assertContains(charSequence, other, message)` binds the message to `ignoreCase: Boolean`, so overload resolution falls through to the Iterable and array candidates and the call does not compile. The message moves to a comment; the assertion is unchanged. This was already fixed in my working tree when 3d7f888 was pushed - the amend that should have carried it was part of a compound command that was denied, and I pushed the pre-fix commit without re-checking. The commit, not the working tree, is what CI builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Shivang <shivang.iitk@gmail.com>
…lag-shaped branch cannot reach git checkout (risa-labs-inc#950) * fix(git): re-validate the remote-prefix-stripped checkout name so a flag-shaped remote branch cannot force-discard uncommitted work Fixes risa-labs-inc#919. checkout() ran isSafeRefName on the name as it arrives, but for a non-local name containing '/' it derives localName = substringAfter('/') and passes THAT to 'git checkout <localName> --' without re-validating. A remote-tracking branch literally named '-f' (origin/-f) passes the guard, strips to '-f', and the argv becomes [checkout, -f, --]: git parses -f as --force with an empty pathspec set and force-checks-out HEAD over the worktree, silently discarding every uncommitted modification. The post-strip name now passes the same isSafeRefName gate before it can reach the command line. 2 regression tests with a real lab repo: a planted origin/-f ref is refused with the PRECIOUS uncommitted content intact (and a safe slashed name still checks out carrying the change over), and a local branch whose whole name is a flag is refused on arrival. * fix(git): pass the window's project path on the top-bar checkout and pin the empty-strip and HEAD-stability contracts Re-audit follow-up on risa-labs-inc#919: the top-bar onCheckout was the one write verb of its cluster that omitted projectPathOverride, so with two windows on different repos the branch picker's checkout ran against whichever window last aligned the global current-project path - the wrong repository. It now passes windowProjectPath exactly like its neighboring pull/push/merge/rebase call sites. Two regression tests join the suite: 'origin/' strips to empty and is refused with the worktree intact, and a refusal leaves HEAD and the uncommitted content untouched. 4/4 tests, detekt and ktlint green.
|
@claude please review this exact head (ff1b71f) for merge into dev. Check that the change is correct and complete for its stated goal, that tests cover the risk being fixed (a test that passes without the fix is a finding), and flag any security issues (secrets, injection, auth or approval bypass, path handling). Keep the review focused on this diff. |
|
Claude finished @fluck-boss's task in 6m 5s —— View job Review — PR #1380 @
|
…risa-labs-inc#987) * perf(db): evaluate statement-constant policy calls once per statement Supabase's advisor reports 68 policies that call auth.uid() or auth.jwt() per row. Writing the call as a scalar subquery lets PostgreSQL hoist it into an InitPlan, which is the documented fix and cannot change which rows a policy admits, because both calls are STABLE. Rewrites 80 policies across 29 tables, hoisting 86 call sites. Twelve of them are authorize('literal') and twelve are is_user_admin(auth.uid()), neither of which the advisor flags: it reads the policy expression but not the body of a function the policy calls. For is_user_admin the advisor's own remediation text is actively weaker, since rewriting only the inner auth.uid() leaves a plpgsql SECURITY DEFINER call running once per row. Thirty-three call sites are left alone because they take a column, so their answer differs per row and hoisting them would be a security bug. * test(db): pin cmd/permissive and add real row admission checks arjun28115's review on risa-labs-inc#987 flagged two gaps. The roles-pinned assertion never checked polcmd/polpermissive, so a future DROP/CREATE that flips a policy to RESTRICTIVE would pass silently - added both to the same 80-row pinned list. Assertions 7-9 also never selected a row under a claim, so they couldn't tell if the rewrite changed admission - replaced with two real SELECTs under authenticated (plain user sees 1 of 2 fixture rows, admin claim sees both). plan(9) -> plan(8). * fix(db): hoist the four terminal_sessions policies added after this PR dev gained public.terminal_sessions (20260921120000) with four owner policies that call auth.uid() once per row. Rebuilt on that dev, assertion 1 (every statement-constant call in every public policy is hoisted) fails on exactly those four, and the policy count moves from 110 to 114. 20260922130000 applies the same ALTER POLICY rewrite to the four. The pinned lists go from 80 to 84 policies, the count to 114, and two assertions are added: the four hoisted functions are STABLE or IMMUTABLE (9), and a user sees only their own terminal session through the rewritten policy (10). The header rule now says the function must be STABLE or IMMUTABLE, and why is_user_admin(auth.uid()) fits, as said in review. * fix(db): date the InitPlan hoist migrations after dev and the open PRs 20260918140000 and 20260922130000 sort before 20260922160000, the newest on dev, so `supabase db push` against an environment that has applied it would refuse them. Renamed to 20260923150000 and 20260923151000, after dev and after every 20260923 migration in the open PRs (the latest is risa-labs-inc#993's 20260923140000). The SQL is unchanged. The terminal_sessions migration explained its separate file by the first one running before that table existed, which is no longer true once both run after 20260921120000. It now says why they stay apart. The comments and the test header name the new timestamps. * fix(db): move the InitPlan hoist migrations off risa-labs-inc#1355's timestamp 20260923150000 is also the version of risa-labs-inc#1355's 20260923150000_plugin_download_serve_rpc.sql, the stamp the maintainer gave that PR before I picked the same one here. The two files are now 20260923160000 and 20260923161000, after dev's newest (20260923140000) and after every migration in the open PRs. The SQL is unchanged; the comments and the test header name the new stamps.
…, not URI.path (risa-labs-inc#1340) * fix(windows): resolve the code source through the filesystem provider, not URI.path `codeSource.location.toURI().path` returns only the URI's path component, and a file on a Windows network share does not keep its server there - the server is the URI's authority. `File` then resolved what was left against the current drive, so a share install computed `C:\software\BOSS\app\BOSS.jar` for `file://nas01/software/BOSS/app/BOSS.jar`. Nothing threw and nothing logged. Four callers acted on that path: the `boss://` protocol command and `shell\open\command` for every registered extension (both written straight into the registry), the StartMenuInternet browser candidate, and the JAR updater, whose `exists()` check failed so the update silently never ran. Three more used `File(uri)`, which rejects an authority outright and failed safe to null, costing the restart and the launcher their code-source fallback. One helper, `CodeSourceLocation`, replaces all seven. `Paths.get(uri)` hands the URI to the platform's filesystem provider, which restores the authority as a UNC root, and returns exactly the previous result for a drive-letter install - so a local install sees no behaviour change. Routing through it also lets WindowsDefaultBrowserHandler reach its development-executable lookup when the code source cannot be resolved, instead of giving up on the one case that shape describes. * fix(windows): answer the risa-labs-inc#1340 review - localhost, fail closed, log, hidden shares - CodeSourceLocation drops a `localhost` authority before Paths.get, as OsOpenArguments.pathFromFileUrl does: the Unix provider rejects every authority, so file://localhost/... resolved to null where URI.path answered it. A real server authority is untouched. - Each null from fileOf is logged at debug with its reason (no URL, the scheme, or the provider's exception class), never the path. - WindowsDefaultBrowserHandler fails closed on an unresolvable code source, as WindowsProtocolHandler does, instead of registering a BOSS.exe found under user.dir. The dev lookup stays for a code source that resolved to a classes directory. The logged path is masked. - UpdateInstaller: drop the dead try/catch around fileFor and its stale KDoc, reword getCurrentJarPath's message, and document that a hidden share (`apps$`) installs without a relaunch. The validator refuses `$` anywhere, which jpackage.app-path already hit before this change. - Tests: a localhost URL, a network-share install resolved end to end from its code-source URL through windowsLauncherPathFor, and the hidden-share outcome pinned.
…-labs-inc#1638) It is SECURITY DEFINER, takes a caller-supplied user id and checks nothing, so any signed-in user could ask how many active API keys any other user holds. The table already refuses that question: plugin_api_keys has RLS and its SELECT policy gives a reader their own rows only. The function answered it anyway. Tracked in risa-labs-inc#1590. It was the last of the five plugin-store SECURITY DEFINER RPCs a client could still execute; the other four were closed by 20260909130000, 20260910000000 and 20260911010000. Its only caller is the plugin-store Edge Function's key-limit check, which runs on the service-role client, so revoking client access loses nothing. A revoke rather than an own-keys check, because a check keeps an RPC reachable by every client for a question no client asks. Same shape as 20260923122000 (risa-labs-inc#994), and the same durability note: CREATE OR REPLACE keeps the ACL, DROP and CREATE would hand it back to authenticated through 20251023000014's default privileges. Tests: nine assertions, including the call itself made as authenticated for another user's id and refused with 42501 rather than answered with a 0 that would read as "no keys". Not run locally for want of a Postgres here; Database (pgTAP) is the check. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: fluck-boss <fluck-boss@users.noreply.github.com>
… store record (risa-labs-inc#1643) importWorkspace (Open from File) saved a Space under whatever id its JSON carried, with neither of the guards the MCP create path has (risa-labs-inc#926). An id of `Space_Themes` overwrote the theme store and wiped every Space's theme; `Last_Session_Set` broke session restore; `last-session` replaced the crash-recovery record - and that is exactly the id an exported Last Session carries, so opening one on another machine replaced THAT machine's record. - withImportableId(), beside withStableId(), re-mints an id that is a slot or would save over a reserved record; importWorkspace uses it. Re-minted rather than refused: the person asked for a layout and never chose the id. Not folded into withStableId, which the load scan uses and where the real record carries `last-session` legitimately. - isSpaceSlot folds case (risa-labs-inc#1590). Ids become file names, and APFS and NTFS fold case. - reservedWorkspaceStoreFileName moves from the MCP provider to WorkspaceIdentity.kt, unchanged but for its KDoc: it must move with the load scan's skips, and it is no longer the MCP path's alone. risa-labs-inc#1590's own example is already refused on the MCP path, by risa-labs-inc#926's case-insensitive file-name check; the unguarded path was the import. Tests drive the rule either side of a real WorkspaceFileManager, as IdlessWorkspaceImportTest does, plus a source check that importWorkspace applies it. Mutation-checked: each of the three changes, reverted, fails a named test. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: fluck-boss <fluck-boss@users.noreply.github.com>
…ce boundary (risa-labs-inc#1619) notification_post stored the agent-supplied `source` argument verbatim, so an agent posting with source: "System" produced an inbox entry indistinguishable from a genuine host notice; the string was the only origin signal and it came from the payload. Provenance is now stamped by the kernel-side boundary instead of trusted from the argument: - BossNotification carries an authoritative `origin` field (NotificationOrigin.HOST / AGENT). The serialized default is AGENT, so entries that predate the field or load with it missing fail closed: they cannot present as host origin. - NotificationCenter.post takes `origin` as a required parameter with no default, so every call site must answer who is posting at this boundary; the answer is never derived from caller-supplied text. - For AGENT posts the boundary demotes the caller-supplied label to a display string - "agent" bare, "agent: <label>" when one was offered, bounded to 80 characters - so agent text cannot masquerade as system origin regardless of the string sent. HOST posts keep the host's own label. - NotificationMcpToolProvider classifies every post that crosses the MCP boundary as AGENT, and both the post result and notifications_list expose `origin`, so a host notice and an agent notice stay distinguishable. The demotion lives in NotificationCenter rather than in the MCP provider so the contract cannot be bypassed by a caller that reaches post() without going through the provider: the only route to host presentation is host code explicitly claiming NotificationOrigin.HOST, which is a review-visible act. Fixes risa-labs-inc#1587
…talled predicate (risa-labs-inc#1640) - writeStagedDownload is tested on both halves of its contract: the whole stream lands, and a stream that dies mid-download removes its partial file and rethrows. The dying stream serves 16 KiB first, so bytes are on disk when it fails; a stream that failed on its first read would pass against a version that never deleted anything. - Both install paths are pinned to stageAndInstall by a source test (comments stripped, since the KDoc names atomicMoveFrom on purpose). Reverting a call site to an inline atomicMoveFrom kept every behavioural test green; now it fails one. - The resident refusal no longer deletes the download when the download IS the destination. stageAndInstall models that case explicitly, and in it the unconditional delete removed the installed jar - in the one refusal that exists because something is still running from that file. - DynamicPluginManager.isInstalled is renamed hasEntry rather than deleted. It still had five production callers (PluginRemoval x3, DependentRestartPrompt, and the manager's own uninstall-cancelled log), so deleting it would have broken plugin removal. The name was the hazard: an entry survives a rejected, deleted jar, which is the reading risa-labs-inc#563 proved wrong. Its KDoc now says so and points at installedAndOnDisk. - The KDoc displacement was already in order on the merged code; nothing to change. Mutation-checked: each guard's removal fails exactly one named test. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: fluck-boss <fluck-boss@users.noreply.github.com>
… and buffer sizes (risa-labs-inc#952) * fix(plugin-store): cap the remote-JAR central-directory range fetches and buffer sizes Fixes risa-labs-inc#914. extractManifestFromRemoteJar trusted the remote JAR's central-directory fields as fetch bounds and allocation sizes with no cap: the CD fetch spanned cdOffset..cdOffset+cdSize-1 straight from the CD header, and each entry fetch sized 30 + fileNameLength + 256 + compressedSize from the attacker-controlled compressedSize - each ending in an unbounded response.arrayBuffer(). A crafted JAR declaring compressedSize ~= 2GB for META-INF/boss-plugin/plugin.json made the isolate buffer a ~2GB range response in ~256MB of edge RAM: a per-request OOM/cost-abuse DoS remotely triggerable by anyone who can get a malicious JAR served from a URL the function inspects. Every range fetch now takes an explicit max-bytes cap (4MB for the CD/EOCD probes, 512KB for the manifest entry - the same bound the host applies to manifest reads), the declared entry size is refused outright above the manifest bound before any fetch, and the response itself is re-checked against the cap so a server that ignores or lies about the Range header cannot bypass it. 2 regression tests with crafted CD bytes: a 2GB CD size is refused without any large fetch, and a 2GB plugin.json entry is refused before its fetch with no range request ever exceeding 5MB. Suite 66/66 (64 baseline held). * fix(plugin-store): bound the manifest deflate-inflate against CD-declared and actual uncompressed sizes Closes the risa-labs-inc#914 follow-up found in self-re-audit: the compressed-size caps alone left the decompression path unbounded. The CD's declared uncompressedSize is as attacker-controlled as its compressedSize, and a <30KB compressed plugin.json entry can inflate to gigabytes - a deflate bomb that passes every fetch cap and OOMs the isolate through the unbounded DecompressionStream accumulation loop. The declared uncompressedSize is now refused above the 512KB manifest bound before any inflate, and the accumulation loop enforces a hard byte counter that cancels the stream the moment actual decompressed bytes cross the same bound, so a lying declared size cannot smuggle a bomb either. Regression test: a small compressed entry declaring a 2GB uncompressedSize fails the extraction. Suite 67/67 (baseline 64 held). * chore: remove local triage bookkeeping files from the branch (never meant to leave the workstation) * fix(plugin-store): address PR risa-labs-inc#952 review - rebuild CD fixtures as valid 46-byte records, tighten first-test assertion, observe decompression cancel, cap the EOCD probe The three tests built 44-byte central-directory records (omitting version-needed and flags), so every field the parser reads landed two bytes early and the crafted sizes never reached the cap checks at all - method read zero padding, nameLen read the extra-field length, and the parser exited through "manifest not found" before any cap could fire. Rebuild cdEntry as a proper 46-byte fixed header so method at +10, compressedSize at +20, uncompressedSize at +24, nameLen at +28 and localOffset at +42 line up with the parser's arithmetic, and have it take an explicit method so the deflate-bomb test actually sets method=8 where the parser reads it. The first test's assertion accepted almost any error string ("cap" OR "Refusing" OR "EOCD" OR "manifest" OR "not found" OR "Failed"), so removing the cdSize span check still passed. Push the CD out of the 65KB tail window with 70KB of padding so the code takes the downloadRange path, then assert on the exact cap message. Removing the span check now fails the test with a different message - mutation-verified. The decompression reader's `await reader.cancel()` could surface as an unhandled rejection if the stream had already errored. Wrap in try/catch so the cancel is best-effort and the throw that follows remains the signal the caller sees, with the stream already past its bound. The initial tail fetch (the EOCD probe) lacked an explicit size cap: a server ignoring Range returned the full body. Add MAX_EOCD_PROBE_BYTES (1MB, well above any real EOCD + zip64 locator) and refuse above it with the same post-read shape downloadRange uses for its cap. Test 2 keeps its loose threw=true assertion because its maxRangeSpan < 5MB check catches a missing per-entry cap once the downloadRange span cap is also removed - mutation-verified together. * fix: detach writer promises on the cancel path; pin guard messages in tests 2 and 3 * ci: retrigger against post-risa-labs-inc#1642 dev --------- Co-authored-by: fluck-boss <fluck-boss@users.noreply.github.com> Co-authored-by: Instinct Agent <agent@instinct.com>
ff1b71f to
e2d80a9
Compare
…sa-labs-inc#1024) * feat(mcp): defensive system path bounds in workspace tools * fix(mcp): address detekt complexity and return count in CLISecurityValidator * fix(mcp): format CLISecurityValidator for ktlint * fix(mcp): address review comments on system path bounds - Add macOS system roots (/System, /Library, /usr/libexec) and canonical /private forms (/private/etc, /private/var/db) to RESTRICTED_POSIX_ROOTS - Handle Windows extended and device namespace prefixes (\\?\C:\..., \\.\C:\...) - Enforce MAX_OPEN_TARGET_PATH_LENGTH in normalizePath - Add symlink regression tests covering the canonical path branch in WorkspaceMcpSecurityBoundsTest - Drop /root from restricted roots to avoid blocking user home in root containers - Restore original relative order of isAbsolute before isValidPath - Relocate unit tests to CLISecurityValidatorTest and clean up KDoc comments
…risa-labs-inc#389) (risa-labs-inc#623) * improve(onboarding): rewrite plugin descriptions to be benefit-driven Replace technical jargon with user-facing descriptions that explain what each plugin enables the user and their agent to do. Addresses risa-labs-inc#389 acceptance criteria: explain plugin/tool choices in terms of what users can do with them. * feat(onboarding): add first session banner after wizard completion Show a dismissible welcome banner immediately after the plugin install wizard completes, giving new users three clear next actions: open a workspace, connect an agent (with MCP address 127.0.0.1:7677 and one-click copy), and open the Toolbox. The banner is shown once and permanently dismissed via UserDataStorage. It slides in/out with AnimatedVisibility. Addresses risa-labs-inc#389 acceptance criteria: - Land user in a usable workspace with a clear first action - Guide users through required workspace steps after setup * refactor(onboarding): redesign welcome overlay as centered card dialog and fix workspace button - Move FirstSessionBanner from BossAppScaffold to BossAppDialogs for proper dialog layering - Rewrite as centered BossDialog with three action cards - Fix "Open a Workspace" to open workspace picker via state.pendingWorkspacePrompt instead of opening Toolbox - Force user to click "Get Started" to dismiss (onDismissRequest is empty) * fix(onboarding): constrain welcome dialog width to 680dp * feat(onboarding): add 4-step agent connection wizard to welcome overlay Transform the "Connect an Agent" card into a guided 4-step wizard that walks new users through: 1. Picking their agent CLI (Claude Code, Gemini, Codex) 2. Running the exact connection command with copy button 3. Verifying the connection by asking the agent 4. Trying their first BOSS action Uses exact commands from README: - claude mcp add --scope user --transport sse boss http://127.0.0.1:7677 - gemini mcp add boss http://127.0.0.1:7677 --transport sse --scope user - codex mcp add boss --url http://127.0.0.1:7677/mcp Addresses the most critical new-user friction point in risa-labs-inc#389: the agent connection flow was completely invisible before setup. * fix(onboarding): improve welcome overlay layout and wizard UX - Wrap dialog content in Surface with elevation for proper card appearance - Increase dialog width to 780dp with 32dp padding - Change agent picker from Column to Row for side-by-side buttons using weight(1f) - Fix all command box text contrast with explicit onSurface color and 0.15f background opacity - Update tip box text to accurately guide copy-open-run flow - Add onOpenTerminal callback and left-aligned Toolbox shortcut - Fix info boxes in Step 3 to use fillMaxWidth and weight(1f) * feat(onboarding): add Getting Started entry in Settings to reopen welcome guide Add a "Getting Started" section to the Settings panel that lets users reopen the first session welcome overlay at any time. Implementation follows existing MenuActionsHandler SharedFlow pattern: - MenuActionsHandler: new reopenGettingStartedEvents SharedFlow - BossAppMenuActionEffects: observer resets firstSessionBannerDismissed and sets showFirstSessionBanner=true - SettingsSidebar: new GETTING_STARTED enum entry with PlayArrow icon - SettingsWindow: settings panel with description and "Reopen Getting Started Guide" button Addresses risa-labs-inc#389 acceptance criteria: - Make skipped setup easy to find later through settings panel * feat(onboarding): hint users about Settings → Getting Started on wizard completion Add a WizardNote on the Complete screen of the plugin install wizard telling users they can reopen the getting started guide from Settings → Getting Started. Closes the loop between first-run and re-discovery. * fix(onboarding): address PR review findings 1-4 Finding 1 — Settings → Getting Started now correctly opens the plugin install wizard via the existing triggerShowPluginWizard event flow. Removed the redundant reopenGettingStartedEvents SharedFlow and its observer from MenuActionsHandler and BossAppMenuActionEffects entirely. Finding 2 — Add pluginInstallWizardRequestGeneration counter to BossAppState, incremented in the showPluginWizardEvents observer alongside showPluginInstallWizard = true, mirroring the existing terminalOnboardingRequestGeneration pattern exactly. Finding 5 — Extract Getting Started settings section from inline SettingsWindow.kt into its own GettingStartedSettings.kt under components/settings/sections/, following the StartupSettings.kt structural pattern. Uses SettingsSection(title=) wrapper, no manual heading, no extra padding, SettingsButtonRow for the action. Registered in SettingsSearchEntries.kt with searchable labels (onboarding, setup wizard, welcome, plugin wizard). Moved GETTING_STARTED to first position in SettingsSection enum so it appears at the top of the Settings sidebar. Updated stale enum description to match actual behavior. Finding 3 — FirstSessionBanner.kt already removed by upstream commit ce54e5a (no further action needed). Finding 4 — Add PluginWizardEventTest verifying: the event reaches the correct window only, fires once per call, and does not reach a different windowId. * style(onboarding): fix ktlint violations flagged by CI - Wrap two description strings in PluginListProvider.kt that exceeded the 140-char line limit (Fluck Agent, Secret Manager) - Fix SettingsSearchEntries.kt indentation and sync the Getting Started search label to match the UI exactly (Plugin Setup Wizard), resolving SettingsSearchIndexDriftTest failures in both directions - Run ktlintFormat across the branch to clean up remaining trailing whitespace, import ordering, and brace/multiline style violations surfaced by CI * style: wrap long description lines for detekt MaxLineLength * style: ktlint-conformant wrap of long description lines * fix: restore wrap-point spaces, spaced hyphens per AGENTS.md, AI Gateway wording; drop dead field and unused local/import * style: remove blank first line left by dead-local removal --------- Co-authored-by: Tanish Bansal <tanishbansal@Tanishs-MacBook-Air.local> Co-authored-by: fluck-boss <fluck-boss@users.noreply.github.com>
…ble through the reload (risa-labs-inc#984) `reload with late refusal leaves refusing manager untouched` failed on the Windows runner with "Reload must fail when manager 2 refuses unload": the reload had succeeded. DynamicPluginManager holds unload-aware components by WeakReference, and the test registered its two listeners as inline anonymous objects that nothing else referenced. A GC between registration and the reload collected them, `ref.get() ?: continue` skipped them, manager 1 never set the flag, manager 2 never refused, and the reload went through. Which runner sees it is only where the GC lands. Reproduced on Linux by forcing a GC before the reload (same failure), and the forced GC stays in the test as the deterministic guard. The listeners are now held in locals and used after the reload, so they are reachable for its whole duration whatever the JIT decides about liveness. The registration's KDoc now says the reference is weak and the caller owns the lifetime, since the same shape is one `registerUnloadAware(object` away in any new test. Co-authored-by: Atharv Gaur <atharvanna71@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…t (per review) Ask 1 - publish permittedToolNames from the registry core (applyExposed) and facade, and derive the flight plan's isPermitted from that set instead of the exposed tools set, which folds the kill-switch in: a user-disabled tool now reports the tool-switch checkpoint, not a permission failure. Ask 2 - new McpFlightPlanLauncherTest drives the launcher's real derivation (mcpFlightPlanViews/mcpFlightPlanInput) through a real McpToolRegistryCore, covering the disabled-but-permitted case the old exposed-set logic mislabeled. Ask 3 - an open plan recomputes on policy change (key over the engine's config/sessionTrustedTools, policyFor re-answered per tool), with a policy-change test; the activity status item gained a registry-facing derivation (mcpActivityStatusShowsFor over allTools) so its binding cannot silently regress to the exposed set, with tests through a real core.
e2d80a9 to
3883b4f
Compare
|
@claude please review this exact head (3883b4f) for merge into dev. Check that the change is correct and complete for its stated goal, that tests cover the risk being fixed (a test that passes without the fix is a finding), that the earlier review findings on this PR are addressed, and flag any security issues (secrets, injection, auth or approval bypass, path handling). Keep the review focused on this diff. |
|
Claude finished @fluck-boss's task in 4m 11s —— View job Review — PR #1380 @
|
|
Review asks, addressed at 3883b4f:
Hosted checks green on 3883b4f. Marking ready for review. |
fluck-boss
left a comment
There was a problem hiding this comment.
Thanks @Mihir-Rabari - item 1 is fixed and its test is real. Before merge (from Claude's review of 3883b4f):
- YOLO mode isn't modelled.
policyFordeliberately ignores YOLO (it's applied at the approval step), so with YOLO on, an ASK tool's plan says "Human approval required" when the call will actually run with no prompt. A forecast that shows a human gate while the bypass is live is the failure this feature exists to prevent.yoloModeis a publicStateFlow(McpPolicyEngine.kt:305): put it onMcpFlightPlanInput, and have ASK + YOLO giveREADY_TO_RUNwith a checkpoint that names the bypass. - An unreadable kill-switch file shows as "This tool is switched off". The outcome (
WITHHELD) is right, but the explanation sends the operator to a Toolbox toggle that can't fix it. ConsultkillSwitchFault(McpToolRegistryImpl.kt:154) the same way you already handlepolicyFault. - F1, still open: the status-item test still passes if
BossBottomBar.kt:543goes back to.tools. Please make it discriminate.
F2, N3, F5 and F6 are fine as follow-ups.
Changes requested + draft until then. Push, get the ubuntu and windows legs green, mark it ready for review, and it goes straight back into the merge queue.
08da448 to
4077231
Compare
072a2bd to
2ae2565
Compare
MCP Flight Plan
Agents can invoke a large MCP surface, but the operator previously had to infer the current outcome from separate kill-switch, policy, and activity views. This adds a zero-execution flight plan to the clickable MCP status surface.
Open MCP Activity Log → Flight Plan, select a registered tool, and BOSS shows its current four-stage route:
The dialog also shows the tool's risk classification, mutating/read-only classification, provider, description, and expected input schema. It receives registry and policy snapshots but has no invocation, approval, or ledger-writing path.
Validation
:composeApp:desktopTest --tests "ai.rever.boss.mcp.McpFlightPlanTest":composeApp:ktlintCheck :composeApp:detekt