chore: package the goose binary in the goose2 tauri app (#8615)

Co-authored-by: Lifei Zhou <lifei@squareup.com>
This commit is contained in:
Jack Amadeo 2026-04-17 03:01:30 -04:00 committed by GitHub
parent bd14186214
commit 75a41a34dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 857 additions and 519 deletions

View file

@ -111,6 +111,10 @@ jobs:
- name: Build frontend
run: pnpm build
- name: Mock goose binary
working-directory: .
run: mkdir -p target/release && touch target/release/goose-$(rustc --print host-tuple)
- name: Check Tauri
run: cd src-tauri && cargo check
@ -167,6 +171,10 @@ jobs:
ui/goose2/src-tauri/target
key: ${{ runner.os }}-goose2-cargo-${{ hashFiles('ui/goose2/src-tauri/Cargo.lock') }}
- name: Mock goose binary
working-directory: .
run: mkdir -p target/release && touch target/release/goose-$(rustc --print host-tuple)
- name: Format check
run: cd src-tauri && cargo fmt --check

View file

@ -2,7 +2,7 @@
members = [
"crates/*",
# Mainly for cargo-machete to not error out during inspection.
"vendor/v8"
"vendor/v8",
]
exclude = ["ui/goose2/src-tauri"]
resolver = "2"

View file

@ -484,3 +484,8 @@ build-test-tools:
record-mcp-tests: build-test-tools
GOOSE_RECORD_MCP=1 cargo test --package goose --test mcp_integration_test
git add crates/goose/tests/mcp_replays/
bundle-goose2:
cargo build --release --package goose-cli --bin goose
cp target/release/goose target/release/goose-$(rustc --print host-tuple)
@just goose2::bundle

View file

@ -8,14 +8,12 @@ Goose2 is a Tauri 2 + React 19 desktop app.
bash/zsh: `source ./bin/activate-hermit`
fish: `source ./bin/activate-hermit.fish`
2. Install git hooks: `lefthook install`
3. Install dependencies: `just setup`
3. Prepare workspace dependencies: `just setup`
4. Start the app: `just dev`
`just clean` removes Rust build artifacts, `dist`, and `node_modules`. Run `just setup` again before `just dev`.
`just setup` bootstraps a shared managed goose checkout in a home-level cache directory when it does not exist, fast-forwards it, builds a local `goose` binary, and stamps the exact branch/commit it used. `just dev` only does a lightweight preflight against that shared stamp; if the managed checkout is missing, stale, or built from the wrong branch, it warns and tells you to rerun `just setup`. By default the helper uses `~/Library/Caches/goose2-dev` on macOS, or `$XDG_CACHE_HOME/goose2-dev` / `~/.cache/goose2-dev` elsewhere. It prefers `origin/baxen/goose2` and falls back to `origin/main` when that branch does not exist yet.
Override the shared cache root or branch with `GOOSE_DEV_ROOT=/path/to/cache` and `GOOSE_DEV_BRANCH=my/integration-branch`. You can also override the checkout path directly with `GOOSE_DEV_REPO=/path/to/goose`, or the clone source with `GOOSE_DEV_CLONE_URL=...`.
`just setup` installs UI workspace dependencies, builds the SDK package, and builds the local debug `goose` CLI binary. `just dev` exports `GOOSE_BIN` to that local binary and loads `src-tauri/tauri.dev.conf.json`, which clears the production `externalBin` requirement during development.
Run `just` to list available commands, or see [justfile](./justfile) for the full recipe definitions.

View file

@ -10,9 +10,9 @@ default:
# Install dependencies and build workspace packages
setup:
pnpm install
cd ../ && pnpm install
cd ../sdk && pnpm build
cd src-tauri && cargo build
cargo build --manifest-path ../../Cargo.toml -p goose-cli --bin goose
# ── Build & Check ────────────────────────────────────────────
@ -50,6 +50,9 @@ tauri-check:
# Full CI gate
ci: check clippy test build tauri-check
bundle:
pnpm tauri build
# ── Test ─────────────────────────────────────────────────────
# Run unit/component tests
@ -82,7 +85,9 @@ dev:
VITE_PORT={{ vite_port }}
export VITE_PORT
PROJECT_DIR=$(pwd)
TAURI_CONFIG="{\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"cd ${PROJECT_DIR} && exec pnpm exec vite --port ${VITE_PORT} --strictPort\",\"cwd\":\".\",\"wait\":false}}}"
GOOSE_BIN="${PROJECT_DIR}/../../target/debug/goose"
export GOOSE_BIN
EXTRA_CONFIG_ARGS=(--config "{\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"cd ${PROJECT_DIR} && exec pnpm exec vite --port ${VITE_PORT} --strictPort\",\"cwd\":\".\",\"wait\":false}}}")
# In worktrees, generate a labeled icon so you can tell instances apart
if git rev-parse --is-inside-work-tree &>/dev/null; then
@ -97,12 +102,12 @@ dev:
if swift scripts/generate-dev-icon.swift src-tauri/icons/icon.icns "$DEV_ICON" "$WORKTREE_LABEL"; then
echo "🌳 Worktree: ${WORKTREE_LABEL}"
TAURI_CONFIG=$(python3 -c "import json,sys; a=json.loads(sys.argv[1]); a['bundle']={'icon':['$DEV_ICON']}; print(json.dumps(a))" "$TAURI_CONFIG")
EXTRA_CONFIG_ARGS+=(--config "{\"bundle\":{\"icon\":[\"$DEV_ICON\"]}}")
fi
fi
fi
pnpm tauri dev --features app-test-driver --config "$TAURI_CONFIG"
pnpm tauri dev --features app-test-driver --config src-tauri/tauri.dev.conf.json "${EXTRA_CONFIG_ARGS[@]}"
# Start the desktop app with dev config
dev-debug:
@ -111,7 +116,10 @@ dev-debug:
VITE_PORT={{ vite_port }}
export VITE_PORT
EXTRA_CONFIG="--config {\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"exec ./node_modules/.bin/vite --port ${VITE_PORT} --strictPort\",\"cwd\":\"..\",\"wait\":false}}}"
PROJECT_DIR=$(pwd)
GOOSE_BIN="${PROJECT_DIR}/../../target/debug/goose"
export GOOSE_BIN
EXTRA_CONFIG_ARGS=(--config "{\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"exec ./node_modules/.bin/vite --port ${VITE_PORT} --strictPort\",\"cwd\":\"..\",\"wait\":false}}}")
# In worktrees, generate a labeled icon so you can tell instances apart
if git rev-parse --is-inside-work-tree &>/dev/null; then
@ -126,12 +134,12 @@ dev-debug:
if swift scripts/generate-dev-icon.swift src-tauri/icons/icon.icns "$DEV_ICON" "$WORKTREE_LABEL"; then
echo "🌳 Worktree: ${WORKTREE_LABEL}"
EXTRA_CONFIG="$EXTRA_CONFIG --config {\"bundle\":{\"icon\":[\"$DEV_ICON\"]}}"
EXTRA_CONFIG_ARGS+=(--config "{\"bundle\":{\"icon\":[\"$DEV_ICON\"]}}")
fi
fi
fi
pnpm tauri dev --config src-tauri/tauri.dev.conf.json $EXTRA_CONFIG
pnpm tauri dev --config src-tauri/tauri.dev.conf.json "${EXTRA_CONFIG_ARGS[@]}"
# Start only the frontend dev server
dev-frontend:
@ -166,8 +174,3 @@ clean:
cd src-tauri && cargo clean
rm -rf dist
rm -rf node_modules
# Cherry-pick commits from the goose2 repo into ui/goose2/
cherry-pick-goose2 *ARGS:
git fetch goose2
git format-patch -1 {{ARGS}} --stdout | git am --directory=ui/goose2

View file

@ -65,8 +65,9 @@
"@tailwindcss/typography": "^0.5.19",
"@tanstack/react-query": "^5.90.21",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2.6.0",
"@tauri-apps/plugin-dialog": "~2.7.0",
"@tauri-apps/plugin-opener": "^2.5.3",
"@tauri-apps/plugin-shell": "~2.3.5",
"@xyflow/react": "^12.10.2",
"ai": "^6.0.142",
"ansi-to-react": "^6.2.6",

View file

@ -1,262 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: ensure-local-goose.sh [--print-bin | --check-bin]
Syncs and builds a dedicated local goose checkout for goose2 development.
Environment variables:
GOOSE_DEV_MODE auto|required (default: auto)
GOOSE_DEV_ROOT path to the shared goose2 dev cache root
(default: platform cache dir under home)
GOOSE_DEV_REPO path to the managed goose checkout
(default: $GOOSE_DEV_ROOT/goose)
GOOSE_DEV_STAMP_FILE path to the shared build stamp file
(default: $GOOSE_DEV_ROOT/stamp.env)
GOOSE_DEV_CLONE_URL git clone URL for the managed goose checkout
(default: https://github.com/block/goose.git)
GOOSE_DEV_REMOTE git remote to sync from (default: origin)
GOOSE_DEV_BRANCH preferred branch to use (default: baxen/goose2)
GOOSE_DEV_FALLBACK_BRANCH fallback branch when the preferred branch does
not exist remotely (default: main)
GOOSE_DEV_ALLOW_DIRTY 1 to allow syncing/building a dirty checkout
EOF
}
action="build"
print_bin=0
while [[ $# -gt 0 ]]; do
case "$1" in
--print-bin)
print_bin=1
shift
;;
--check-bin)
action="check"
print_bin=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
mode="${GOOSE_DEV_MODE:-auto}"
clone_url="${GOOSE_DEV_CLONE_URL:-https://github.com/block/goose.git}"
remote="${GOOSE_DEV_REMOTE:-origin}"
preferred_branch="${GOOSE_DEV_BRANCH:-baxen/goose2}"
fallback_branch="${GOOSE_DEV_FALLBACK_BRANCH:-main}"
allow_dirty="${GOOSE_DEV_ALLOW_DIRTY:-0}"
log() {
echo "[goose-dev] $*" >&2
}
fail_or_skip() {
local message="$1"
if [[ "${mode}" == "required" ]]; then
echo "${message}" >&2
exit 1
fi
log "${message}"
# In check mode, exit 2 so callers (e.g. just dev) can detect "not ready"
# and block instead of silently continuing without a goose binary.
if [[ "${action}" == "check" ]]; then
exit 2
fi
exit 0
}
default_goose_dev_root() {
if [[ -n "${XDG_CACHE_HOME:-}" ]]; then
printf '%s/goose2-dev\n' "${XDG_CACHE_HOME}"
return
fi
case "$(uname -s)" in
Darwin)
printf '%s/Library/Caches/goose2-dev\n' "${HOME}"
;;
*)
printf '%s/.cache/goose2-dev\n' "${HOME}"
;;
esac
}
goose_dev_root="${GOOSE_DEV_ROOT:-$(default_goose_dev_root)}"
goose_repo="${GOOSE_DEV_REPO:-${goose_dev_root}/goose}"
stamp_file="${GOOSE_DEV_STAMP_FILE:-${goose_dev_root}/stamp.env}"
bin_path="${goose_repo}/target/debug/goose"
resolve_remote_head() {
local branch_name="$1"
git -C "${goose_repo}" ls-remote --heads "${remote}" "${branch_name}" 2>/dev/null | awk 'NR == 1 { print $1 }'
}
resolve_branch() {
local resolved_branch="${preferred_branch}"
local resolved_head
resolved_head="$(resolve_remote_head "${resolved_branch}")"
if [[ -z "${resolved_head}" && "${resolved_branch}" != "${fallback_branch}" ]]; then
log "Remote branch ${remote}/${resolved_branch} not found; falling back to ${remote}/${fallback_branch}."
resolved_branch="${fallback_branch}"
resolved_head="$(resolve_remote_head "${resolved_branch}")"
fi
if [[ -z "${resolved_head}" ]]; then
if [[ "${mode}" == "required" ]]; then
echo "Could not resolve ${remote}/${resolved_branch} for managed goose checkout at ${goose_repo}." >&2
return 1
fi
log "Could not resolve ${remote}/${resolved_branch} for managed goose checkout at ${goose_repo}."
return 2
fi
RESOLVED_BRANCH="${resolved_branch}"
RESOLVED_REMOTE_HEAD="${resolved_head}"
return 0
}
write_stamp() {
local branch_name="$1"
local commit_sha="$2"
mkdir -p "$(dirname "${stamp_file}")"
{
printf 'STAMP_REPO=%q\n' "${goose_repo}"
printf 'STAMP_BRANCH=%q\n' "${branch_name}"
printf 'STAMP_COMMIT=%q\n' "${commit_sha}"
printf 'STAMP_BIN=%q\n' "${bin_path}"
} >"${stamp_file}"
}
ensure_checkout_exists() {
if [[ -d "${goose_repo}/.git" ]]; then
return 0
fi
if [[ "${action}" == "check" ]]; then
fail_or_skip "Managed goose checkout not found at ${goose_repo}. Rerun just setup."
fi
log "Cloning managed goose checkout into ${goose_repo}."
mkdir -p "$(dirname "${goose_repo}")"
git clone "${clone_url}" "${goose_repo}" >/dev/null 2>&1 || {
fail_or_skip "Failed to clone managed goose checkout from ${clone_url} into ${goose_repo}."
}
}
ensure_checkout_exists
if [[ "${allow_dirty}" != "1" ]]; then
if [[ -n "$(git -C "${goose_repo}" status --porcelain)" ]]; then
fail_or_skip "Managed goose checkout at ${goose_repo} is dirty. Use a dedicated checkout or set GOOSE_DEV_ALLOW_DIRTY=1."
fi
fi
if resolve_branch; then
branch="${RESOLVED_BRANCH}"
remote_head="${RESOLVED_REMOTE_HEAD}"
else
resolve_branch_status=$?
case "${resolve_branch_status}" in
1)
exit 1
;;
2)
exit 0
;;
*)
echo "Unexpected resolve_branch status: ${resolve_branch_status}" >&2
exit 1
;;
esac
fi
if [[ "${action}" == "check" ]]; then
if [[ ! -f "${stamp_file}" ]]; then
fail_or_skip "Managed goose checkout is configured, but no local goose build stamp was found. Rerun just setup."
fi
# shellcheck disable=SC1090
source "${stamp_file}"
if [[ "${STAMP_REPO:-}" != "${goose_repo}" ]]; then
fail_or_skip "Managed goose checkout changed since the last local goose build. Rerun just setup."
fi
if [[ "${STAMP_BRANCH:-}" != "${branch}" ]]; then
fail_or_skip "Managed goose branch is now ${branch}, but the local goose build was prepared for ${STAMP_BRANCH:-unknown}. Rerun just setup."
fi
if [[ ! -x "${STAMP_BIN:-}" ]]; then
fail_or_skip "Local goose binary was not found at ${STAMP_BIN:-unknown}. Rerun just setup."
fi
local_head="$(git -C "${goose_repo}" rev-parse HEAD)"
if [[ "${STAMP_COMMIT:-}" != "${local_head}" ]]; then
fail_or_skip "Managed goose checkout changed after the last local build. Rerun just setup."
fi
if [[ "${STAMP_COMMIT:-}" != "${remote_head}" ]]; then
fail_or_skip "Managed goose checkout is behind ${remote}/${branch}. Rerun just setup."
fi
if [[ "${print_bin}" == "1" ]]; then
printf '%s\n' "${STAMP_BIN}"
fi
exit 0
fi
git -C "${goose_repo}" fetch "${remote}" "${branch}" >/dev/null 2>&1
remote_ref="refs/remotes/${remote}/${branch}"
if ! git -C "${goose_repo}" show-ref --verify --quiet "${remote_ref}"; then
fail_or_skip "Fetched ${remote}/${branch}, but ${remote_ref} is not available in ${goose_repo}."
fi
if git -C "${goose_repo}" show-ref --verify --quiet "refs/heads/${branch}"; then
git -C "${goose_repo}" checkout "${branch}" >/dev/null 2>&1
else
git -C "${goose_repo}" checkout -b "${branch}" --track "${remote}/${branch}" >/dev/null 2>&1
fi
# Reset to the remote head. This is a managed build-only checkout, so we
# always want to match the remote exactly. A plain `pull --ff-only` would
# fail when the remote branch has been force-pushed (rebased/amended).
git -C "${goose_repo}" reset --hard "${remote}/${branch}" >/dev/null 2>&1
log "Building goose from ${goose_repo} on ${branch}."
(
cd "${goose_repo}"
cargo build -p goose-cli --bin goose
)
if [[ -n "$(git -C "${goose_repo}" status --porcelain -- Cargo.lock)" ]]; then
# Cargo may refresh the lockfile while compiling a freshly synced checkout.
# This managed repo is only a build source for goose2, so restore the tracked
# lockfile to keep the checkout clean for later preflight checks.
git -C "${goose_repo}" checkout -- Cargo.lock
fi
if [[ ! -x "${bin_path}" ]]; then
echo "Expected goose binary at ${bin_path}, but it was not built successfully." >&2
exit 1
fi
write_stamp "${branch}" "$(git -C "${goose_repo}" rev-parse HEAD)"
log "Local goose binary ready at ${bin_path}."
if [[ "${print_bin}" == "1" ]]; then
printf '%s\n' "${bin_path}"
fi

