From ed91423666c6733191979c6038b94eae6facd60c Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 23 Aug 2026 15:20:22 -0700 Subject: [PATCH] Hold back the Disk panel and OS drag-out from the ship branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a scope reduction, not an abandonment. Both subsystems are preserved in full on `hold/disk-and-dragout` and are intended to come back once they have been hardened separately. Nothing here is a judgement that the features are unwanted — three successive audit-and-fix cycles each closed a critical defect in these two areas and each opened a new one, so the rest of the round ships now and these two get their own cycle rather than holding it up. Removed: the Disk settings panel and its whole reclaim / destroy / compaction surface — `DiskSettings`, `DiskProjectTable`, `useDiskUsage`, `docker/disk.rs`, `disk_tests.rs`, the disk commands in `docker_commands.rs`, and their `generate_handler!` entries. Dropping the IPC entries is the point: a UI-only removal would have left five commands callable by a compromised webview, one of them a verified arbitrary-DELETE primitive. `sweep_orphaned_snapshots`'s *command* goes with them (the panel was its only caller); the sweep itself stays. Removed: OS drag-out from the Files tab — `stage_container_file_for_drag` and its host staging lifecycle, the pointer gesture and `dragPreview`, `stageForDrag` / `isStagedHostPath`, the `tauri-plugin-drag` and `@crabnebula/tauri-plugin-drag` dependencies, and the `drag:allow-start-drag` capability grant, which could not be scoped. The capability test's expected list is updated; its `*:default` and `store:*` assertions are untouched. Kept, deliberately: drag-and-drop *into* the app (Files pane and terminal) and "Save to host…", which is now the only route out of a container. The prevention work is untouched — the pre-commit scrub and `SNAPSHOT_SCRUB_PATHS`, capped container logs, the `triple-c.base` / `triple-c.managed` labels, `sweep_orphaned_snapshots` and the startup housekeeping, the migration pin/probe reapers, scheduler log pruning, `formatBytes.ts`, and `project_lock.rs` in full with every acquisition site outside `disk.rs`. Entanglements, resolved rather than deleted blind: * `container.rs`'s `a_compaction_runs_this_module_s_scrub_script_byte_for_byte` pinned the compaction Dockerfile against `snapshot_scrub_script()`. Dropped — it existed only for compaction. `snapshot_scrub_script` and its containment tests are untouched. * `lib.rs`'s startup reap of `:compacting` tags and `triple-c-compact-*` containers is dropped: nothing on this branch creates them. * `project_lock`'s `Compaction` / `CacheClear` variants and `any_held_excluding`, `migration_commands::is_migrating`, and `formatBytes{Delta,Ceiling}` lose their last production caller but are kept and still tested, annotated with why. * `projects_store::corrupt_since` and `migration_store::peek_ownerless_since` were read only by the disk survey and are removed. The corrupt-load marker and `.bak` are still written. Verified: `npm run test` 611 passing, `npx tsc --noEmit` clean, `npm run build` green; `cargo test` 419 passed / 2 ignored, `cargo build` 0 warnings. Every test removed belongs to a removed feature — no kept-behaviour test was weakened or deleted. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc --- CLAUDE.md | 36 +- HOW-TO-USE.md | 6 +- app/package-lock.json | 9 - app/package.json | 1 - app/src-tauri/Cargo.lock | 200 +- app/src-tauri/Cargo.toml | 1 - app/src-tauri/capabilities/default.json | 5 +- app/src-tauri/gen/schemas/acl-manifests.json | 2 +- app/src-tauri/gen/schemas/capabilities.json | 2 +- app/src-tauri/gen/schemas/desktop-schema.json | 18 - app/src-tauri/gen/schemas/linux-schema.json | 18 - app/src-tauri/src/commands/docker_commands.rs | 77 - app/src-tauri/src/commands/file_commands.rs | 353 +- .../src/commands/migration_commands.rs | 7 + app/src-tauri/src/docker/container.rs | 72 +- app/src-tauri/src/docker/disk.rs | 4295 ----------------- app/src-tauri/src/docker/disk_tests.rs | 1973 -------- app/src-tauri/src/docker/mod.rs | 5 - app/src-tauri/src/lib.rs | 53 +- app/src-tauri/src/project_lock.rs | 20 +- app/src-tauri/src/storage/migration_store.rs | 16 - app/src-tauri/src/storage/projects_store.rs | 49 +- .../projects/home/FilesTab.test.tsx | 238 - app/src/components/projects/home/FilesTab.tsx | 198 +- .../projects/home/config/AuthBridgeRow.tsx | 2 +- .../projects/home/dragPreview.test.ts | 79 - .../components/projects/home/dragPreview.ts | 85 - .../components/settings/DiskProjectTable.tsx | 250 - .../components/settings/DiskSettings.test.tsx | 1092 ----- app/src/components/settings/DiskSettings.tsx | 855 ---- app/src/components/settings/SettingsPanel.tsx | 5 - .../components/ui/TypedConfirmModal.test.tsx | 133 - app/src/components/ui/TypedConfirmModal.tsx | 145 - app/src/hooks/useDiskUsage.test.tsx | 473 -- app/src/hooks/useDiskUsage.ts | 270 -- app/src/hooks/useFileManager.test.ts | 96 - app/src/hooks/useFileManager.ts | 72 +- app/src/lib/formatBytes.test.ts | 12 +- app/src/lib/formatBytes.ts | 25 +- app/src/lib/tauri-commands.ts | 40 +- app/src/lib/types.ts | 242 - 41 files changed, 126 insertions(+), 11404 deletions(-) delete mode 100644 app/src-tauri/src/docker/disk.rs delete mode 100644 app/src-tauri/src/docker/disk_tests.rs delete mode 100644 app/src/components/projects/home/dragPreview.test.ts delete mode 100644 app/src/components/projects/home/dragPreview.ts delete mode 100644 app/src/components/settings/DiskProjectTable.tsx delete mode 100644 app/src/components/settings/DiskSettings.test.tsx delete mode 100644 app/src/components/settings/DiskSettings.tsx delete mode 100644 app/src/components/ui/TypedConfirmModal.test.tsx delete mode 100644 app/src/components/ui/TypedConfirmModal.tsx delete mode 100644 app/src/hooks/useDiskUsage.test.tsx delete mode 100644 app/src/hooks/useDiskUsage.ts diff --git a/CLAUDE.md b/CLAUDE.md index 3c1e74b..e21ecc6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,23 +79,27 @@ docker exec stdout → tokio task → emit("terminal-output-{sessionId}") → li - **`components/projects/home/`** — **Project Home**, the main-area view for a project: Overview / Sessions / Automation / Config / Files. Per-project configuration lives here, not in modals — see "UI conventions" below. - - **Files moves in both directions and neither direction uses HTML5 drag.** Dropping *into* - the pane is Tauri's native `onDragDropEvent`, which is window-wide and therefore routed by - a hit-test of the physical-pixel payload position against the pane's rect ÷ + - **Files takes drops *in*, and that path does not use HTML5 drag.** Dropping into the pane + is Tauri's native `onDragDropEvent`, which is window-wide and therefore routed by a + hit-test of the physical-pixel payload position against the pane's rect ÷ `devicePixelRatio` — a hidden pane has a zero-size rect, which is what stops it and - `TerminalView`'s listener both firing. Dragging *out* is pointer events into - `tauri-plugin-drag`, for the same `dragDropEnabled` reason the tab strip is pointer-driven. - - **A drag-out is a copy first and a drag second.** The OS can only drag a path that exists - on the host, and these files are inside a container, so `stage_container_file_for_drag` - materialises one into `/triple-c-drag-out//` (via the shared - `fetch_container_file`, keeping the original filename, capped at the same 256 MiB as an - upload) and `startDrag` is handed *that*. Two consequences worth keeping: the copy is an - async gap inside a gesture that feels instantaneous, so the staged path is cached and the - UI says "drag it again" when the pointer came up first; and the staging directory is - cleared on exit **and** reaped at startup, because a drag-out quietly filling the host temp - dir with whole files would be the disk problem this project just fixed, in a new place. - "Save to host…" stays — `startDrag` is an enhancement and can fail per platform. -- **`components/settings/`** — Host-level settings: Docker, AWS, Web Terminal, STT, shared auth + `TerminalView`'s listener both firing. Keep `lib/dropTarget.ts` and both listeners. + - **Getting a file *out* is "Save to host…", and there is no other route.** OS drag-out — + `tauri-plugin-drag`, `stage_container_file_for_drag` and its host staging directory — was + removed from the ship branch and held back for separate hardening; it lives on + `hold/disk-and-dragout`. Do not re-add `drag:allow-start-drag` or a staging command + without taking that work back whole: the plugin has no scope mechanism, so the grant lets + a compromised webview start a drag on *any* host path the user can read, and the staging + directory is a host-temp disk leak with a gesture attached unless its exit-clear and + startup-reap come back with it. +- **`components/settings/`** — Host-level settings: Docker, AWS, Web Terminal, STT, shared auth. + There is deliberately **no Disk panel** here. The disk survey and its reclaim / destroy / + compaction surface were held back for separate hardening and live on `hold/disk-and-dragout`; + one of their IPC commands was a verified arbitrary-DELETE primitive, so if that work returns it + returns whole, `generate_handler!` entries and typed confirmations included. The *prevention* + half stayed and is not disk-panel code: the pre-commit scrub in `docker/container.rs`, capped + container logs, the `triple-c.base` / `triple-c.managed` labels, `sweep_orphaned_snapshots` and + the startup housekeeping in `lib.rs`, the migration reapers, and `project_lock.rs`. - **`components/ui/`** — Shared primitives. **Use these; do not hand-roll replacements.** `Modal` (the only correct way to build a dialog — it supplies `role="dialog"`, `aria-modal`, focus trap and restore), `Button`, `Toggle`, `Field`, `SegmentedControl`, `StatusIndicator`, diff --git a/HOW-TO-USE.md b/HOW-TO-USE.md index df4a4e0..d744049 100644 --- a/HOW-TO-USE.md +++ b/HOW-TO-USE.md @@ -1164,8 +1164,10 @@ When you scroll up in the terminal to review previous output, a **Jump to Curren The **Files** tab of Project Home browses inside a running container. You can: - **Browse** the container filesystem, starting at `/workspace`, with breadcrumb navigation -- **Download** any file to your host machine via the **Download** button on each file entry -- **Upload file** from your host into the current container directory +- **Save to host…** — copy any file out to a location you pick. This is the way to get a file out + of a container; there is one button per file entry, and the file viewer offers it too +- **Upload file** from your host into the current container directory — or **drop files straight + onto the pane** from your desktop, which uploads them into the directory on screen - **Refresh** the directory listing at any time The listing shows file names, sizes, and modification dates. diff --git a/app/package-lock.json b/app/package-lock.json index e87abfb..5626285 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,7 +8,6 @@ "name": "triple-c", "version": "0.4.0", "dependencies": { - "@crabnebula/tauri-plugin-drag": "^2.1.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.7.0", "@tauri-apps/plugin-opener": "^2.5.3", @@ -414,14 +413,6 @@ "specificity": "bin/cli.js" } }, - "node_modules/@crabnebula/tauri-plugin-drag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@crabnebula/tauri-plugin-drag/-/tauri-plugin-drag-2.1.0.tgz", - "integrity": "sha512-LnUXAZwQt1cdMoGDLJ6ogW9wFCYServCZXlGadS7CA+CZ9eXS7L+Q7QyQW6g/zGw9YI2MwKFqf1aSNBGyWw+OA==", - "dependencies": { - "@tauri-apps/api": "^2.0.0" - } - }, "node_modules/@csstools/color-helpers": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", diff --git a/app/package.json b/app/package.json index f9d2f0d..229cd9d 100644 --- a/app/package.json +++ b/app/package.json @@ -12,7 +12,6 @@ "test:watch": "vitest" }, "dependencies": { - "@crabnebula/tauri-plugin-drag": "^2.1.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.7.0", "@tauri-apps/plugin-opener": "^2.5.3", diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index 6315973..c75619d 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -630,19 +630,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.10.1", - "core-graphics-types", - "foreign-types", - "libc", -] - [[package]] name = "core-graphics" version = "0.25.0" @@ -1029,28 +1016,6 @@ dependencies = [ "serde", ] -[[package]] -name = "drag" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e90b4a25ace5ce0561534b073943594cbcd21af936e64d09aec444568411f8c" -dependencies = [ - "core-graphics 0.24.0", - "dunce", - "gdk", - "gdkx11", - "gtk", - "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "raw-window-handle", - "serde", - "thiserror 2.0.18", - "windows 0.52.0", - "windows-core 0.58.0", -] - [[package]] name = "dtoa" version = "1.0.11" @@ -2682,17 +2647,9 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.11.0", "block2", - "libc", "objc2", - "objc2-cloud-kit", - "objc2-core-data", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", "objc2-foundation", - "objc2-quartz-core", ] [[package]] @@ -2712,7 +2669,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.11.0", "objc2", "objc2-foundation", ] @@ -2773,19 +2729,6 @@ dependencies = [ "objc2-core-graphics", ] -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", -] - [[package]] name = "objc2-encode" version = "4.1.0" @@ -4457,7 +4400,7 @@ dependencies = [ "bitflags 2.11.0", "block2", "core-foundation 0.10.1", - "core-graphics 0.25.0", + "core-graphics", "crossbeam-channel", "dbus", "dispatch2", @@ -4482,7 +4425,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows 0.61.3", + "windows", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -4565,7 +4508,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows 0.61.3", + "windows", ] [[package]] @@ -4665,21 +4608,6 @@ dependencies = [ "url", ] -[[package]] -name = "tauri-plugin-drag" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "729ca0ce4b1169869d3405216d3c09a524f41ea5e2eec89f917cd6623f8a70ca" -dependencies = [ - "base64 0.22.1", - "drag", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", -] - [[package]] name = "tauri-plugin-fs" version = "2.5.0" @@ -4722,7 +4650,7 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.18", "url", - "windows 0.61.3", + "windows", "zbus", ] @@ -4748,7 +4676,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.61.3", + "windows", ] [[package]] @@ -4773,7 +4701,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.61.3", + "windows", "wry", ] @@ -5242,7 +5170,6 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-dialog", - "tauri-plugin-drag", "tauri-plugin-opener", "tokio", "tower-http", @@ -5695,10 +5622,10 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows 0.61.3", + "windows", "windows-core 0.61.2", - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", ] [[package]] @@ -5719,7 +5646,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows 0.61.3", + "windows", "windows-core 0.61.2", ] @@ -5769,18 +5696,6 @@ dependencies = [ "windows-version", ] -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-implement 0.52.0", - "windows-interface 0.52.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.61.3" @@ -5803,36 +5718,14 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -5844,8 +5737,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -5862,28 +5755,6 @@ dependencies = [ "windows-threading", ] -[[package]] -name = "windows-implement" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -5895,28 +5766,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "windows-interface" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -5950,15 +5799,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -5977,16 +5817,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -6414,7 +6244,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows 0.61.3", + "windows", "windows-core 0.61.2", "windows-version", "x11-dl", diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 9ec41c3..b97b6cb 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -36,7 +36,6 @@ tower-http = { version = "0.6", features = ["cors"] } base64 = "0.22" rand = "0.9" local-ip-address = "0.6" -tauri-plugin-drag = "2.1" [dev-dependencies] # `test-util` (not part of tokio's `full`) lets the auto-start retry tests run diff --git a/app/src-tauri/capabilities/default.json b/app/src-tauri/capabilities/default.json index 840d5ee..2c13a4c 100644 --- a/app/src-tauri/capabilities/default.json +++ b/app/src-tauri/capabilities/default.json @@ -1,6 +1,6 @@ { "identifier": "default", - "description": "Default capabilities for Triple-C. Every entry here is an IPC command a compromised webview can call directly, so the set is an enumeration of what `app/src` actually invokes — verified against tauri 2.11.0's `PLUGINS` table in `build.rs`, not assumed from a plugin's `default` set. `core:default` in particular is NOT used: it is an alias for `core:{path,event,window,webview,app,image,resources,menu,tray}:default`, and `core:image:default` carries `allow-from-path`, whose handler (`tauri-2.11.0/src/image/plugin.rs:41` → `src/image/mod.rs:96`) is a bare `std::fs::read(path)` with no scope mechanism of any kind. Nothing imports `@tauri-apps/api/image`, so the whole plugin is dropped rather than scoped — there is nothing to scope it with. `core:menu` and `core:tray` are dropped for the same reason (no menu, no tray icon); `core:window` and `core:path` because nothing imports them; `core:resources:allow-close` because no frontend value is a `Resource` (`startDrag`'s `Channel` is not one); and `core:event`'s `allow-emit`/`allow-emit-to` because the frontend only ever *listens* — every emit in this app originates in Rust. Three notes on what is deliberately kept or accepted: (1) `core:webview:allow-internal-toggle-devtools` is not called by `app/src` at all — it is called by Tauri's own injected `toggle-devtools.js`, which binds Ctrl/Cmd+Shift+I. Both that script and the command behind it are `#[cfg(any(debug_assertions, feature = \"devtools\"))]`, so this grant is a `tauri dev` convenience that does not exist in a release bundle. (2) `opener:allow-open-url` cannot be narrowed by host. `TerminalView`'s `WebLinksAddon` opens links Claude printed inside the container, which are arbitrary by construction, so a host allowlist here would delete the feature rather than bound it. What *is* bounded: `opener:default` is not used, so `open_path` and `reveal_item_in_dir` are absent; the scope's two entries restrict the scheme to http/https (`file:`, `mailto:`, `tel:`, `smb:` are all refused by `Scope::is_url_allowed`); and because each entry leaves `app` at its serde default of `Application::Default`, which matches only `with == None`, `openUrl(url, \"/bin/sh\")` is refused — the `with` argument is not a usable exec primitive. The call sites re-validate through `sanitizeRelayUrl` (scheme allowlist, no embedded credentials, length cap) before anything reaches the opener. Accepted residual risk: a compromised webview can make the OS open an attacker-chosen http(s) URL, which is an outbound channel. Recorded here rather than fixed. (3) `drag:allow-start-drag` stays, and cannot be scoped — `tauri-plugin-drag` takes the item paths from the caller and has no scope mechanism, so a compromised webview could call `startDrag({ item: ['~/.ssh/id_rsa'] })` against any host path the user can read. It is not a silent exfiltration primitive: the drag only delivers anything if the user completes a real drop onto a real target, and the OS shows the drag under the cursor while it is in flight. Removing it would remove drag-out from the Files pane (`stage_container_file_for_drag`), whose fallback is the explicit \"Save to host…\" action. Accepted residual risk. Historical note kept because it is easy to re-introduce: the `store:*` grants were removed — nothing in `app/src` uses `@tauri-apps/plugin-store`, and the plugin's `resolve_store_path` is a `PathBuf::push` against AppData, which `push` discards outright when handed an absolute path, so the grant was an arbitrary host-file read/write primitive (`plugin:store|load` + `set` + `save` on `~/.claude/settings.json` is host code execution). On the CSP side: `app.security.csp` in `tauri.conf.json` covers the shipped bundle, and there is deliberately no `devCsp`. `npm run tauri dev` loads the main document straight from Vite at `build.devUrl` (`http://localhost:1420`), and Tauri only attaches a CSP to documents it serves itself — `protocol/tauri.rs:217` sets the header on `tauri://` assets, and the dev server is proxied through that protocol only when `PROXY_DEV_SERVER`, which is `cfg!(all(dev, mobile))` and therefore false for every desktop build. A `devCsp` here would be inert config that reads as protection, which is worse than its absence. If a CSP in dev is wanted, the only place that can set one is the Vite dev server's own `server.headers` in `app/vite.config.ts`; it is not set today, and dev is not the shipped configuration.", + "description": "Default capabilities for Triple-C. Every entry here is an IPC command a compromised webview can call directly, so the set is an enumeration of what `app/src` actually invokes — verified against tauri 2.11.0's `PLUGINS` table in `build.rs`, not assumed from a plugin's `default` set. `core:default` in particular is NOT used: it is an alias for `core:{path,event,window,webview,app,image,resources,menu,tray}:default`, and `core:image:default` carries `allow-from-path`, whose handler (`tauri-2.11.0/src/image/plugin.rs:41` → `src/image/mod.rs:96`) is a bare `std::fs::read(path)` with no scope mechanism of any kind. Nothing imports `@tauri-apps/api/image`, so the whole plugin is dropped rather than scoped — there is nothing to scope it with. `core:menu` and `core:tray` are dropped for the same reason (no menu, no tray icon); `core:window` and `core:path` because nothing imports them; `core:resources:allow-close` because no frontend value is a `Resource`; and `core:event`'s `allow-emit`/`allow-emit-to` because the frontend only ever *listens* — every emit in this app originates in Rust. Three notes on what is deliberately kept or accepted: (1) `core:webview:allow-internal-toggle-devtools` is not called by `app/src` at all — it is called by Tauri's own injected `toggle-devtools.js`, which binds Ctrl/Cmd+Shift+I. Both that script and the command behind it are `#[cfg(any(debug_assertions, feature = \"devtools\"))]`, so this grant is a `tauri dev` convenience that does not exist in a release bundle. (2) `opener:allow-open-url` cannot be narrowed by host. `TerminalView`'s `WebLinksAddon` opens links Claude printed inside the container, which are arbitrary by construction, so a host allowlist here would delete the feature rather than bound it. What *is* bounded: `opener:default` is not used, so `open_path` and `reveal_item_in_dir` are absent; the scope's two entries restrict the scheme to http/https (`file:`, `mailto:`, `tel:`, `smb:` are all refused by `Scope::is_url_allowed`); and because each entry leaves `app` at its serde default of `Application::Default`, which matches only `with == None`, `openUrl(url, \"/bin/sh\")` is refused — the `with` argument is not a usable exec primitive. The call sites re-validate through `sanitizeRelayUrl` (scheme allowlist, no embedded credentials, length cap) before anything reaches the opener. Accepted residual risk: a compromised webview can make the OS open an attacker-chosen http(s) URL, which is an outbound channel. Recorded here rather than fixed. (3) `drag:allow-start-drag` is **gone**, together with the OS drag-out it existed for. It could not be scoped — `tauri-plugin-drag` takes the item paths from the caller and has no scope mechanism, so a compromised webview could call `startDrag({ item: ['~/.ssh/id_rsa'] })` against any host path the user can read — and it was carried as an accepted residual risk for one gesture. Drag-out was held back for separate hardening (see branch `hold/disk-and-dragout`), the plugin is no longer a dependency, and getting a file out of a container is now the explicit \"Save to host…\" action, which never touches this permission. Note that dragging files *into* the app is unaffected: `dragDropEnabled` and `onDragDropEvent` are core webview behaviour and need no grant. Historical note kept because it is easy to re-introduce: the `store:*` grants were removed — nothing in `app/src` uses `@tauri-apps/plugin-store`, and the plugin's `resolve_store_path` is a `PathBuf::push` against AppData, which `push` discards outright when handed an absolute path, so the grant was an arbitrary host-file read/write primitive (`plugin:store|load` + `set` + `save` on `~/.claude/settings.json` is host code execution). On the CSP side: `app.security.csp` in `tauri.conf.json` covers the shipped bundle, and there is deliberately no `devCsp`. `npm run tauri dev` loads the main document straight from Vite at `build.devUrl` (`http://localhost:1420`), and Tauri only attaches a CSP to documents it serves itself — `protocol/tauri.rs:217` sets the header on `tauri://` assets, and the dev server is proxied through that protocol only when `PROXY_DEV_SERVER`, which is `cfg!(all(dev, mobile))` and therefore false for every desktop build. A `devCsp` here would be inert config that reads as protection, which is worse than its absence. If a CSP in dev is wanted, the only place that can set one is the Vite dev server's own `server.headers` in `app/vite.config.ts`; it is not set today, and dev is not the shipped configuration.", "windows": ["main"], "permissions": [ "core:event:allow-listen", @@ -11,7 +11,6 @@ { "identifier": "opener:allow-open-url", "allow": [{ "url": "http://*" }, { "url": "https://*" }] - }, - "drag:allow-start-drag" + } ] } diff --git a/app/src-tauri/gen/schemas/acl-manifests.json b/app/src-tauri/gen/schemas/acl-manifests.json index 672eeb1..c4a47e8 100644 --- a/app/src-tauri/gen/schemas/acl-manifests.json +++ b/app/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"drag":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin","permissions":["allow-start-drag"]},"permissions":{"allow-start-drag":{"identifier":"allow-start-drag","description":"Enables the start_drag command without any pre-configured scope.","commands":{"allow":["start_drag"],"deny":[]}},"deny-start-drag":{"identifier":"deny-start-drag","description":"Denies the start_drag command without any pre-configured scope.","commands":{"allow":[],"deny":["start_drag"]}}},"permission_sets":{},"global_scope_schema":null},"opener":{"default_permission":{"identifier":"default","description":"This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer","permissions":["allow-open-url","allow-reveal-item-in-dir","allow-default-urls"]},"permissions":{"allow-default-urls":{"identifier":"allow-default-urls","description":"This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"url":"mailto:*"},{"url":"tel:*"},{"url":"http://*"},{"url":"https://*"}]}},"allow-open-path":{"identifier":"allow-open-path","description":"Enables the open_path command without any pre-configured scope.","commands":{"allow":["open_path"],"deny":[]}},"allow-open-url":{"identifier":"allow-open-url","description":"Enables the open_url command without any pre-configured scope.","commands":{"allow":["open_url"],"deny":[]}},"allow-reveal-item-in-dir":{"identifier":"allow-reveal-item-in-dir","description":"Enables the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":["reveal_item_in_dir"],"deny":[]}},"deny-open-path":{"identifier":"deny-open-path","description":"Denies the open_path command without any pre-configured scope.","commands":{"allow":[],"deny":["open_path"]}},"deny-open-url":{"identifier":"deny-open-url","description":"Denies the open_url command without any pre-configured scope.","commands":{"allow":[],"deny":["open_url"]}},"deny-reveal-item-in-dir":{"identifier":"deny-reveal-item-in-dir","description":"Denies the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":[],"deny":["reveal_item_in_dir"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this url with, for example: firefox."},"url":{"description":"A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"","type":"string"}},"required":["url"],"type":"object"},{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this path with, for example: xdg-open."},"path":{"description":"A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"}},"required":["path"],"type":"object"}],"definitions":{"Application":{"anyOf":[{"description":"Open in default application.","type":"null"},{"description":"If true, allow open with any application.","type":"boolean"},{"description":"Allow specific application to open with.","type":"string"}],"description":"Opener scope application."}},"description":"Opener scope entry.","title":"OpenerScopeEntry"}}} \ No newline at end of file +{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"opener":{"default_permission":{"identifier":"default","description":"This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer","permissions":["allow-open-url","allow-reveal-item-in-dir","allow-default-urls"]},"permissions":{"allow-default-urls":{"identifier":"allow-default-urls","description":"This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"url":"mailto:*"},{"url":"tel:*"},{"url":"http://*"},{"url":"https://*"}]}},"allow-open-path":{"identifier":"allow-open-path","description":"Enables the open_path command without any pre-configured scope.","commands":{"allow":["open_path"],"deny":[]}},"allow-open-url":{"identifier":"allow-open-url","description":"Enables the open_url command without any pre-configured scope.","commands":{"allow":["open_url"],"deny":[]}},"allow-reveal-item-in-dir":{"identifier":"allow-reveal-item-in-dir","description":"Enables the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":["reveal_item_in_dir"],"deny":[]}},"deny-open-path":{"identifier":"deny-open-path","description":"Denies the open_path command without any pre-configured scope.","commands":{"allow":[],"deny":["open_path"]}},"deny-open-url":{"identifier":"deny-open-url","description":"Denies the open_url command without any pre-configured scope.","commands":{"allow":[],"deny":["open_url"]}},"deny-reveal-item-in-dir":{"identifier":"deny-reveal-item-in-dir","description":"Denies the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":[],"deny":["reveal_item_in_dir"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this url with, for example: firefox."},"url":{"description":"A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"","type":"string"}},"required":["url"],"type":"object"},{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this path with, for example: xdg-open."},"path":{"description":"A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"}},"required":["path"],"type":"object"}],"definitions":{"Application":{"anyOf":[{"description":"Open in default application.","type":"null"},{"description":"If true, allow open with any application.","type":"boolean"},{"description":"Allow specific application to open with.","type":"string"}],"description":"Opener scope application."}},"description":"Opener scope entry.","title":"OpenerScopeEntry"}}} \ No newline at end of file diff --git a/app/src-tauri/gen/schemas/capabilities.json b/app/src-tauri/gen/schemas/capabilities.json index bae6a07..ed7fb0c 100644 --- a/app/src-tauri/gen/schemas/capabilities.json +++ b/app/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"Default capabilities for Triple-C. Every entry here is an IPC command a compromised webview can call directly, so the set is an enumeration of what `app/src` actually invokes — verified against tauri 2.11.0's `PLUGINS` table in `build.rs`, not assumed from a plugin's `default` set. `core:default` in particular is NOT used: it is an alias for `core:{path,event,window,webview,app,image,resources,menu,tray}:default`, and `core:image:default` carries `allow-from-path`, whose handler (`tauri-2.11.0/src/image/plugin.rs:41` → `src/image/mod.rs:96`) is a bare `std::fs::read(path)` with no scope mechanism of any kind. Nothing imports `@tauri-apps/api/image`, so the whole plugin is dropped rather than scoped — there is nothing to scope it with. `core:menu` and `core:tray` are dropped for the same reason (no menu, no tray icon); `core:window` and `core:path` because nothing imports them; `core:resources:allow-close` because no frontend value is a `Resource` (`startDrag`'s `Channel` is not one); and `core:event`'s `allow-emit`/`allow-emit-to` because the frontend only ever *listens* — every emit in this app originates in Rust. Three notes on what is deliberately kept or accepted: (1) `core:webview:allow-internal-toggle-devtools` is not called by `app/src` at all — it is called by Tauri's own injected `toggle-devtools.js`, which binds Ctrl/Cmd+Shift+I. Both that script and the command behind it are `#[cfg(any(debug_assertions, feature = \"devtools\"))]`, so this grant is a `tauri dev` convenience that does not exist in a release bundle. (2) `opener:allow-open-url` cannot be narrowed by host. `TerminalView`'s `WebLinksAddon` opens links Claude printed inside the container, which are arbitrary by construction, so a host allowlist here would delete the feature rather than bound it. What *is* bounded: `opener:default` is not used, so `open_path` and `reveal_item_in_dir` are absent; the scope's two entries restrict the scheme to http/https (`file:`, `mailto:`, `tel:`, `smb:` are all refused by `Scope::is_url_allowed`); and because each entry leaves `app` at its serde default of `Application::Default`, which matches only `with == None`, `openUrl(url, \"/bin/sh\")` is refused — the `with` argument is not a usable exec primitive. The call sites re-validate through `sanitizeRelayUrl` (scheme allowlist, no embedded credentials, length cap) before anything reaches the opener. Accepted residual risk: a compromised webview can make the OS open an attacker-chosen http(s) URL, which is an outbound channel. Recorded here rather than fixed. (3) `drag:allow-start-drag` stays, and cannot be scoped — `tauri-plugin-drag` takes the item paths from the caller and has no scope mechanism, so a compromised webview could call `startDrag({ item: ['~/.ssh/id_rsa'] })` against any host path the user can read. It is not a silent exfiltration primitive: the drag only delivers anything if the user completes a real drop onto a real target, and the OS shows the drag under the cursor while it is in flight. Removing it would remove drag-out from the Files pane (`stage_container_file_for_drag`), whose fallback is the explicit \"Save to host…\" action. Accepted residual risk. Historical note kept because it is easy to re-introduce: the `store:*` grants were removed — nothing in `app/src` uses `@tauri-apps/plugin-store`, and the plugin's `resolve_store_path` is a `PathBuf::push` against AppData, which `push` discards outright when handed an absolute path, so the grant was an arbitrary host-file read/write primitive (`plugin:store|load` + `set` + `save` on `~/.claude/settings.json` is host code execution). On the CSP side: `app.security.csp` in `tauri.conf.json` covers the shipped bundle, and there is deliberately no `devCsp`. `npm run tauri dev` loads the main document straight from Vite at `build.devUrl` (`http://localhost:1420`), and Tauri only attaches a CSP to documents it serves itself — `protocol/tauri.rs:217` sets the header on `tauri://` assets, and the dev server is proxied through that protocol only when `PROXY_DEV_SERVER`, which is `cfg!(all(dev, mobile))` and therefore false for every desktop build. A `devCsp` here would be inert config that reads as protection, which is worse than its absence. If a CSP in dev is wanted, the only place that can set one is the Vite dev server's own `server.headers` in `app/vite.config.ts`; it is not set today, and dev is not the shipped configuration.","local":true,"windows":["main"],"permissions":["core:event:allow-listen","core:event:allow-unlisten","core:webview:allow-internal-toggle-devtools","dialog:allow-open","dialog:allow-save",{"identifier":"opener:allow-open-url","allow":[{"url":"http://*"},{"url":"https://*"}]},"drag:allow-start-drag"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"Default capabilities for Triple-C. Every entry here is an IPC command a compromised webview can call directly, so the set is an enumeration of what `app/src` actually invokes — verified against tauri 2.11.0's `PLUGINS` table in `build.rs`, not assumed from a plugin's `default` set. `core:default` in particular is NOT used: it is an alias for `core:{path,event,window,webview,app,image,resources,menu,tray}:default`, and `core:image:default` carries `allow-from-path`, whose handler (`tauri-2.11.0/src/image/plugin.rs:41` → `src/image/mod.rs:96`) is a bare `std::fs::read(path)` with no scope mechanism of any kind. Nothing imports `@tauri-apps/api/image`, so the whole plugin is dropped rather than scoped — there is nothing to scope it with. `core:menu` and `core:tray` are dropped for the same reason (no menu, no tray icon); `core:window` and `core:path` because nothing imports them; `core:resources:allow-close` because no frontend value is a `Resource`; and `core:event`'s `allow-emit`/`allow-emit-to` because the frontend only ever *listens* — every emit in this app originates in Rust. Three notes on what is deliberately kept or accepted: (1) `core:webview:allow-internal-toggle-devtools` is not called by `app/src` at all — it is called by Tauri's own injected `toggle-devtools.js`, which binds Ctrl/Cmd+Shift+I. Both that script and the command behind it are `#[cfg(any(debug_assertions, feature = \"devtools\"))]`, so this grant is a `tauri dev` convenience that does not exist in a release bundle. (2) `opener:allow-open-url` cannot be narrowed by host. `TerminalView`'s `WebLinksAddon` opens links Claude printed inside the container, which are arbitrary by construction, so a host allowlist here would delete the feature rather than bound it. What *is* bounded: `opener:default` is not used, so `open_path` and `reveal_item_in_dir` are absent; the scope's two entries restrict the scheme to http/https (`file:`, `mailto:`, `tel:`, `smb:` are all refused by `Scope::is_url_allowed`); and because each entry leaves `app` at its serde default of `Application::Default`, which matches only `with == None`, `openUrl(url, \"/bin/sh\")` is refused — the `with` argument is not a usable exec primitive. The call sites re-validate through `sanitizeRelayUrl` (scheme allowlist, no embedded credentials, length cap) before anything reaches the opener. Accepted residual risk: a compromised webview can make the OS open an attacker-chosen http(s) URL, which is an outbound channel. Recorded here rather than fixed. (3) `drag:allow-start-drag` is **gone**, together with the OS drag-out it existed for. It could not be scoped — `tauri-plugin-drag` takes the item paths from the caller and has no scope mechanism, so a compromised webview could call `startDrag({ item: ['~/.ssh/id_rsa'] })` against any host path the user can read — and it was carried as an accepted residual risk for one gesture. Drag-out was held back for separate hardening (see branch `hold/disk-and-dragout`), the plugin is no longer a dependency, and getting a file out of a container is now the explicit \"Save to host…\" action, which never touches this permission. Note that dragging files *into* the app is unaffected: `dragDropEnabled` and `onDragDropEvent` are core webview behaviour and need no grant. Historical note kept because it is easy to re-introduce: the `store:*` grants were removed — nothing in `app/src` uses `@tauri-apps/plugin-store`, and the plugin's `resolve_store_path` is a `PathBuf::push` against AppData, which `push` discards outright when handed an absolute path, so the grant was an arbitrary host-file read/write primitive (`plugin:store|load` + `set` + `save` on `~/.claude/settings.json` is host code execution). On the CSP side: `app.security.csp` in `tauri.conf.json` covers the shipped bundle, and there is deliberately no `devCsp`. `npm run tauri dev` loads the main document straight from Vite at `build.devUrl` (`http://localhost:1420`), and Tauri only attaches a CSP to documents it serves itself — `protocol/tauri.rs:217` sets the header on `tauri://` assets, and the dev server is proxied through that protocol only when `PROXY_DEV_SERVER`, which is `cfg!(all(dev, mobile))` and therefore false for every desktop build. A `devCsp` here would be inert config that reads as protection, which is worse than its absence. If a CSP in dev is wanted, the only place that can set one is the Vite dev server's own `server.headers` in `app/vite.config.ts`; it is not set today, and dev is not the shipped configuration.","local":true,"windows":["main"],"permissions":["core:event:allow-listen","core:event:allow-unlisten","core:webview:allow-internal-toggle-devtools","dialog:allow-open","dialog:allow-save",{"identifier":"opener:allow-open-url","allow":[{"url":"http://*"},{"url":"https://*"}]}]}} \ No newline at end of file diff --git a/app/src-tauri/gen/schemas/desktop-schema.json b/app/src-tauri/gen/schemas/desktop-schema.json index 0412071..156390b 100644 --- a/app/src-tauri/gen/schemas/desktop-schema.json +++ b/app/src-tauri/gen/schemas/desktop-schema.json @@ -2426,24 +2426,6 @@ "const": "dialog:deny-save", "markdownDescription": "Denies the save command without any pre-configured scope." }, - { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-start-drag`", - "type": "string", - "const": "drag:default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-start-drag`" - }, - { - "description": "Enables the start_drag command without any pre-configured scope.", - "type": "string", - "const": "drag:allow-start-drag", - "markdownDescription": "Enables the start_drag command without any pre-configured scope." - }, - { - "description": "Denies the start_drag command without any pre-configured scope.", - "type": "string", - "const": "drag:deny-start-drag", - "markdownDescription": "Denies the start_drag command without any pre-configured scope." - }, { "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", "type": "string", diff --git a/app/src-tauri/gen/schemas/linux-schema.json b/app/src-tauri/gen/schemas/linux-schema.json index 0412071..156390b 100644 --- a/app/src-tauri/gen/schemas/linux-schema.json +++ b/app/src-tauri/gen/schemas/linux-schema.json @@ -2426,24 +2426,6 @@ "const": "dialog:deny-save", "markdownDescription": "Denies the save command without any pre-configured scope." }, - { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-start-drag`", - "type": "string", - "const": "drag:default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-start-drag`" - }, - { - "description": "Enables the start_drag command without any pre-configured scope.", - "type": "string", - "const": "drag:allow-start-drag", - "markdownDescription": "Enables the start_drag command without any pre-configured scope." - }, - { - "description": "Denies the start_drag command without any pre-configured scope.", - "type": "string", - "const": "drag:deny-start-drag", - "markdownDescription": "Denies the start_drag command without any pre-configured scope." - }, { "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", "type": "string", diff --git a/app/src-tauri/src/commands/docker_commands.rs b/app/src-tauri/src/commands/docker_commands.rs index 6dcc687..d8f7a32 100644 --- a/app/src-tauri/src/commands/docker_commands.rs +++ b/app/src-tauri/src/commands/docker_commands.rs @@ -54,80 +54,3 @@ pub async fn list_sibling_containers() -> Result, String> .collect(); Ok(result) } - -// --------------------------------------------------------------------------- -// Disk -// --------------------------------------------------------------------------- -// -// The disk view's IPC surface. It lives here rather than in a module of its own -// for the same reason `check_image_exists` does: these are thin shims over -// `crate::docker`, and the logic they call is in `docker/disk.rs` where it can -// be unit-tested without a daemon. - -/// Measure where the daemon's bytes have gone. -/// -/// **Expensive on purpose.** This is `GET /system/df` plus an `image_history` -/// per distinct image, and `df()` walks every image, container and volume on -/// the daemon to compute shared-layer sizes. On a 100 GB store that is seconds. -/// The frontend must keep it behind an explicit Scan button — never on panel -/// open, never on a timer. -#[tauri::command] -pub async fn get_docker_disk_usage( - state: State<'_, AppState>, -) -> Result { - let projects = state.projects_store.list(); - docker::disk::scan(&projects).await -} - -/// Everything that could be reclaimed, each with its measured cost. -/// -/// Takes the report from [`get_docker_disk_usage`] rather than re-measuring, so -/// a user who re-plans after ticking a box does not pay for a second `df()`. -#[tauri::command] -pub async fn list_reclaimable( - report: docker::disk::DiskUsageReport, - state: State<'_, AppState>, -) -> Result { - let projects = state.projects_store.list(); - docker::disk::list_reclaimable(&projects, &report).await -} - -/// Run the ticked targets and report what each one actually freed. -/// -/// `ReclaimTarget` cannot express a destructive action — that is a different -/// type, reached only through [`destroy_project_disk_object`] with a typed -/// confirmation — so there is no selection a user can build here that deletes a -/// live project's data. -#[tauri::command] -pub async fn reclaim( - targets: Vec, - state: State<'_, AppState>, -) -> Result { - let projects = state.projects_store.list(); - Ok(docker::disk::reclaim(&targets, &projects).await) -} - -/// Delete one object that has no other copy, against a typed confirmation of -/// the project's name. -/// -/// Deliberately one target per call: this is never part of a bulk action. -#[tauri::command] -pub async fn destroy_project_disk_object( - target: docker::disk::DestructiveTarget, - confirmation: String, - state: State<'_, AppState>, -) -> Result { - let projects = state.projects_store.list(); - docker::disk::destroy(&target, &confirmation, &projects).await -} - -/// Run the orphaned-snapshot sweep on demand and return its report. -/// -/// The sweep already runs at startup, after every recreation and after a -/// migration settles, but every one of those callers throws the report away — -/// so a user has never been able to see that 11.9 GB of superseded images were -/// found and left because a stopped container still pinned them. -#[tauri::command] -pub async fn sweep_orphaned_snapshots() -> Result { - Ok(docker::sweep_orphaned_snapshots().await) -} diff --git a/app/src-tauri/src/commands/file_commands.rs b/app/src-tauri/src/commands/file_commands.rs index 48cdbff..6b8f476 100644 --- a/app/src-tauri/src/commands/file_commands.rs +++ b/app/src-tauri/src/commands/file_commands.rs @@ -1,7 +1,6 @@ use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, OnceLock}; -use std::time::{Duration, SystemTime}; +use std::sync::Arc; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine as _; @@ -9,7 +8,7 @@ use bollard::container::{DownloadFromContainerOptions, LogOutput, UploadToContai use bollard::exec::{CreateExecOptions, StartExecResults}; use futures_util::StreamExt; use serde::Serialize; -use tauri::{AppHandle, Manager, State}; +use tauri::State; use crate::docker::client::get_docker; use crate::docker::exec::{ @@ -1271,241 +1270,6 @@ pub async fn read_container_file( }) } -// ───────────────────────────────────────────────────────────────────────────── -// Drag-out staging -// ───────────────────────────────────────────────────────────────────────────── -// -// Dragging a file onto the host desktop hands the OS a *host* path, and the -// files in this panel live inside a container, where nothing on the desktop can -// reach them. So a drag-out is really a copy-then-drag: materialise the file -// into a host temp directory first, then start the native drag on that copy. -// -// The copy is the reason this section carries a lifecycle. A staging directory -// nobody empties is a disk leak with a gesture attached to it, so there are two -// halves and both matter: `clear_drag_staging` on exit, and -// `reap_drag_staging` at startup for whatever a crash left behind. - -/// Ceiling on one staged copy. Deliberately the same 256 MiB as -/// [`MAX_UPLOAD_BYTES`] — it is the same whole-file-through-host-RAM round trip, -/// only in the other direction. -const MAX_DRAG_STAGE_BYTES: u64 = 256 * 1024 * 1024; - -/// Name of the app-owned directory inside the OS temp dir. Everything staged by -/// any Triple-C process lives under it, so housekeeping has exactly one place to -/// look and never walks the rest of the user's temp dir. -const DRAG_STAGE_DIR_NAME: &str = "triple-c-drag-out"; - -/// How long *another* process's leftover staging directory may sit before -/// startup housekeeping deletes it. -/// -/// Only ever applied to directories this process does not own (see -/// [`drag_stage_session_dir`]), so it is not a limit on how long a staged file -/// survives in a live session — it is the crash-recovery threshold, and it is -/// generous because a second Triple-C running right now would also look like a -/// leftover. -const DRAG_STAGE_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60); - -/// This process's own sub-directory name, stable for the life of the process. -/// -/// Per-process rather than shared so exit cleanup can delete *ours* outright -/// without reaching into a directory another instance may be dragging out of. -fn drag_stage_session() -> &'static str { - static SESSION: OnceLock = OnceLock::new(); - SESSION.get_or_init(|| uuid::Uuid::new_v4().to_string()) -} - -/// The app-owned staging root inside `temp_dir`. -/// -/// Takes the temp dir rather than reading it, because on Windows it is neither -/// `/tmp` nor a constant — Tauri's path API is the only thing that knows it — -/// and because a pure function is what the tests can drive. -pub fn drag_stage_root(temp_dir: &Path) -> PathBuf { - temp_dir.join(DRAG_STAGE_DIR_NAME) -} - -/// This process's staging directory: `/triple-c-drag-out/`. -pub fn drag_stage_session_dir(temp_dir: &Path) -> PathBuf { - drag_stage_root(temp_dir).join(drag_stage_session()) -} - -/// The per-file sub-directory a staged copy lives in, derived from the -/// container path. -/// -/// Filenames are only unique within a directory, so `a/notes.txt` and -/// `b/notes.txt` would otherwise be the same host path — and the second drag -/// would silently rewrite the first one's contents under the first one's cached -/// path. A digest of the full container path separates them while staying -/// *deterministic*, so re-staging the same file reuses its slot instead of -/// growing a new one every drag. -fn drag_stage_slot(container_path: &str) -> String { - use sha2::{Digest, Sha256}; - let digest = Sha256::digest(container_path.as_bytes()); - digest[..8].iter().map(|b| format!("{:02x}", b)).collect() -} - -/// The name the staged copy is given on the host. -/// -/// The whole point is that what lands on the desktop is called `notes.txt` and -/// not `tmp1234`, so the container's basename is kept verbatim wherever it can -/// be. Only the characters Windows refuses outright are substituted — a Linux -/// file really can be called `a:b`, and the staged copy has to exist on NTFS. -/// A name that is not a filename at all (empty, `.`, `..`) is rejected rather -/// than invented: that means the caller passed something that never named a -/// file, and quietly inventing a name would stage the wrong thing. -fn stage_file_name(container_path: &str) -> Result { - let base = container_path - .trim_end_matches('/') - .rsplit('/') - .next() - .unwrap_or(""); - - let cleaned: String = base - .chars() - .map(|c| match c { - '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*' => '_', - c if (c as u32) < 0x20 => '_', - c => c, - }) - .collect(); - // Windows also silently drops a trailing dot or space, which would make the - // path we hand back not the path that exists. - let cleaned = cleaned.trim_end_matches([' ', '.']); - - if cleaned.is_empty() || cleaned == "." || cleaned == ".." { - return Err(format!("{} does not name a file", container_path)); - } - Ok(cleaned.to_string()) -} - -/// Reject an oversize file *by its real size*, before anything is written. -/// -/// Split out so the ceiling and its wording are testable without a container. -/// The message names the fallback, because "too large" with no way forward is -/// the one thing a size cap must not be. -fn check_stage_size(size: u64) -> Result<(), String> { - if size > MAX_DRAG_STAGE_BYTES { - return Err(format!( - "{:.0} MB is too large to drag out (limit {} MB) — use \"Save to host…\" instead.", - size as f64 / (1024.0 * 1024.0), - MAX_DRAG_STAGE_BYTES / (1024 * 1024) - )); - } - Ok(()) -} - -/// Whether a leftover staging directory is old enough to delete. -/// -/// A modification time in the *future* (a clock step, a copied temp dir) makes -/// `duration_since` fail, and that answers "not stale" — housekeeping deleting -/// something it cannot date is worse than leaving it for the next startup. -fn drag_stage_is_stale(modified: SystemTime, now: SystemTime, max_age: Duration) -> bool { - now.duration_since(modified) - .map(|age| age >= max_age) - .unwrap_or(false) -} - -/// Delete every staging directory except this process's own, once it is older -/// than [`DRAG_STAGE_MAX_AGE`]. Called from startup housekeeping. -pub async fn reap_drag_staging(temp_dir: PathBuf) { - let root = drag_stage_root(&temp_dir); - let keep = drag_stage_session_dir(&temp_dir); - let now = SystemTime::now(); - - let mut dir = match tokio::fs::read_dir(&root).await { - Ok(dir) => dir, - // Nothing staged yet is the normal case, not a problem. - Err(_) => return, - }; - - let mut reaped = 0usize; - while let Ok(Some(entry)) = dir.next_entry().await { - let path = entry.path(); - if path == keep { - continue; - } - let stale = match entry.metadata().await.and_then(|m| m.modified()) { - Ok(modified) => drag_stage_is_stale(modified, now, DRAG_STAGE_MAX_AGE), - Err(_) => false, - }; - if !stale { - continue; - } - if tokio::fs::remove_dir_all(&path).await.is_ok() { - reaped += 1; - } - } - - if reaped > 0 { - log::info!("Startup housekeeping removed {} stale drag-out staging directory(ies)", reaped); - } -} - -/// Delete this process's staging directory. Called from the shutdown teardown. -pub async fn clear_drag_staging(temp_dir: PathBuf) { - let dir = drag_stage_session_dir(&temp_dir); - if let Err(e) = tokio::fs::remove_dir_all(&dir).await { - if e.kind() != std::io::ErrorKind::NotFound { - log::warn!("Failed to clear drag-out staging at {}: {}", dir.display(), e); - } - } - // Best effort: leave no empty root behind either. Fails harmlessly while - // another instance still has a directory in there. - let _ = tokio::fs::remove_dir(drag_stage_root(&temp_dir)).await; -} - -/// Copy a container file onto the host so it can be dragged to the desktop, and -/// return the absolute host path. -/// -/// Reuses [`fetch_container_file`] rather than extracting a second way, so a -/// dragged file, a downloaded file and a previewed file are byte-identical and -/// refuse folders and links with the same words. The fetch is capped at -/// [`MAX_DRAG_STAGE_BYTES`], so an oversize file is recognised from the tar -/// header without being pulled across the socket in full. -#[tauri::command] -pub async fn stage_container_file_for_drag( - app: AppHandle, - project_id: String, - path: String, - state: State<'_, AppState>, -) -> Result { - validate_container_path("File", &path)?; - - let project = state - .projects_store - .get(&project_id) - .ok_or_else(|| format!("Project {} not found", project_id))?; - - let container_id = project - .container_id - .as_ref() - .ok_or_else(|| "Container not running".to_string())?; - - // Before the transfer: a path that cannot become a host filename is not - // worth a round trip. - let file_name = stage_file_name(&path)?; - - let fetched = fetch_container_file(container_id, &path, MAX_DRAG_STAGE_BYTES).await?; - // `size` is the tar entry's, i.e. the file's real size, which is exactly - // what a truncated fetch does not tell you from `bytes.len()`. - check_stage_size(fetched.size)?; - - let temp_dir = app - .path() - .temp_dir() - .map_err(|e| format!("No host temporary directory available: {}", e))?; - let dir = drag_stage_session_dir(&temp_dir).join(drag_stage_slot(&path)); - tokio::fs::create_dir_all(&dir) - .await - .map_err(|e| format!("Failed to create the drag staging directory: {}", e))?; - - let dest = dir.join(&file_name); - tokio::fs::write(&dest, &fetched.bytes) - .await - .map_err(|e| format!("Failed to stage {} on the host: {}", file_name, e))?; - - Ok(dest.to_string_lossy().to_string()) -} - /// Rename an entry in place. `to_path` is the **new name**, not a destination /// path — moving between directories is deliberately not offered here, so the /// name is validated to carry no `/`. @@ -2425,8 +2189,7 @@ mod tests { // `fetch_container_file` takes a plain `u64` now, so the `None` that // made the cap inert cannot be written again. These are the two callers // left, and both buffer. - assert!(MAX_READ_BYTES <= MAX_DRAG_STAGE_BYTES); - assert!(MAX_DRAG_STAGE_BYTES < MAX_DOWNLOAD_BYTES); + assert!(MAX_READ_BYTES < MAX_DOWNLOAD_BYTES); } #[test] @@ -2445,116 +2208,6 @@ mod tests { ); } - // ── Drag-out staging ──────────────────────────────────────────────────── - - #[test] - fn the_staging_path_is_built_under_the_supplied_temp_dir() { - // Never `/tmp`: on Windows the temp dir is per-user and nowhere near it, - // so the whole path has to be derived from what Tauri hands us. - let temp = Path::new("/somewhere/else"); - let root = drag_stage_root(temp); - assert_eq!(root, Path::new("/somewhere/else/triple-c-drag-out")); - - let session = drag_stage_session_dir(temp); - assert_eq!(session.parent(), Some(root.as_path())); - assert!(session.starts_with(root)); - } - - #[test] - fn every_call_in_a_process_stages_into_the_same_session_directory() { - // Exit cleanup deletes this directory by name rather than tracking what - // it wrote, which only works if the name does not move. - let temp = Path::new("/tmp-ish"); - assert_eq!(drag_stage_session_dir(temp), drag_stage_session_dir(temp)); - assert_ne!(drag_stage_session_dir(temp), drag_stage_root(temp)); - } - - #[test] - fn the_staged_copy_keeps_the_original_file_name() { - // The reason the feature stages into a per-session directory at all: a - // plain temp file would be dropped onto the desktop called `tmp1234`. - assert_eq!(stage_file_name("/workspace/notes.txt").unwrap(), "notes.txt"); - assert_eq!(stage_file_name("/workspace/a b/.env").unwrap(), ".env"); - assert_eq!(stage_file_name("report.pdf").unwrap(), "report.pdf"); - assert_eq!(stage_file_name("/workspace/über.md").unwrap(), "über.md"); - } - - #[test] - fn a_name_windows_cannot_hold_is_substituted_rather_than_dropped() { - // These are all legal on Linux and all refused by NTFS, and the staged - // copy has to exist on the host we are dragging onto. - assert_eq!(stage_file_name("/workspace/a:b.txt").unwrap(), "a_b.txt"); - assert_eq!(stage_file_name("/workspace/q?.log").unwrap(), "q_.log"); - assert_eq!(stage_file_name("/workspace/a\\b").unwrap(), "a_b"); - // A trailing dot or space is not refused, it is silently dropped — so - // the path we return would not be the path that exists. - assert_eq!(stage_file_name("/workspace/trailing. ").unwrap(), "trailing"); - } - - #[test] - fn a_path_that_does_not_name_a_file_is_refused_not_invented() { - assert!(stage_file_name("/").is_err()); - assert!(stage_file_name("").is_err()); - assert!(stage_file_name("/workspace/..").is_err()); - assert!(stage_file_name("/workspace/.").is_err()); - // Trims down to nothing, which is the same problem one step later. - assert!(stage_file_name("/workspace/...").is_err()); - } - - #[test] - fn two_files_with_the_same_name_stage_to_different_places() { - // Names are unique per directory, not per container — and the second - // drag would otherwise rewrite the first one's bytes under the path the - // first one is still cached at. - assert_ne!( - drag_stage_slot("/workspace/a/notes.txt"), - drag_stage_slot("/workspace/b/notes.txt") - ); - } - - #[test] - fn re_staging_the_same_file_reuses_its_slot() { - // Deterministic, so a file dragged repeatedly does not grow a new - // directory in the host temp dir every time. - assert_eq!( - drag_stage_slot("/workspace/notes.txt"), - drag_stage_slot("/workspace/notes.txt") - ); - // Short enough to keep the path sane, long enough not to collide. - assert_eq!(drag_stage_slot("/workspace/notes.txt").len(), 16); - } - - #[test] - fn the_drag_size_cap_matches_the_established_ceiling_and_names_the_fallback() { - assert_eq!(MAX_DRAG_STAGE_BYTES, MAX_UPLOAD_BYTES); - assert!(check_stage_size(MAX_DRAG_STAGE_BYTES).is_ok()); - - let err = check_stage_size(MAX_DRAG_STAGE_BYTES + 1).unwrap_err(); - assert!(err.contains("256 MB"), "{}", err); - // A size cap with no way forward is the one thing this must not be. - assert!(err.contains("Save to host"), "{}", err); - } - - #[test] - fn the_reaper_only_takes_entries_past_the_age_threshold() { - let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000); - let age = Duration::from_secs(3_600); - - assert!(drag_stage_is_stale(now - Duration::from_secs(3_601), now, age)); - assert!(drag_stage_is_stale(now - age, now, age)); - assert!(!drag_stage_is_stale(now - Duration::from_secs(3_599), now, age)); - assert!(!drag_stage_is_stale(now, now, age)); - } - - #[test] - fn a_future_timestamp_is_left_alone_rather_than_reaped() { - // A clock step must not turn housekeeping into deletion of something it - // cannot date. - let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000); - let age = Duration::from_secs(3_600); - assert!(!drag_stage_is_stale(now + Duration::from_secs(60), now, age)); - } - // ── Host path normalisation, on every platform ────────────────────────── #[test] diff --git a/app/src-tauri/src/commands/migration_commands.rs b/app/src-tauri/src/commands/migration_commands.rs index a312578..c9510e4 100644 --- a/app/src-tauri/src/commands/migration_commands.rs +++ b/app/src-tauri/src/commands/migration_commands.rs @@ -243,6 +243,13 @@ async fn container_label(container_id: &str, label: &str) -> Option { /// function is the specialisation of it that reconcile still needs: a *live* /// migration is indistinguishable from a crashed one from the outside, and only /// this process knows which it is looking at. +/// +/// No production caller on this branch: the Disk panel's survey was the last +/// one, and it went to `hold/disk-and-dragout`. Kept — and still exercised by +/// `a_live_migration_is_distinguishable_from_a_crashed_one` — because it is the +/// one named answer to that question and re-inventing it is how the two +/// disagreeing answers happened the first time. +#[allow(dead_code)] pub(crate) fn is_migrating(project_id: &str) -> bool { crate::project_lock::is_held_by(project_id, crate::project_lock::ProjectOp::Migration) } diff --git a/app/src-tauri/src/docker/container.rs b/app/src-tauri/src/docker/container.rs index 1cdc019..cc9591d 100644 --- a/app/src-tauri/src/docker/container.rs +++ b/app/src-tauri/src/docker/container.rs @@ -2184,8 +2184,8 @@ pub(crate) fn snapshot_scrub_script() -> String { /// expands to itself and is skipped by the existence guard. /// /// Passing the whole pattern rather than the two halves also keeps each entry -/// readable verbatim in the compaction `RUN` line — `disk.rs` asserts exactly -/// that, to catch a second forked copy of the list. +/// readable verbatim in the generated script, so a test can assert the script +/// names this list rather than a second forked copy of it. /// /// ## The containment guarantee (C1) /// @@ -2258,15 +2258,15 @@ pub(crate) fn snapshot_scrub_script() -> String { /// which the agent's passwordless sudo can. It closes the part of the gap that /// survives a container restart and needs no privileges at all. /// -/// ## Why every line ends in `;` +/// ## Why every line ends in `;`, and why there are no `#` comments /// -/// `disk.rs` folds this script onto a single `RUN` line for the compaction -/// build, joining non-blank lines with a space. That is only a join and not a -/// rewrite if each line already terminates its own statement — the previous -/// version did not, and its folded form was a `"do" unexpected` syntax error, -/// so compaction had been running no scrub at all. It also means the script -/// carries **no `#` comments**: folded, one would swallow the rest of the -/// program. A test pins both the multi-line and the folded form. +/// A self-terminating statement per line, and no comments, is what makes the +/// script safe to join onto one line: any embedder that folds it with spaces +/// gets a join rather than a rewrite. That property was learnt the hard way — +/// an earlier version's folded form was a `"do" unexpected` syntax error, so +/// the scrub ran not at all — and it is kept even though the folding caller is +/// gone, because a script that survives being flattened is the cheap invariant +/// and re-learning it is not. /// /// ## Why `root` exists /// @@ -2903,8 +2903,8 @@ pub async fn scrub_secrets_from_snapshots() -> SnapshotScrubReport { // Claim the project before touching its snapshot. // - // This is the third writer of `triple-c-snapshot-{id}:latest`, after a - // recreate's commit and a compaction, and it has the same + // This is the second writer of `triple-c-snapshot-{id}:latest`, after a + // recreate's commit, and it has the same // read-modify-write shape: create a scratch container *from* the // snapshot, then commit back over the same tag. A `:latest` move // landing in between is silently overwritten by an image derived from @@ -4233,8 +4233,7 @@ mod tests { assert!(script.contains("scrub_in '/var/log/apt/*' '-';")); assert!(script.contains("scrub_in '/tmp/triple-c-drops/*' '14';")); // The parent/glob split happens in the shell, so every entry stays - // readable verbatim — `disk.rs` folds this onto one `RUN` line and - // asserts each pattern appears there rather than a forked copy. + // readable verbatim in the script rather than as a forked copy. for pattern in SNAPSHOT_SCRUB_PATHS { assert!(script.contains(pattern), "{} is not named in the script", pattern); } @@ -4572,51 +4571,6 @@ mod tests { assert!(SCRUB_TIMEOUT.as_secs() <= 300, "long enough that a user would force-quit first"); } - /// `disk.rs` folds this script onto one `RUN` line for the compaction - /// build by joining its non-blank lines with a space, so the script has to - /// be a sequence of self-terminating statements and carry no `#` comments. - /// The previous version was neither: its folded form was a `"do" - /// unexpected` syntax error, which means compaction had been scrubbing - /// nothing at all. The fold is reproduced here rather than imported - /// because it is private to the other module — a divergence would show up - /// as this test passing while the real Dockerfile broke, so it is pinned - /// against the same wording in `fold_shell_script`. - #[cfg(unix)] - #[test] - fn a_compaction_runs_this_module_s_scrub_script_byte_for_byte() { - // The compaction build used to fold the script onto one `RUN` line by - // joining its lines with a space, which turned `for p in …; do` into - // `do` in statement position and made every compaction fail with - // `syntax error: unexpected "do"`. That fold is gone — `disk.rs` now - // emits the JSON exec form, whose string escapes carry newlines — so - // the assertion worth pinning from this side is no longer "the folded - // one-liner still parses" but the stronger one: whatever encoding - // `disk.rs` chooses, the bytes that reach `sh` are *this* script. - // - // This is what stops the two files drifting. `container.rs` owns the - // containment rules in `snapshot_scrub_script`; a compaction that ran a - // mangled copy would be running a scrub with those rules altered, and - // the mangling would be silent. - let expected = snapshot_scrub_script(); - // Build the real Dockerfile the compaction would, then pull the script - // back out of it — going through `compaction_dockerfile` rather than a - // helper means a change to how the RUN line is emitted is caught here. - let dockerfile = crate::docker::disk::compaction_dockerfile( - "triple-c-snapshot-00000000-0000-0000-0000-000000000000:latest", - &expected, - ); - let run_line = dockerfile - .lines() - .find(|l| l.starts_with("RUN ")) - .expect("the compaction Dockerfile should carry a RUN line"); - let actual = crate::docker::disk::script_from_run_line(run_line) - .expect("the compaction RUN line should be the JSON exec form"); - assert_eq!( - actual, expected, - "the compaction runs a different script than snapshot_scrub_script() produces" - ); - } - #[test] fn every_container_is_created_with_a_bounded_log() { let cfg = capped_log_config(); diff --git a/app/src-tauri/src/docker/disk.rs b/app/src-tauri/src/docker/disk.rs deleted file mode 100644 index da37e02..0000000 --- a/app/src-tauri/src/docker/disk.rs +++ /dev/null @@ -1,4295 +0,0 @@ -//! Disk accounting and reclaim for the objects Triple-C creates. -//! -//! ## The problem this exists to make visible -//! -//! Every recreation runs `docker commit`, and a commit **stacks a new layer** -//! rather than rewriting one. A file deleted after it has been committed does -//! not give its bytes back — the layer above records a whiteout and the -//! original bytes stay below it forever. `container_needs_recreation` has 24 -//! conditions, so changing one settings field costs a multi-gigabyte layer that -//! nothing in the app ever reclaims. One project was measured at 14 stacked -//! commit layers, ~5.1 GB above its base, 12.3 GB total. -//! -//! Prevention already landed (the pre-commit scrub, capped container logs, -//! base-image labels, the startup sweep, the migration-pin reaper). What was -//! missing is the half a user can act on: *seeing* where the bytes are, and -//! being able to get them back. That is this module. -//! -//! **Making the layer count visible is the point.** "Snapshot 12.3 GB / 14 -//! layers / next commit adds 868 MB" explains the growth mechanism in one row, -//! which no total ever does. -//! -//! ## Why the scan is explicit -//! -//! [`scan`] is built on `Docker::df()` (`GET /system/df`), which walks every -//! image, container and volume on the daemon and computes shared-layer sizes. -//! On a 100 GB store that is seconds, not milliseconds, and it is the *only* -//! call that populates `ImageSummary::shared_size`, `ContainerSummary::size_rw` -//! and `VolumeUsageData::size` at all. So it sits behind a Scan button and is -//! never run on panel open or on a timer. -//! -//! ## Safety, which is the whole design -//! -//! Reclaim targets are split across **two Rust types that cannot be confused -//! for one another**: -//! -//! * [`ReclaimTarget`] — safe and semi-safe work. Everything here is either -//! already unreachable (dangling images, ownerless rollback pins, probe and -//! scrub leftovers), regenerable (build cache, package caches), or a rewrite -//! that preserves content (snapshot compaction). [`reclaim`] accepts these. -//! * [`DestructiveTarget`] — a live project's home volume, config volume, -//! snapshot image, or a rollback pin whose migration is still awaiting -//! confirmation. [`destroy`] accepts these, one at a time, and only against a -//! typed confirmation of the project name. -//! -//! `reclaim` does not have a code path that can reach a `DestructiveTarget` — -//! it cannot be passed one. That is deliberate: the guarantee is in the type -//! system rather than in a runtime check somebody can forget to write. -//! -//! Two rules apply throughout, and both are inherited from -//! `sweep_orphaned_snapshots`: -//! -//! * **Never call an unfiltered prune.** `prune_images`/`prune_volumes` without -//! filters would reach the user's own postgres, mysql and site-builder work -//! on the same daemon. Every removal here names one object we created. -//! * **Only ever touch a `triple-c*` name or a `triple-c.*` label.** - -use std::collections::{HashMap, HashSet}; - -use bollard::container::ListContainersOptions; -use bollard::image::{ListImagesOptions, RemoveImageOptions}; -use bollard::models::{BuildCache, ContainerSummary, ImageSummary, Volume}; -use serde::{Deserialize, Serialize}; - -use super::client::get_docker; -use super::container::{ - self, config_volume_name, home_volume_name, get_snapshot_image_name, CONFIG_VOLUME_PREFIX, - HOME_VOLUME_PREFIX, LABEL_BASE, LABEL_MANAGED, -}; -use super::migration; -use crate::models::Project; -use crate::storage::migration_store; - -/// Default age filter for a build-cache prune, matching `docker builder prune -/// --filter until=168h`. A week is long enough that an active build tree keeps -/// its warm cache and short enough that abandoned trees are collected. -pub const BUILD_CACHE_DEFAULT_UNTIL_HOURS: i64 = 168; - -/// How long [`docker_cli`] waits for the `docker` command line tool. -/// -/// Long enough for `buildx du` over a large build tree on a cold daemon, short -/// enough that a wedged daemon does not hold the Scan button down forever. A -/// prune uses the same bound; a prune that outruns it has still done its work -/// on the daemon, and the next scan reports the result. -const DOCKER_CLI_TIMEOUT_SECS: u64 = 45; - -/// The bound for a `builder prune`, which is a different kind of wait. -/// -/// `buildx du` is a query and 45 seconds is generous for it. A prune of a -/// 60 GB cache genuinely takes minutes, and cancelling it does not undo the -/// daemon-side work — it only loses the `Total reclaimed space:` line, so the -/// run would be reported as a failure that in fact freed the space. Ten minutes -/// is a bound against a wedged daemon rather than against a slow one. -const DOCKER_PRUNE_TIMEOUT_SECS: u64 = 600; - -// --------------------------------------------------------------------------- -// Scan result -// --------------------------------------------------------------------------- - -/// One row of the per-project table — the mental model users actually have. -/// -/// Serde is plain snake_case, matching every other IPC struct in this crate and -/// `app/src/lib/types.ts`. -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -pub struct ProjectDiskRow { - pub project_id: String, - pub project_name: String, - /// `triple-c-snapshot-{id}:latest`, present whether or not it exists yet. - pub snapshot_image: String, - pub snapshot_exists: bool, - /// Total size of the snapshot image, base image included. - pub snapshot_bytes: i64, - /// Bytes of the snapshot that are *also* in some other image — almost - /// always the shared base. Only `df()` computes this. - pub snapshot_shared_bytes: i64, - /// How many layers the snapshot has stacked **above its base image**. This - /// is the number that explains the growth: one per recreation. - /// - /// Only means that when [`Self::base_lineage_known`] is true. Otherwise it - /// is every layer carrying bytes, base included — an upper bound, and a - /// misleading one to present as a recreation count. - pub snapshot_commit_layers: u32, - /// Whether the base image this snapshot descends from could be identified. - /// - /// False when `triple-c.base-image-id` is absent, which is the **normal** - /// case for a project created before that label existed. The UI must not - /// present `snapshot_commit_layers` as a recreation count in that state, - /// and compaction is not offered, because a never-recreated project would - /// otherwise report its base's ~15 layers and qualify. - pub base_lineage_known: bool, - /// Bytes those stacked layers account for. `None` when the base image the - /// snapshot descends from is no longer on the daemon, so the split cannot - /// be measured and must not be guessed. - pub snapshot_above_base_bytes: Option, - pub container_exists: bool, - pub container_running: bool, - /// The container's writable layer — i.e. **exactly what the next commit - /// will add** to the snapshot. Surfaced under that name in the UI. - pub container_writable_bytes: i64, - pub home_volume_bytes: i64, - pub home_volume_present: bool, - pub config_volume_bytes: i64, - pub config_volume_present: bool, - /// **The one snapshot figure the row adds up from.** - /// - /// The Snapshot column shows this and [`Self::total_bytes`] is computed - /// from it, so the Total column reconciles with its parts. It did not - /// before: `total_bytes` used `size - shared_size` unconditionally while - /// the column fell back to [`Self::snapshot_above_base_bytes`] or to `—`, - /// and in that fallback branch `size - shared_size` is the *whole base - /// image*. A row could show `—` for its snapshot and still carry 4.7 GB of - /// base in its total, which `triple_c_total_bytes` then added again as a - /// base-image row. - /// - /// The rule, in order: - /// - /// 1. `df()` computed a shared size → `size - shared`, the daemon's own - /// measurement of what is unique to this image. - /// 2. No shared size but the base lineage is known → the layer arithmetic - /// in [`layer_stats`]. - /// 3. Neither → the full size. Not a fallback to zero: an image nothing - /// shares with and whose lineage is unknown really does cost its whole - /// size, and a flattened snapshot is exactly that shape. - pub snapshot_attributed_bytes: i64, - pub total_bytes: i64, - /// A migration is in flight; every action on this row is blocked. - pub migrating: bool, -} - -/// A base image, shared by every project built from it. -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -pub struct BaseImageRow { - pub reference: String, - pub bytes: i64, - pub shared_bytes: i64, - /// Containers still built from it, as `df()` counts them. A base with - /// `containers > 0` cannot be removed and is not offered. - pub containers: i64, - /// Carries `triple-c.base=true`. - pub is_labelled_base: bool, -} - -/// Where the daemon actually keeps its bytes, and whether the Windows/WSL2 -/// caveat applies. See [`WSL2_VHDX_NOTE`]. -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -pub struct HostStorage { - /// `docker info`'s `DockerRootDir`. On Docker Desktop this is a path - /// *inside the VM*, not something the host can stat — which is the whole - /// reason the WSL2 note exists. - pub docker_root_dir: String, - /// `docker info`'s `OperatingSystem`, e.g. `"Docker Desktop"`. - pub operating_system: String, - pub is_docker_desktop: bool, - /// The app itself is running on Windows. - pub is_windows_host: bool, - /// Windows + Docker Desktop: pruning frees space *inside* `ext4.vhdx` and - /// returns nothing to `C:` until the disk is compacted. - pub vhdx_applies: bool, - /// [`WSL2_VHDX_NOTE`], [`WSL2_VHDX_FIX`] and [`WSL2_VHDX_FIX_GUI`], carried - /// over IPC rather than restated in the frontend. - /// - /// A second copy of this copy in TypeScript would drift from the one the - /// Rust tests pin, and this is the paragraph that stops a user reporting - /// "I pruned and C: did not change" as a bug. Empty when the caveat does - /// not apply, so the UI has nothing to decide. - pub vhdx_note: String, - pub vhdx_fix: Vec, - pub vhdx_fix_gui: String, -} - -/// Everything one Scan produces. -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -pub struct DiskUsageReport { - /// RFC3339. The UI shows how stale the numbers are rather than refreshing - /// them, because a refresh costs another `df()`. - pub scanned_at: String, - pub projects: Vec, - pub base_images: Vec, - pub base_images_bytes: i64, - /// Dangling `triple-c.managed=true` images — superseded snapshot commits. - pub orphan_image_bytes: i64, - pub orphan_image_count: usize, - /// `triple-c-home-*` / `triple-c-claude-config-*` volumes belonging to no - /// project in the store. Empty — and `orphan_volumes_unavailable` set — - /// when the store could not be read. - pub orphan_volumes: Vec, - pub orphan_volume_bytes: i64, - pub orphan_volumes_unavailable: Option, - pub build_cache: BuildCacheUsage, - /// Daemon-wide totals, for context: the user's unrelated work lives here - /// too, and the per-project rows will not add up to `docker system df`. - pub images_total_bytes: i64, - pub containers_total_bytes: i64, - pub volumes_total_bytes: i64, - /// Sum of the per-project rows — the part of the daemon that is ours. - pub triple_c_total_bytes: i64, - pub host: HostStorage, -} - -/// Build-cache figures, and where they came from. -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -pub struct BuildCacheUsage { - pub total_bytes: i64, - pub reclaimable_bytes: i64, - /// Bytes a `--filter until=168h` prune would reach. - pub stale_bytes: i64, - /// `"buildx du"` or `"system df"`. - /// - /// **`docker system df` under-reports build-cache reclaimable while - /// `docker buildx du` reports it correctly** (df only counts records with - /// no parent as reclaimable). The buildx figure is preferred whenever the - /// CLI is reachable; the field says which one is on screen so a user - /// comparing against their terminal is not left guessing. - pub source: String, - /// Set when the `docker` CLI could not be run, so `source` fell back. - pub cli_error: Option, -} - -/// A per-project volume whose project id is not in Triple-C's project store. -/// -/// **Not "a volume with no container".** See [`orphan_volumes`] for why that -/// distinction is the whole safety property. -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -pub struct OrphanVolume { - pub name: String, - /// The project id parsed out of the name. Shown, because a user who - /// recognises it may want to recover it rather than delete it. - pub project_id: String, - pub bytes: i64, - /// `"home"` or `"config"`. The config volume is the one that held Claude - /// credentials and transcripts, so it is worth saying which is which. - pub role: String, - /// When Docker created the volume, from `df()`'s own metadata. - /// - /// Evidence, not bookkeeping: a size and a UUID identify nothing, and this - /// is the only cheap fact that lets a user recognise which project a - /// candidate was before deleting it. It costs no extra call. - /// - /// **Never inspect a volume by mounting it.** `docker run -v :/path` - /// *creates* the volume when it does not exist, so a "just look inside" - /// probe can conjure the very thing it was checking for. Everything shown - /// about a volume here comes from `df()` metadata. - pub created_at: Option, -} - -// --------------------------------------------------------------------------- -// Reclaim targets -// --------------------------------------------------------------------------- - -/// How much trust an action needs. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Safety { - /// Already unreachable, or regenerated on demand. One button, no - /// confirmation. - Safe, - /// Reversible in substance but not in time — a rewrite, or a cache the user - /// pays to refill. One clear confirmation. - SemiSafe, -} - -/// Work [`reclaim`] is allowed to do. -/// -/// **This type cannot express a destructive action.** Adding a variant that -/// deletes a live project's data would be the mistake this split exists to -/// prevent; put it on [`DestructiveTarget`] instead. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum ReclaimTarget { - /// Dangling `triple-c.managed=true` images that are *not* base images — - /// the superseded snapshot commits every recreation leaves behind. This is - /// `sweep_orphaned_snapshots`, made visible and runnable on demand. - DanglingSnapshots, - /// Dangling `triple-c.managed=true` images that *are* base images - /// (`triple-c.base=true`). Same sweep, reported separately because a user - /// recognises "the old sandbox image" and not "a dangling commit". - SupersededBaseImages, - /// `docker builder prune`. **Daemon-wide, not Triple-C-only.** - BuildCache { - /// `true` prunes everything; `false` filters `until=168h`. - all: bool, - }, - /// `pre-migration-*` tags no migration record claims. Untagged only — the - /// image becomes dangling and the sweep collects it under its own rules. - MigrationPins, - /// `{id}-payload.tar` staging files with no record beside them. These are - /// host files under the user's data dir, **not** inside the daemon's - /// storage — on Windows they sit on `C:` directly rather than in the vhdx, - /// so this is the one bucket that gives space back to `C:` immediately. - MigrationStaging, - /// Containers labelled `triple-c.probe=migration`. - ProbeContainers, - /// `triple-c-scrub-*` containers left by an interrupted secret rewrite. - ScrubContainers, - /// Rewrite a project's stacked commit layers into a single layer. The - /// highest-yield action in this module. - CompactSnapshot { project_id: String }, - /// `rm -rf` the regenerable package caches in a running container's home - /// volume. - ClearCaches { - project_id: String, - /// `~/.rustup/toolchains` — regenerable, but expensive to re-download, - /// so it is a separate tick rather than part of the set. - include_rustup: bool, - }, -} - -/// Work [`destroy`] is allowed to do, one item at a time, against a typed -/// confirmation. Every variant deletes something with no other copy. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum DestructiveTarget { - /// The project's home volume: shell history, dotfiles, installed - /// toolchains, Playwright browsers. - HomeVolume { project_id: String }, - /// The project's config volume: **Claude credentials, plugins and every - /// conversation transcript**. - ConfigVolume { project_id: String }, - /// The project's snapshot image: every package the agent ever installed. - /// The project falls back to the base image on its next start. - SnapshotImage { project_id: String }, - /// A rollback pin whose migration is still awaiting confirmation — the only - /// copy of that migration's rollback target. - RollbackPin { project_id: String, tag: String }, - /// A `triple-c-home-*` / `triple-c-claude-config-*` volume whose project id - /// is in no `projects.json` this app can find. - /// - /// **It was a `ReclaimTarget` at `Safety::Safe`** — a tick and the group - /// Reclaim button, no confirmation. The object behind that tick is a - /// `triple-c-claude-config-*` volume holding a Claude OAuth credential, - /// every installed plugin and skill, and every conversation transcript the - /// project ever had, and the *same volume* for a project still in the store - /// requires typing the project's name. The only difference between the two - /// is a lookup against a file this app has been wrong about before: a - /// second app instance's project is absent from an in-memory list, a - /// corrupt `projects.json` empties it, and a data directory restored - /// without it empties it too. So it is confirmed like everything else that - /// has no other copy — see [`destroy`], where the typed string is the - /// **volume name**, there being no project name to type. - OrphanVolume { - name: String, - /// The id parsed out of the volume name. Display only — it names no - /// project in the store, which is the whole reason this variant exists. - project_id: String, - }, -} - -impl ReclaimTarget { - /// How much confirmation the UI must ask for. Pure, and pinned by a test - /// that walks every variant. - pub fn safety(&self) -> Safety { - match self { - // Unreachable already, or a host file nothing refers to. - ReclaimTarget::DanglingSnapshots - | ReclaimTarget::SupersededBaseImages - | ReclaimTarget::MigrationPins - | ReclaimTarget::MigrationStaging - | ReclaimTarget::ProbeContainers - | ReclaimTarget::ScrubContainers - | ReclaimTarget::BuildCache { .. } => Safety::Safe, - // A rewrite and a cache flush: nothing is lost, but time is. - ReclaimTarget::CompactSnapshot { .. } | ReclaimTarget::ClearCaches { .. } => { - Safety::SemiSafe - } - } - } - - /// Whether the action reaches beyond Triple-C's own objects. - /// - /// Only the build cache does, and the UI has to say so out loud: the same - /// daemon holds the user's unrelated postgres/mysql/site-builder work, and - /// a prune takes their warm cache with ours. - pub fn is_daemon_wide(&self) -> bool { - matches!(self, ReclaimTarget::BuildCache { .. }) - } - - /// The project this acts on, when it acts on one. - pub fn project_id(&self) -> Option<&str> { - match self { - ReclaimTarget::CompactSnapshot { project_id } - | ReclaimTarget::ClearCaches { project_id, .. } => Some(project_id), - _ => None, - } - } -} - -impl DestructiveTarget { - pub fn project_id(&self) -> &str { - match self { - DestructiveTarget::HomeVolume { project_id } - | DestructiveTarget::ConfigVolume { project_id } - | DestructiveTarget::SnapshotImage { project_id } - | DestructiveTarget::RollbackPin { project_id, .. } - | DestructiveTarget::OrphanVolume { project_id, .. } => project_id, - } - } - -} - -/// One offered action, with its measured cost. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ReclaimItem { - pub target: ReclaimTarget, - pub safety: Safety, - pub daemon_wide: bool, - pub label: String, - pub detail: String, - /// Bytes this would free, **measured**, never estimated. - pub bytes: i64, - /// `false` when `bytes` is a bound rather than a measurement — set only by - /// snapshot compaction, whose real yield cannot be known until it runs. - /// The UI must say "up to" whenever this is false. - pub bytes_are_exact: bool, - /// For compaction: the lower bound of the range, when `bytes` is the upper. - pub bytes_floor: Option, - /// Populated when the action cannot run right now (migration in flight, - /// container in the wrong state). The UI disables the tick and shows this. - pub blocked: Option, -} - -/// Everything [`list_reclaimable`] found. -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -pub struct ReclaimPlan { - pub items: Vec, - /// Destructive per-project objects, surfaced for display only. These are - /// never in `items` and [`reclaim`] cannot act on them. - pub destructive: Vec, - /// Set when the project store could not be read. Orphan detection is - /// suppressed entirely in that case — see [`orphan_volumes`]. - pub store_error: Option, -} - -/// A destructive object, described so the UI can offer it per project. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct DestructiveItem { - pub target: DestructiveTarget, - pub project_id: String, - pub project_name: String, - pub label: String, - /// Spelled out in full — this is the copy the confirmation shows. - pub loses: String, - pub bytes: i64, - pub blocked: Option, -} - -/// What actually happened. -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -pub struct ReclaimOutcome { - pub results: Vec, - /// Sum of the measured `freed_bytes` below. - pub total_freed_bytes: i64, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ReclaimResult { - /// The reclaim target this reports on, or `None` when it reports a - /// [`destroy`]. - /// - /// Deliberately not reused to carry a destructive action: an earlier - /// version returned `OrphanVolume { name }` for a home-volume deletion, - /// which named a volume that was never an orphan and would attribute the - /// outcome to a plan row the user never ticked. `destroyed` carries it - /// instead, and exactly one of the two is ever set. - pub target: Option, - /// The destructive action this reports on, when it is one. - #[serde(default)] - pub destroyed: Option, - pub ok: bool, - /// Bytes actually freed, measured after the fact. - pub freed_bytes: i64, - /// What was projected before the run, for the one action that projects. - pub projected_bytes: Option, - pub message: String, -} - -// --------------------------------------------------------------------------- -// Pure helpers — everything below is unit-tested without a daemon -// --------------------------------------------------------------------------- - -/// Classification of a dangling `triple-c.managed=true` image. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DanglingClass { - /// Built from `container/Dockerfile`, which stamps `triple-c.base=true`. - Base, - /// A superseded `docker commit` from some project's recreation. - SnapshotCommit, -} - -/// Split the dangling managed images into the two buckets the UI shows. -/// -/// The base label is the only thing separating them, and it is reliable for the -/// same reason `triple-c.managed` is: `create_container` writes -/// `triple-c.base` **explicitly empty**, so a container built from a base -/// cannot inherit `true` and have its commit claim to be a base image. -pub fn classify_dangling(labels: &HashMap) -> DanglingClass { - if labels.get(LABEL_BASE).map(String::as_str) == Some("true") { - DanglingClass::Base - } else { - DanglingClass::SnapshotCommit - } -} - -/// A volume as far as orphan detection is concerned. -#[derive(Debug, Clone, Default, PartialEq)] -pub struct VolumeFacts { - pub name: String, - pub bytes: i64, - /// `VolumeUsageData::ref_count` — containers currently referencing it. - /// `-1` means the daemon did not compute it, which is **not** zero. - /// - /// Note what this is *not* used for: a zero ref count is not evidence that - /// a volume is unclaimed. An idle project whose container has been removed - /// has exactly this shape. It is only ever an extra brake on top of the - /// store check. - pub links: i64, - pub created_at: Option, -} - -/// Volumes that look like ours and belong to no project in the store. -/// -/// ## The only authority is the project store -/// -/// From the daemon's side an **idle live project and a deleted one are -/// indistinguishable**. A project that has not been opened for a while has had -/// its container removed and may have no snapshot image either — volumes alone, -/// no container, nothing running. That is the *normal* resting state of a live -/// project, not a signal. -/// -/// This is not hypothetical: the heuristic "no container and no snapshot image -/// means orphaned" was tried against a real project list and flagged two live -/// projects whose volumes held `.credentials.json`, Claude transcripts and -/// shell history. So nothing in this function looks at containers, images or -/// activity. The test is membership in the project store, and only that. -/// -/// ## Why a store-load failure returns nothing -/// -/// The whole test is "in the store? then live". If the store failed to load, -/// *every* project's volumes look unclaimed — a blanket delete would wipe the -/// credentials, transcripts and toolchains of every project the user has. So a -/// failure returns an empty set and the caller says why, rather than returning -/// what would look like a very productive reclaim. -/// -/// `links == 0` is required on top of that. A volume with a container attached -/// belongs to something, whatever the store says, and `-1` (not computed) is -/// treated as "attached" for the same reason: unknown is never permission. -pub fn orphan_volumes( - volumes: &[VolumeFacts], - known_project_ids: &HashSet, - store_loaded: bool, -) -> Vec { - if !store_loaded { - return Vec::new(); - } - let mut out = Vec::new(); - for volume in volumes { - let (project_id, role) = match parse_project_volume_name(&volume.name) { - Some(parsed) => parsed, - None => continue, - }; - if known_project_ids.contains(project_id) { - continue; - } - if volume.links != 0 { - continue; - } - out.push(OrphanVolume { - name: volume.name.clone(), - project_id: project_id.to_string(), - bytes: volume.bytes.max(0), - role: role.to_string(), - created_at: volume.created_at.clone(), - }); - } - out.sort_by(|a, b| b.bytes.cmp(&a.bytes).then_with(|| a.name.cmp(&b.name))); - out -} - -/// Split a project volume name into `(project_id, role)`. -/// -/// Order matters and is not interchangeable: `triple-c-claude-config-` is -/// checked first because `triple-c-home-` does not prefix it, but a future -/// prefix that *does* nest would silently mis-attribute if this were reversed. -pub fn parse_project_volume_name(name: &str) -> Option<(&str, &'static str)> { - if let Some(id) = name.strip_prefix(CONFIG_VOLUME_PREFIX) { - if id.is_empty() { - return None; - } - return Some((id, "config")); - } - if let Some(id) = name.strip_prefix(HOME_VOLUME_PREFIX) { - if id.is_empty() { - return None; - } - return Some((id, "home")); - } - None -} - -/// This project's share of its snapshot image, in bytes. -/// -/// The single rule behind [`ProjectDiskRow::snapshot_attributed_bytes`], pulled -/// out of [`scan`] so it can be tested without a daemon — the bug it fixes was -/// two call sites disagreeing, and a rule that lives in one function cannot -/// disagree with itself. -/// -/// 1. `df()` computed a shared size → `size - shared`. The daemon's own -/// measurement of what is unique to this image, and the only exact answer -/// available. -/// 2. No shared size, but the base lineage is known → the layer arithmetic from -/// [`layer_stats`]. -/// 3. Neither → `size - shared`, which with no shared size is the full image. -/// Deliberately not zero: an image that shares nothing measurable really -/// does cost its whole size, and a flattened snapshot is exactly that shape. -pub fn snapshot_attribution( - snapshot_bytes: i64, - snapshot_shared_bytes: i64, - above_base_bytes: Option, -) -> i64 { - let unique = (snapshot_bytes - snapshot_shared_bytes.max(0)).max(0); - if snapshot_shared_bytes > 0 { - return unique; - } - above_base_bytes.map(|b| b.max(0)).unwrap_or(unique) -} - -/// What a snapshot's layer stack looks like relative to its base. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct LayerStats { - /// Layers stacked above the base image — one per recreation. - pub commit_layers: u32, - /// Bytes those layers account for, or `None` when the base is unknown. - pub above_base_bytes: Option, -} - -/// Work out how much of a snapshot is stacked commits. -/// -/// `Docker::image_history` returns entries **newest first**, and a snapshot's -/// history is its base's history with the commits appended — so the base is the -/// tail, and the commits are the first `len - base_len` entries. Comparing -/// lengths rather than layer digests keeps this a pure function over two -/// vectors of sizes, and is exactly as accurate: the base is by construction a -/// prefix of the snapshot's chain. -/// -/// `base_history_len` is `None` when the base image is no longer on the daemon. -/// In that case the count falls back to "layers that carry bytes", which is an -/// honest upper bound on the commits, and the byte split is reported as unknown -/// rather than guessed. -pub fn layer_stats(snapshot_history_sizes: &[i64], base_history_len: Option) -> LayerStats { - match base_history_len { - Some(base_len) if base_len <= snapshot_history_sizes.len() => { - let commits = &snapshot_history_sizes[..snapshot_history_sizes.len() - base_len]; - LayerStats { - commit_layers: commits.len() as u32, - above_base_bytes: Some(commits.iter().sum()), - } - } - // Either no base, or a base longer than the snapshot's own history, - // which means they are not in the same lineage at all. - _ => LayerStats { - commit_layers: snapshot_history_sizes.iter().filter(|s| **s > 0).count() as u32, - above_base_bytes: None, - }, - } -} - -/// The range a compaction can land in, given the stacked layers it will merge. -/// -/// Nothing can measure the real figure in advance: it depends on how much of -/// each layer is superseded by a later one, which is exactly what flattening -/// discovers. But it is bounded, and both bounds are computable: -/// -/// * **Floor: zero.** Every byte may still be live, in which case flattening -/// frees nothing — and can even cost a little, since the merged layer -/// recompresses independently. Verified on a synthetic stack with nothing -/// superseded: 29.8 MB → 30.8 MB. -/// * **Ceiling: everything but the largest layer.** The result cannot be -/// smaller than the biggest single layer's worth of content, so at most -/// `sum - max` is superseded. -/// -/// Reporting a range is why [`ReclaimItem::bytes_are_exact`] exists. A single -/// invented number here would be the one place in this module that shows a -/// guess as if it were a measurement. -pub fn compaction_bounds(commit_layer_sizes: &[i64]) -> (i64, i64) { - let sum: i64 = commit_layer_sizes.iter().filter(|s| **s > 0).sum(); - let max = commit_layer_sizes.iter().copied().max().unwrap_or(0).max(0); - (0, (sum - max).max(0)) -} - -/// Bytes a `docker builder prune --filter until={hours}h` would reach. -/// -/// Records still in use are never reclaimable however old they are, and a -/// record the daemon gave no `last_used_at` is treated as too young to touch — -/// unknown is not permission here either. -pub fn stale_build_cache_bytes( - entries: &[BuildCacheFacts], - until_hours: i64, - now: chrono::DateTime, -) -> i64 { - let cutoff = now - chrono::Duration::hours(until_hours); - entries - .iter() - .filter(|e| !e.in_use) - .filter(|e| e.last_used_at.map(|t| t < cutoff).unwrap_or(false)) - .map(|e| e.size.max(0)) - .sum() -} - -/// A build-cache record, reduced to what the age filter needs. -#[derive(Debug, Clone, PartialEq)] -pub struct BuildCacheFacts { - pub size: i64, - pub in_use: bool, - pub last_used_at: Option>, -} - -impl From<&BuildCache> for BuildCacheFacts { - fn from(entry: &BuildCache) -> Self { - BuildCacheFacts { - size: entry.size.unwrap_or(0), - in_use: entry.in_use.unwrap_or(false), - last_used_at: entry.last_used_at.as_ref().and_then(parse_bollard_date), - } - } -} - -/// bollard's `BollardDate` is a `chrono` type behind a feature flag and a -/// string otherwise; going through its `Display` keeps this working either way -/// without pinning the feature. -fn parse_bollard_date(date: &bollard::models::BollardDate) -> Option> { - chrono::DateTime::parse_from_rfc3339(&date.to_string()) - .ok() - .map(|d| d.with_timezone(&chrono::Utc)) -} - -/// Parse a size the `docker` CLI printed, e.g. `"46.88GB"`, `"0B"`, `"1.5kB"`. -/// -/// Docker formats these with `units.HumanSize`, which is **base 1000**, not -/// 1024 — using 1024 here would overstate a 28 GB build cache by ~7%. Returns -/// `None` for anything unrecognised so a CLI output change degrades to "fall -/// back to `df()`" rather than to a wrong number. -pub fn parse_docker_size(raw: &str) -> Option { - let raw = raw.trim(); - let split = raw.find(|c: char| c.is_ascii_alphabetic())?; - let (number, unit) = raw.split_at(split); - let value: f64 = number.trim().parse().ok()?; - // Docker never prints a negative size, and a negative `freed_bytes` reaching - // the UI would subtract from the running total. Fail rather than propagate. - if !value.is_finite() || value < 0.0 { - return None; - } - let multiplier: f64 = match unit.trim() { - "B" => 1.0, - "kB" | "KB" => 1e3, - "MB" => 1e6, - "GB" => 1e9, - "TB" => 1e12, - "PB" => 1e15, - _ => return None, - }; - Some((value * multiplier) as i64) -} - -/// Pull `Reclaimable:` and `Total:` out of `docker buildx du`'s trailing -/// summary. Returns `(total, reclaimable)`. -pub fn parse_buildx_du(output: &str) -> Option<(i64, i64)> { - let mut total = None; - let mut reclaimable = None; - for line in output.lines() { - let line = line.trim(); - if let Some(rest) = line.strip_prefix("Reclaimable:") { - reclaimable = parse_docker_size(rest); - } else if let Some(rest) = line.strip_prefix("Total:") { - total = parse_docker_size(rest); - } - } - Some((total?, reclaimable.unwrap_or(0))) -} - -/// Pull the figure a prune reports out of its output. -/// -/// **Two different wordings, and `builder prune` uses the less obvious one.** -/// Verified against Docker 29.7.2: `docker builder prune` ends with a bare -/// `Total:\t20.59MB`, while `docker system prune` and `docker image prune` end -/// with `Total reclaimed space: 20.59MB`. A parser that only knew the second -/// form would silently report every build-cache prune as having freed nothing — -/// which is exactly what the first draft of this did. -/// -/// The scan is last-line-first so the summary wins over any record line that -/// happens to contain the word. -pub fn parse_reclaimed_space(output: &str) -> i64 { - output - .lines() - .rev() - .find_map(|line| { - let line = line.trim(); - let rest = line - .strip_prefix("Total reclaimed space:") - .or_else(|| line.strip_prefix("Total:"))?; - parse_docker_size(rest) - }) - .unwrap_or(0) -} - -/// The Dockerfile that flattens a snapshot into a single layer. -/// -/// ## Why a build and not `docker commit --squash` or export/import -/// -/// `commit` cannot squash — squashing is the one thing it does not do, and it -/// is why the stack grows. `--squash` on the classic builder needs an -/// experimental daemon. `docker export | docker import` moves the whole -/// filesystem through this process, and bollard's import takes a fully -/// buffered `Bytes` — a 12 GB image in RAM. -/// -/// A two-stage build keeps every byte inside the daemon. `COPY --from` a whole -/// root into `FROM scratch` collapses the chain to one layer, and it preserves -/// uid/gid and setuid bits — **verified against Docker 29.7.2**: a 192.6 MB / -/// 4-layer synthetic came out 45.7 MB / 1 layer with `-rwsr-xr-x root` and -/// `uid 1000` intact, through the plain `POST /build` endpoint bollard uses -/// (no BuildKit session). -/// -/// ## Why the scrub runs in the first stage -/// -/// The bytes are only free to drop *before* the layer that captures them is -/// written, and here that layer is the flattened one. Running -/// [`container::snapshot_scrub_script`] in the `src` stage costs a throwaway -/// layer on a stage that is discarded, and reuses the one reviewed path list — -/// `SNAPSHOT_SCRUB_PATHS` — rather than forking a second copy of it, which is -/// the failure mode a list like that invites. -/// -/// The image's config (env, cmd, entrypoint, labels, workdir) does **not** -/// survive `FROM scratch`; it is replayed afterwards by -/// [`restore_image_config`], which is why this function does not try to emit it -/// as Dockerfile instructions. A multi-line `CLAUDE_INSTRUCTIONS` env var alone -/// makes that escaping a bad bet. -/// -/// The one label it *does* emit is `triple-c.managed=true`, and it is not -/// decoration. Everything that cleans up after this build — the discard path -/// when the result is not smaller, the untag after a successful commit — relies -/// on `sweep_orphaned_snapshots` collecting the intermediate, and that sweep -/// filters on `dangling=true` **and** this label. Without it the sweep can -/// never match, and the flattened intermediate is left to whatever `untag_image` -/// happens to delete on its own. -pub fn compaction_dockerfile(snapshot_ref: &str, scrub_script: &str) -> String { - format!( - "FROM {snapshot_ref} AS src\n\ - RUN {run}\n\ - FROM scratch\n\ - COPY --from=src / /\n\ - LABEL {LABEL_MANAGED}=true\n", - run = run_exec_form(scrub_script) - ) -} - -/// Render a multi-line `/bin/sh` program as a `RUN` the daemon will actually -/// execute. -/// -/// ## What was here before, and why it never worked once -/// -/// The scrub script is multi-line shell, and a Dockerfile instruction does not -/// continue over a bare newline — so the script was folded onto one line by -/// joining its lines **with a space**. That is not a rewrite the shell -/// tolerates. `for p in …` on one line and `do` on the next are separated by a -/// newline that *is* load-bearing; joining them produces -/// `… for p in …; do [ -e "$p" ] || continue sz=$(…) …`, and `sh` stops at: -/// -/// ```text -/// /bin/sh: line 0: syntax error: unexpected "do" -/// ERROR: process "/bin/sh -c total=0 for p in …" did not complete successfully: exit code: 2 -/// ``` -/// -/// Verified with a real `docker build`, not reasoned about. The build failed on -/// its first stage every single time, so `compact_snapshot` has always returned -/// a failure — the headline action of the whole panel, broken since it landed. -/// Nothing was lost, because the failure is before anything is removed, but -/// nothing was ever reclaimed either. The test that was supposed to catch this -/// asserted only that the `RUN` was *one line*, which the broken fold satisfied -/// perfectly. -/// -/// ## Why the JSON exec form rather than a better fold -/// -/// Any fold is a rewrite of somebody else's shell, and the script is not this -/// module's to own — `container::snapshot_scrub_script` is free to grow a -/// `case`, an `if`, a heredoc or a function, and each of those breaks a -/// different set of join rules. Inserting `;` between lines is wrong for -/// exactly the same reason a space was: `; do` is fine, but `if x; then; y` is -/// not. -/// -/// So the script is not transformed at all. `RUN ["/bin/sh", "-c", "