View file

@ -1079,6 +1079,15 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "endi"
version = "1.1.1"
@ -1675,6 +1684,7 @@ dependencies = [
"tauri-plugin-dialog",
"tauri-plugin-log",
"tauri-plugin-opener",
"tauri-plugin-shell",
"tauri-plugin-window-state",
"tempfile",
"tokio",
@ -2920,6 +2930,16 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "os_pipe"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "pango"
version = "0.18.3"
@ -4085,12 +4105,44 @@ dependencies = [
"digest",
]
[[package]]
name = "shared_child"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
dependencies = [
"libc",
"sigchld",
"windows-sys 0.60.2",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "sigchld"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
dependencies = [
"libc",
"os_pipe",
"signal-hook",
]
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
@ -4541,9 +4593,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-dialog"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b"
checksum = "a1fa4150c95ae391946cc8b8f905ab14797427caba3a8a2f79628e956da91809"
dependencies = [
"log",
"raw-window-handle",
@ -4625,6 +4677,27 @@ dependencies = [
"zbus 5.14.0",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b"
dependencies = [
"encoding_rs",
"log",
"open",
"os_pipe",
"regex",
"schemars 0.8.22",
"serde",
"serde_json",
"shared_child",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
"tokio",
]
[[package]]
name = "tauri-plugin-window-state"
version = "2.4.1"

View file

@ -6,7 +6,7 @@ authors = ["you"]
edition = "2021"
[[bin]]
name = "Goose"
name = "goose-tauri"
path = "src/main.rs"
[lib]
@ -36,6 +36,7 @@ doctor = { git = "https://github.com/block/builderbot", rev = "8e1c3ec145edc0df5
ignore = "0.4.25"
base64 = "0.22"
mime_guess = "2"
tauri-plugin-shell = "2"
[target.'cfg(target_os = "macos")'.dependencies]
keyring = { version = "3", features = ["apple-native"] }

View file

@ -13,17 +13,39 @@
{
"identifier": "opener:allow-open-path",
"allow": [
{ "path": "$HOME/**" },
{ "path": "$HOME/.goose/**" },
{ "path": "$HOME/.goose/artifacts/**" },
{ "path": "$TEMP/**" },
{ "path": "/Volumes/**" },
{ "path": "/mnt/**" },
{ "path": "/workspace/**" },
{ "path": "/workspaces/**" },
{ "path": "/opt/**" },
{ "path": "/srv/**" },
{ "path": "*:/**" }
{
"path": "$HOME/**"
},
{
"path": "$HOME/.goose/**"
},
{
"path": "$HOME/.goose/artifacts/**"
},
{
"path": "$TEMP/**"
},
{
"path": "/Volumes/**"
},
{
"path": "/mnt/**"
},
{
"path": "/workspace/**"
},
{
"path": "/workspaces/**"
},
{
"path": "/opt/**"
},
{
"path": "/srv/**"
},
{
"path": "*:/**"
}
]
},
"window-state:allow-restore-state",

File diff suppressed because one or more lines are too long

View file

@ -302,6 +302,216 @@
}
}
},
{
"if": {
"properties": {
"identifier": {
"anyOf": [
{
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string",
"const": "shell:default",
"markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`"
},
{
"description": "Enables the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-execute",
"markdownDescription": "Enables the execute command without any pre-configured scope."
},
{
"description": "Enables the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-kill",
"markdownDescription": "Enables the kill command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-spawn",
"markdownDescription": "Enables the spawn command without any pre-configured scope."
},
{
"description": "Enables the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-stdin-write",
"markdownDescription": "Enables the stdin_write command without any pre-configured scope."
},
{
"description": "Denies the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-execute",
"markdownDescription": "Denies the execute command without any pre-configured scope."
},
{
"description": "Denies the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-kill",
"markdownDescription": "Denies the kill command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-spawn",
"markdownDescription": "Denies the spawn command without any pre-configured scope."
},
{
"description": "Denies the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-stdin-write",
"markdownDescription": "Denies the stdin_write command without any pre-configured scope."
}
]
}
}
},
"then": {
"properties": {
"allow": {
"items": {
"title": "ShellScopeEntry",
"description": "Shell scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"cmd",
"name"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"cmd": {
"description": "The command name. It 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`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
}
},
"additionalProperties": false
}
]
}
},
"deny": {
"items": {
"title": "ShellScopeEntry",
"description": "Shell scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"cmd",
"name"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"cmd": {
"description": "The command name. It 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`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
}
},
"additionalProperties": false
}
]
}
}
}
},
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
}
}
},
{
"properties": {
"identifier": {
@ -2331,22 +2541,22 @@
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
},
{
"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\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`",
"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\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
"type": "string",
"const": "dialog:default",
"markdownDescription": "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\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`"
"markdownDescription": "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\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
},
{
"description": "Enables the ask command without any pre-configured scope.",
"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)",
"type": "string",
"const": "dialog:allow-ask",
"markdownDescription": "Enables the ask command without any pre-configured scope."
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the confirm command without any pre-configured scope.",
"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)",
"type": "string",
"const": "dialog:allow-confirm",
"markdownDescription": "Enables the confirm command without any pre-configured scope."
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the message command without any pre-configured scope.",
@ -2367,16 +2577,16 @@
"markdownDescription": "Enables the save command without any pre-configured scope."
},
{
"description": "Denies the ask command without any pre-configured scope.",
"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)",
"type": "string",
"const": "dialog:deny-ask",
"markdownDescription": "Denies the ask command without any pre-configured scope."
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the confirm command without any pre-configured scope.",
"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)",
"type": "string",
"const": "dialog:deny-confirm",
"markdownDescription": "Denies the confirm command without any pre-configured scope."
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the message command without any pre-configured scope.",
@ -2462,6 +2672,72 @@
"const": "opener:deny-reveal-item-in-dir",
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string",
"const": "shell:default",
"markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`"
},
{
"description": "Enables the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-execute",
"markdownDescription": "Enables the execute command without any pre-configured scope."
},
{
"description": "Enables the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-kill",
"markdownDescription": "Enables the kill command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-spawn",
"markdownDescription": "Enables the spawn command without any pre-configured scope."
},
{
"description": "Enables the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-stdin-write",
"markdownDescription": "Enables the stdin_write command without any pre-configured scope."
},
{
"description": "Denies the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-execute",
"markdownDescription": "Denies the execute command without any pre-configured scope."
},
{
"description": "Denies the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-kill",
"markdownDescription": "Denies the kill command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-spawn",
"markdownDescription": "Denies the spawn command without any pre-configured scope."
},
{
"description": "Denies the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-stdin-write",
"markdownDescription": "Denies the stdin_write command without any pre-configured scope."
},
{
"description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-filename`\n- `allow-restore-state`\n- `allow-save-window-state`",
"type": "string",
@ -2616,6 +2892,50 @@
"type": "string"
}
]
},
"ShellScopeEntryAllowedArg": {
"description": "A command argument allowed to be executed by the webview API.",
"anyOf": [
{
"description": "A non-configurable argument that is passed to the command in the order it was specified.",
"type": "string"
},
{
"description": "A variable that is set while calling the command from the webview API.",
"type": "object",
"required": [
"validator"
],
"properties": {
"raw": {
"description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.",
"default": false,
"type": "boolean"
},
"validator": {
"description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>",
"type": "string"
}
},
"additionalProperties": false
}
]
},
"ShellScopeEntryAllowedArgs": {
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
"anyOf": [
{
"description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
"type": "boolean"
},
{
"description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.",
"type": "array",
"items": {
"$ref": "#/definitions/ShellScopeEntryAllowedArg"
}
}
]
}
}
}

View file

@ -302,6 +302,216 @@
}
}
},
{
"if": {
"properties": {
"identifier": {
"anyOf": [
{
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string",
"const": "shell:default",
"markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`"
},
{
"description": "Enables the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-execute",
"markdownDescription": "Enables the execute command without any pre-configured scope."
},
{
"description": "Enables the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-kill",
"markdownDescription": "Enables the kill command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-spawn",
"markdownDescription": "Enables the spawn command without any pre-configured scope."
},
{
"description": "Enables the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-stdin-write",
"markdownDescription": "Enables the stdin_write command without any pre-configured scope."
},
{
"description": "Denies the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-execute",
"markdownDescription": "Denies the execute command without any pre-configured scope."
},
{
"description": "Denies the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-kill",
"markdownDescription": "Denies the kill command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-spawn",
"markdownDescription": "Denies the spawn command without any pre-configured scope."
},
{
"description": "Denies the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-stdin-write",
"markdownDescription": "Denies the stdin_write command without any pre-configured scope."
}
]
}
}
},
"then": {
"properties": {
"allow": {
"items": {
"title": "ShellScopeEntry",
"description": "Shell scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"cmd",
"name"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"cmd": {
"description": "The command name. It 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`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
}
},
"additionalProperties": false
}
]
}
},
"deny": {
"items": {
"title": "ShellScopeEntry",
"description": "Shell scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"cmd",
"name"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"cmd": {
"description": "The command name. It 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`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
}
},
"additionalProperties": false
}
]
}
}
}
},
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
}
}
},
{
"properties": {
"identifier": {
@ -2331,22 +2541,22 @@
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
},
{
"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\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`",
"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\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
"type": "string",
"const": "dialog:default",
"markdownDescription": "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\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`"
"markdownDescription": "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\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
},
{
"description": "Enables the ask command without any pre-configured scope.",
"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)",
"type": "string",
"const": "dialog:allow-ask",
"markdownDescription": "Enables the ask command without any pre-configured scope."
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the confirm command without any pre-configured scope.",
"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)",
"type": "string",
"const": "dialog:allow-confirm",
"markdownDescription": "Enables the confirm command without any pre-configured scope."
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the message command without any pre-configured scope.",
@ -2367,16 +2577,16 @@
"markdownDescription": "Enables the save command without any pre-configured scope."
},
{
"description": "Denies the ask command without any pre-configured scope.",
"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)",
"type": "string",
"const": "dialog:deny-ask",
"markdownDescription": "Denies the ask command without any pre-configured scope."
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the confirm command without any pre-configured scope.",
"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)",
"type": "string",
"const": "dialog:deny-confirm",
"markdownDescription": "Denies the confirm command without any pre-configured scope."
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the message command without any pre-configured scope.",
@ -2462,6 +2672,72 @@
"const": "opener:deny-reveal-item-in-dir",
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string",
"const": "shell:default",
"markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`"
},
{
"description": "Enables the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-execute",
"markdownDescription": "Enables the execute command without any pre-configured scope."
},
{
"description": "Enables the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-kill",
"markdownDescription": "Enables the kill command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-spawn",
"markdownDescription": "Enables the spawn command without any pre-configured scope."
},
{
"description": "Enables the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-stdin-write",
"markdownDescription": "Enables the stdin_write command without any pre-configured scope."
},
{
"description": "Denies the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-execute",
"markdownDescription": "Denies the execute command without any pre-configured scope."
},
{
"description": "Denies the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-kill",
"markdownDescription": "Denies the kill command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-spawn",
"markdownDescription": "Denies the spawn command without any pre-configured scope."
},
{
"description": "Denies the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-stdin-write",
"markdownDescription": "Denies the stdin_write command without any pre-configured scope."
},
{
"description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-filename`\n- `allow-restore-state`\n- `allow-save-window-state`",
"type": "string",
@ -2616,6 +2892,50 @@
"type": "string"
}
]
},
"ShellScopeEntryAllowedArg": {
"description": "A command argument allowed to be executed by the webview API.",
"anyOf": [
{
"description": "A non-configurable argument that is passed to the command in the order it was specified.",
"type": "string"
},
{
"description": "A variable that is set while calling the command from the webview API.",
"type": "object",
"required": [
"validator"
],
"properties": {
"raw": {
"description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.",
"default": false,
"type": "boolean"
},
"validator": {
"description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>",
"type": "string"
}
},
"additionalProperties": false
}
]
},
"ShellScopeEntryAllowedArgs": {
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
"anyOf": [
{
"description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
"type": "boolean"
},
{
"description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.",
"type": "array",
"items": {
"$ref": "#/definitions/ShellScopeEntryAllowedArg"
}
}
]
}
}
}

View file

@ -1,8 +1,7 @@
use crate::services::acp::GooseServeProcess;
#[tauri::command]
pub async fn get_goose_serve_url() -> Result<String, String> {
GooseServeProcess::start().await?;
let process = GooseServeProcess::get()?;
pub async fn get_goose_serve_url(app_handle: tauri::AppHandle) -> Result<String, String> {
let process = GooseServeProcess::get(app_handle).await?;
Ok(process.ws_url())
}

View file

@ -1,7 +1,7 @@
use tauri::{AppHandle, Emitter};
use tokio::io::{AsyncBufReadExt, BufReader};
use crate::services::acp::resolve_goose_binary;
use crate::services::acp::goose_serve::get_goose_command;
#[derive(serde::Serialize, Clone)]
#[serde(rename_all = "camelCase")]
@ -20,9 +20,9 @@ pub async fn authenticate_model_provider(
return Err("Native Goose sign-in is not supported on Windows yet".to_string());
}
let goose_binary = resolve_goose_binary()?;
let goose_command = get_goose_command(&app_handle)?;
let quoted_label = shell_quote(&provider_label);
let quoted_binary = shell_quote(&goose_binary.to_string_lossy());
let quoted_binary = shell_quote(&goose_command.as_std().get_program().to_string_lossy());
let command = if cfg!(target_os = "linux") {
format!(

View file

@ -9,6 +9,7 @@ use tauri_plugin_window_state::StateFlags;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let builder = tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(
tauri_plugin_log::Builder::new()
.level(log::LevelFilter::Debug)

View file

@ -1,4 +1,6 @@
use std::path::{Path, PathBuf};
use tauri_plugin_shell::ShellExt;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use tokio::process::{Child, Command};
@ -7,12 +9,6 @@ use tokio::sync::OnceCell;
const GOOSE_SERVE_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
const GOOSE_SERVE_CONNECT_RETRY_DELAY: Duration = Duration::from_millis(100);
const LOCALHOST: &str = "127.0.0.1";
const COMMON_GOOSE_PATHS: &[&str] = &[
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
"/home/linuxbrew/.linuxbrew/bin",
];
// ---------------------------------------------------------------------------
// GooseServeProcess — singleton that owns the long-lived `goose serve` child
// ---------------------------------------------------------------------------
@ -36,29 +32,15 @@ impl GooseServeProcess {
format!("ws://{LOCALHOST}:{}/acp", self.port)
}
/// Start the singleton `goose serve` process.
///
/// This is called once from `lib.rs` during app startup. Subsequent calls
/// are no-ops (the `OnceCell` ensures single initialisation). The process
/// is spawned with `kill_on_drop(true)` so it is automatically terminated
/// when the Tauri app exits.
pub async fn start() -> Result<(), String> {
GOOSE_SERVE
.get_or_try_init(|| async { Self::spawn().await })
.await
.map(|_| ())
}
/// Get a reference to the running process, or an error if it was never
/// started (should not happen in normal operation).
pub fn get() -> Result<&'static GooseServeProcess, String> {
pub async fn get(app_handle: tauri::AppHandle) -> Result<&'static GooseServeProcess, String> {
GOOSE_SERVE
.get()
.ok_or_else(|| "Goose serve process has not been started".to_string())
.get_or_try_init(|| async { Self::spawn(app_handle).await })
.await
}
async fn spawn() -> Result<GooseServeProcess, String> {
let binary_path = resolve_goose_binary()?;
async fn spawn(app_handle: tauri::AppHandle) -> Result<GooseServeProcess, String> {
let port = reserve_free_port()?;
// Use a stable working directory for the long-lived server process.
@ -71,7 +53,9 @@ impl GooseServeProcess {
)
})?;
let mut command = Command::new(&binary_path);
let mut command: Command = get_goose_command(&app_handle)?;
let binary_display = command.as_std().get_program().to_string_lossy().to_string();
command
.arg("serve")
.arg("--host")
@ -85,16 +69,13 @@ impl GooseServeProcess {
.kill_on_drop(true);
log::info!(
"Spawning long-lived goose serve: binary={} port={} cwd={}",
binary_path.display(),
port,
"Spawning long-lived goose serve: binary={binary_display} port={port} cwd={}",
working_dir.display(),
);
let mut child = command.spawn().map_err(|error| {
format!(
"Failed to spawn goose serve (binary: {}, cwd: {}): {error}",
binary_path.display(),
"Failed to spawn goose serve (binary: {binary_display}, cwd: {}): {error}",
working_dir.display()
)
})?;
@ -110,6 +91,19 @@ impl GooseServeProcess {
}
}
pub fn get_goose_command(app_handle: &tauri::AppHandle) -> Result<Command, String> {
if let Ok(override_path) = std::env::var("GOOSE_BIN") {
Ok(Command::new(override_path))
} else {
let tauri_command = app_handle
.shell()
.sidecar("goose")
.map_err(|e| format!("could not resolve goose binary: {e}"))?;
let std_command: std::process::Command = tauri_command.into();
Ok(std_command.into())
}
}
async fn wait_for_server_ready(port: u16, child: &mut Child) -> Result<(), String> {
let deadline = Instant::now() + GOOSE_SERVE_CONNECT_TIMEOUT;
let addr = format!("{LOCALHOST}:{port}");
@ -144,164 +138,6 @@ fn default_serve_working_dir() -> PathBuf {
.join("artifacts")
}
// ---------------------------------------------------------------------------
// Binary resolution
// ---------------------------------------------------------------------------
pub(crate) fn resolve_goose_binary() -> Result<PathBuf, String> {
let binary_path = if let Ok(override_path) = std::env::var("GOOSE_BIN") {
let path = PathBuf::from(&override_path);
if !path.exists() {
return Err(format!(
"GOOSE_BIN points to non-existent path: {override_path}"
));
}
if !goose_binary_supports_serve(&path)? {
return Err(format!(
"GOOSE_BIN points to a goose binary without `serve` support: {}",
path.display()
));
}
log::info!("Using GOOSE_BIN override: {override_path}");
path
} else {
let path = find_goose_binary()
.ok_or_else(|| "Unknown or unavailable agent provider: goose".to_string())?;
if !goose_binary_supports_serve(&path)? {
return Err(format!(
"Resolved goose binary does not support `serve`: {}. Set GOOSE_BIN to a newer goose binary.",
path.display()
));
}
log::info!(
"Resolved goose binary via local discovery: {}",
path.display()
);
path
};
// Log the binary version for debugging.
match std::process::Command::new(&binary_path)
.env("GOOSE_PATH_ROOT", goose_probe_root())
.arg("--version")
.output()
{
Ok(output) => {
let version = String::from_utf8_lossy(&output.stdout);
log::info!(
"Goose binary version: {} (path: {})",
version.trim(),
binary_path.display()
);
}
Err(err) => {
log::warn!(
"Could not determine goose binary version at {}: {err}",
binary_path.display()
);
}
}
Ok(binary_path)
}
fn find_goose_binary() -> Option<PathBuf> {
find_goose_via_login_shell().or_else(find_goose_in_common_paths)
}
fn find_goose_via_login_shell() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
None
}
#[cfg(not(target_os = "windows"))]
{
for shell in ["/bin/zsh", "/bin/bash"] {
let Ok(output) = std::process::Command::new(shell)
.args(["-l", "-c", "which goose"])
.output()
else {
continue;
};
if !output.status.success() {
continue;
}
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(path_str) = stdout.lines().rfind(|line| !line.trim().is_empty()) {
let path = PathBuf::from(path_str.trim());
if path.is_file() {
return Some(path);
}
}
}
None
}
}
fn find_goose_in_common_paths() -> Option<PathBuf> {
COMMON_GOOSE_PATHS
.iter()
.map(|dir| Path::new(dir).join(goose_binary_name()))
.find(|path| path.is_file())
}
fn goose_binary_name() -> &'static str {
#[cfg(target_os = "windows")]
{
"goose.exe"
}
#[cfg(not(target_os = "windows"))]
{
"goose"
}
}
fn goose_binary_supports_serve(binary_path: &PathBuf) -> Result<bool, String> {
let output = std::process::Command::new(binary_path)
.env("GOOSE_PATH_ROOT", goose_probe_root())
.arg("serve")
.arg("--help")
.output()
.map_err(|error| {
format!(
"Failed to probe goose binary {}: {error}",
binary_path.display()
)
})?;
if output.status.success() {
return Ok(true);
}
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
log::warn!(
"Goose binary probe failed for {}: status={} stderr={} stdout={}",
binary_path.display(),
output
.status
.code()
.map(|code| code.to_string())
.unwrap_or_else(|| "signal".to_string()),
stderr.trim(),
stdout.trim(),
);
Ok(false)
}
fn goose_probe_root() -> PathBuf {
std::env::temp_dir().join("block-goose2-goose-probe")
}
fn reserve_free_port() -> Result<u16, String> {
let listener = std::net::TcpListener::bind((LOCALHOST, 0))
.map_err(|error| format!("Failed to reserve Goose serve port: {error}"))?;

View file

@ -1,4 +1,3 @@
pub(crate) mod goose_serve;
pub(crate) use goose_serve::resolve_goose_binary;
pub(crate) use goose_serve::GooseServeProcess;

View file

@ -43,6 +43,7 @@
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
],
"externalBin": ["../../../target/release/goose"]
}
}

View file

@ -1,4 +1,7 @@
{
"identifier": "com.goose.app.dev",
"productName": "Goose Dev"
"productName": "Goose Dev",
"bundle": {
"externalBin": []
}
}

20
ui/pnpm-lock.yaml generated
View file

@ -468,11 +468,14 @@ importers:
specifier: ^2
version: 2.10.1
'@tauri-apps/plugin-dialog':
specifier: ~2.6.0
version: 2.6.0
specifier: ~2.7.0
version: 2.7.0
'@tauri-apps/plugin-opener':
specifier: ^2.5.3
version: 2.5.3
'@tauri-apps/plugin-shell':
specifier: ~2.3.5
version: 2.3.5
'@xyflow/react':
specifier: ^12.10.2
version: 12.10.2(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@ -3495,12 +3498,15 @@ packages:
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-dialog@2.6.0':
resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==}
'@tauri-apps/plugin-dialog@2.7.0':
resolution: {integrity: sha512-4nS/hfGMGCXiAS3LtVjH9AgsSAPJeG/7R+q8agTFqytjnMa4Zq95Bq8WzVDkckpanX+yyRHXnRtrKXkANKDHvw==}
'@tauri-apps/plugin-opener@2.5.3':
resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==}
'@tauri-apps/plugin-shell@2.3.5':
resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==}
'@testing-library/dom@10.4.1':
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
engines: {node: '>=18'}
@ -11794,7 +11800,7 @@ snapshots:
'@tauri-apps/cli-win32-ia32-msvc': 2.10.1
'@tauri-apps/cli-win32-x64-msvc': 2.10.1
'@tauri-apps/plugin-dialog@2.6.0':
'@tauri-apps/plugin-dialog@2.7.0':
dependencies:
'@tauri-apps/api': 2.10.1
@ -11802,6 +11808,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.10.1
'@tauri-apps/plugin-shell@2.3.5':
dependencies:
'@tauri-apps/api': 2.10.1
'@testing-library/dom@10.4.1':
dependencies:
'@babel/code-frame': 7.29.0