Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ members = [
"crates/acp-registry-client",
"crates/peekoo-mcp-server",
"crates/peekoo-analytics",
"crates/peekoo-python-sdk",
"apps/desktop-tauri/src-tauri",
]
resolver = "2"
Expand All @@ -42,4 +43,3 @@ strip = true




96 changes: 96 additions & 0 deletions apps/desktop-tauri/src-tauri/gen/schemas/macOS-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2636,6 +2636,60 @@
"const": "notification:deny-show",
"markdownDescription": "Denies the show command without any pre-configured scope."
},
{
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-capture`\n- `allow-identify`\n- `allow-alias`\n- `allow-reset`\n- `allow-get-distinct-id`\n- `allow-get-config`",
"type": "string",
"const": "posthog:default",
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-capture`\n- `allow-identify`\n- `allow-alias`\n- `allow-reset`\n- `allow-get-distinct-id`\n- `allow-get-config`"
Comment on lines +2639 to +2643
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These PostHog permission constants appear unrelated to the shared Python SDK runtime changes described in the PR. If this schema update is accidental or unrelated, consider reverting or splitting it into a separate PR to keep scope focused.

Copilot uses AI. Check for mistakes.
},
{
"description": "Allows creating user aliases in PostHog",
"type": "string",
"const": "posthog:allow-alias",
"markdownDescription": "Allows creating user aliases in PostHog"
},
{
"description": "Allows capturing PostHog events",
"type": "string",
"const": "posthog:allow-capture",
"markdownDescription": "Allows capturing PostHog events"
},
{
"description": "Allows getting PostHog configuration for frontend initialization",
"type": "string",
"const": "posthog:allow-get-config",
"markdownDescription": "Allows getting PostHog configuration for frontend initialization"
},
{
"description": "Allows getting the current distinct ID",
"type": "string",
"const": "posthog:allow-get-distinct-id",
"markdownDescription": "Allows getting the current distinct ID"
},
{
"description": "Allows identifying users in PostHog",
"type": "string",
"const": "posthog:allow-identify",
"markdownDescription": "Allows identifying users in PostHog"
},
{
"description": "Enables the ping command without any pre-configured scope.",
"type": "string",
"const": "posthog:allow-ping",
"markdownDescription": "Enables the ping command without any pre-configured scope."
},
{
"description": "Allows resetting user data in PostHog",
"type": "string",
"const": "posthog:allow-reset",
"markdownDescription": "Allows resetting user data in PostHog"
},
{
"description": "Denies the ping command without any pre-configured scope.",
"type": "string",
"const": "posthog:deny-ping",
"markdownDescription": "Denies the ping command without any pre-configured scope."
},
{
"description": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`",
"type": "string",
Expand Down Expand Up @@ -2666,6 +2720,48 @@
"const": "process:deny-restart",
"markdownDescription": "Denies the restart command without any pre-configured scope."
},
{
"description": "Allows all required permissions (envelope, breadcrumb)\n#### This default permission set includes:\n\n- `allow-envelope`\n- `allow-breadcrumb`",
"type": "string",
"const": "sentry:default",
"markdownDescription": "Allows all required permissions (envelope, breadcrumb)\n#### This default permission set includes:\n\n- `allow-envelope`\n- `allow-breadcrumb`"
},
{
"description": "Enables the breadcrumb command.",
"type": "string",
"const": "sentry:allow-breadcrumb",
"markdownDescription": "Enables the breadcrumb command."
},
{
"description": "Enables the envelope command.",
"type": "string",
"const": "sentry:allow-envelope",
"markdownDescription": "Enables the envelope command."
},
{
"description": "Enables the event command without any pre-configured scope.",
"type": "string",
"const": "sentry:allow-event",
"markdownDescription": "Enables the event command without any pre-configured scope."
},
{
"description": "Denies the breadcrumb command.",
"type": "string",
"const": "sentry:deny-breadcrumb",
"markdownDescription": "Denies the breadcrumb command."
},
{
"description": "Denies the envelope command.",
"type": "string",
"const": "sentry:deny-envelope",
"markdownDescription": "Denies the envelope command."
},
{
"description": "Denies the event command without any pre-configured scope.",
"type": "string",
"const": "sentry:deny-event",
"markdownDescription": "Denies the event 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",
Expand Down
76 changes: 71 additions & 5 deletions crates/peekoo-plugin-host/src/host_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -884,13 +884,32 @@ fn host_process_exec(
) -> Result<(), Error> {
let ctx = user_data.get().map_err(|e| Error::msg(format!("{e}")))?;
let ctx = ctx.lock().map_err(|e| Error::msg(format!("{e}")))?;
require_declared_capability(&ctx, "process:exec")?;
if let Err(err) = require_declared_capability(&ctx, "process:exec") {
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

host_process_exec only verifies that process:exec is declared (require_declared_capability) and does not check whether it is granted. This bypasses the optional permission model used elsewhere (require_capability). Please enforce the grant check here (you can still return an ok:false JSON response instead of trapping).

Suggested change
if let Err(err) = require_declared_capability(&ctx, "process:exec") {
if let Err(err) = require_capability(&ctx, "process:exec") {

Copilot uses AI. Check for mistakes.
let response = serde_json::json!({
"ok": false,
"statusCode": -1,
"stdout": "",
"stderr": err.to_string(),
})
.to_string();
write_output(plugin, outputs, &response)?;
return Ok(());
}

let input_str = read_input(plugin, inputs)?;
let req: serde_json::Value = serde_json::from_str(&input_str).unwrap_or_default();

let program = req["program"].as_str().unwrap_or_default().trim();
if program.is_empty() {
return Err(Error::msg("process program is required"));
let response = serde_json::json!({
"ok": false,
"statusCode": -1,
"stdout": "",
"stderr": "process program is required",
})
.to_string();
write_output(plugin, outputs, &response)?;
return Ok(());
}

let args = req["args"]
Expand All @@ -903,13 +922,44 @@ fn host_process_exec(
.unwrap_or_default();

let cwd = req["cwd"].as_str().unwrap_or(".");
let resolved_cwd = resolve_process_cwd(&ctx, cwd)?;
let resolved_cwd = match resolve_process_cwd(&ctx, cwd) {
Ok(path) => path,
Err(err) => {
let response = serde_json::json!({
"ok": false,
"statusCode": -1,
"stdout": "",
"stderr": err.to_string(),
})
.to_string();
write_output(plugin, outputs, &response)?;
return Ok(());
}
};

let output = Command::new(program)
let resolved_program = resolve_process_program(program, &resolved_cwd);
let output = match Command::new(&resolved_program)
.args(&args)
.current_dir(&resolved_cwd)
.output()
.map_err(|e| Error::msg(format!("Process spawn failed: {e}")))?;
{
Ok(output) => output,
Err(e) => {
let response = serde_json::json!({
"ok": false,
"statusCode": -1,
"stdout": "",
"stderr": format!(
"Process spawn failed: program='{}' cwd='{}' ({e})",
resolved_program.display(),
resolved_cwd.display()
),
Comment on lines +952 to +956
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spawn error string includes resolved_program and resolved_cwd (often absolute paths under the user’s home directory). If this message is surfaced in plugin UIs/logs, it can leak local filesystem paths. Consider redacting/sanitizing these values or limiting detail.

Suggested change
"stderr": format!(
"Process spawn failed: program='{}' cwd='{}' ({e})",
resolved_program.display(),
resolved_cwd.display()
),
"stderr": format!("Process spawn failed ({e})"),

Copilot uses AI. Check for mistakes.
})
.to_string();
write_output(plugin, outputs, &response)?;
return Ok(());
}
};

let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Expand All @@ -930,6 +980,22 @@ fn host_process_exec(
Ok(())
}

fn resolve_process_program(program: &str, cwd: &Path) -> PathBuf {
let expanded = expand_tilde_path(program);
let candidate = PathBuf::from(expanded);
if candidate.is_absolute() {
return candidate;
}

// Keep PATH lookup behavior for plain command names (e.g. "python3").
let has_path_separator = program.contains('/') || program.contains('\\');
if !has_path_separator {
return candidate;
}

cwd.join(candidate)
}

fn host_set_mood(
plugin: &mut CurrentPlugin,
inputs: &[Val],
Expand Down
2 changes: 2 additions & 0 deletions crates/peekoo-plugin-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ pub mod log;
pub mod mood;
pub mod notify;
pub mod oauth;
pub mod process;
pub mod schedule;
pub mod secrets;
pub mod state;
Expand Down Expand Up @@ -125,6 +126,7 @@ pub mod peekoo {
pub use crate::mood;
pub use crate::notify;
pub use crate::oauth;
pub use crate::process;
pub use crate::schedule;
pub use crate::secrets;
pub use crate::state;
Expand Down
8 changes: 8 additions & 0 deletions crates/peekoo-python-sdk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "peekoo-python-sdk"
version = "0.1.0"
edition = "2024"
license = "MIT"

[lib]
path = "src/lib.rs"
56 changes: 56 additions & 0 deletions crates/peekoo-python-sdk/scripts/build_runtime_package.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail

REQ_FILE="${1:-}"
OUT_FILE="${2:-}"

if [[ -z "$REQ_FILE" || -z "$OUT_FILE" ]]; then
echo "usage: $0 <requirements.txt> <output.tar.gz>" >&2
exit 1
fi

if [[ ! -f "$REQ_FILE" ]]; then
echo "requirements file not found: $REQ_FILE" >&2
exit 1
fi

platform="$(uname -s | tr '[:upper:]' '[:lower:]')"
arch="$(uname -m)"
case "$platform:$arch" in
darwin:arm64) target_triple="aarch64-apple-darwin" ;;
darwin:x86_64) target_triple="x86_64-apple-darwin" ;;
linux:x86_64) target_triple="x86_64-unknown-linux-gnu" ;;
linux:aarch64) target_triple="aarch64-unknown-linux-gnu" ;;
*)
echo "unsupported platform: $platform/$arch" >&2
exit 1
;;
esac

release_tag="${PEEKOO_PYTHON_STANDALONE_TAG:-20250317}"
archive_url="${PEEKOO_PYTHON_STANDALONE_URL:-https://github.com/indygreg/python-build-standalone/releases/download/${release_tag}/cpython-3.12.9+${release_tag}-${target_triple}-install_only.tar.gz}"

workdir="$(mktemp -d)"
trap 'rm -rf "$workdir"' EXIT

archive_path="$workdir/python-runtime.tar.gz"
curl -fL "$archive_url" -o "$archive_path"
Comment on lines +31 to +37
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runtime packaging script downloads and extracts a prebuilt Python archive without any integrity verification. Consider adding checksum/signature verification (e.g., pinned SHA256 for the archive URL) to reduce the risk of compromised downloads.

Suggested change
archive_url="${PEEKOO_PYTHON_STANDALONE_URL:-https://github.com/indygreg/python-build-standalone/releases/download/${release_tag}/cpython-3.12.9+${release_tag}-${target_triple}-install_only.tar.gz}"
workdir="$(mktemp -d)"
trap 'rm -rf "$workdir"' EXIT
archive_path="$workdir/python-runtime.tar.gz"
curl -fL "$archive_url" -o "$archive_path"
archive_url="${PEEKOO_PYTHON_STANDALONE_URL:-https://github.com/indygreg/python-build-standalone/releases/download/${release_tag}/cpython-3.12.9+${release_tag}-${target_triple}-install_only.tar.gz}"
archive_sha256="${PEEKOO_PYTHON_STANDALONE_SHA256:-}"
if [[ -z "$archive_sha256" ]]; then
echo "missing required SHA256 for runtime archive: set PEEKOO_PYTHON_STANDALONE_SHA256" >&2
exit 1
fi
workdir="$(mktemp -d)"
trap 'rm -rf "$workdir"' EXIT
archive_path="$workdir/python-runtime.tar.gz"
curl -fL "$archive_url" -o "$archive_path"
if command -v sha256sum >/dev/null 2>&1; then
actual_sha256="$(sha256sum "$archive_path" | awk '{print $1}')"
elif command -v shasum >/dev/null 2>&1; then
actual_sha256="$(shasum -a 256 "$archive_path" | awk '{print $1}')"
else
echo "no SHA-256 tool available; install sha256sum or shasum" >&2
exit 1
fi
if [[ "$actual_sha256" != "$archive_sha256" ]]; then
echo "downloaded runtime archive SHA256 mismatch" >&2
echo "expected: $archive_sha256" >&2
echo "actual: $actual_sha256" >&2
exit 1
fi

Copilot uses AI. Check for mistakes.
tar -xzf "$archive_path" -C "$workdir"

if [[ -x "$workdir/python/bin/python3" ]]; then
py_bin="$workdir/python/bin/python3"
elif [[ -x "$workdir/python/bin/python" ]]; then
py_bin="$workdir/python/bin/python"
else
echo "python binary not found in extracted runtime" >&2
exit 1
fi

"$py_bin" -m ensurepip --upgrade || true
"$py_bin" -m pip install --upgrade pip setuptools wheel
"$py_bin" -m pip install --no-cache-dir -r "$REQ_FILE"

mkdir -p "$(dirname "$OUT_FILE")"
tar -czf "$OUT_FILE" -C "$workdir" python

echo "Python runtime package created: $OUT_FILE"
21 changes: 21 additions & 0 deletions crates/peekoo-python-sdk/scripts/install_runtime_package.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail

PACKAGE_FILE="${1:-}"
TARGET_DIR="${2:-$HOME/.peekoo/python-sdk}"

if [[ -z "$PACKAGE_FILE" ]]; then
echo "usage: $0 <runtime-package.tar.gz> [target-dir]" >&2
exit 1
fi

if [[ ! -f "$PACKAGE_FILE" ]]; then
echo "package file not found: $PACKAGE_FILE" >&2
exit 1
fi

mkdir -p "$TARGET_DIR"
rm -rf "$TARGET_DIR/python"
tar -xzf "$PACKAGE_FILE" -C "$TARGET_DIR"

echo "Python runtime installed to: $TARGET_DIR/python"
39 changes: 39 additions & 0 deletions crates/peekoo-python-sdk/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
pub const SHARED_PYTHON_SDK_ROOT: &str = "~/.peekoo/python-sdk";

pub fn shared_python_candidates() -> Vec<String> {
vec![
format!("{SHARED_PYTHON_SDK_ROOT}/python/bin/python3"),
format!("{SHARED_PYTHON_SDK_ROOT}/python/bin/python"),
format!("{SHARED_PYTHON_SDK_ROOT}/python/python.exe"),
format!("{SHARED_PYTHON_SDK_ROOT}/python/bin/python.exe"),
]
}

pub fn plugin_local_python_candidates() -> Vec<String> {
vec![
"runtime/python/bin/python3".to_string(),
"runtime/python/bin/python".to_string(),
"runtime/python/python.exe".to_string(),
"runtime/python/bin/python.exe".to_string(),
]
}

pub fn system_python_candidates() -> Vec<String> {
vec![
"python3".to_string(),
"python".to_string(),
"py".to_string(),
]
}

pub fn all_python_candidates() -> Vec<String> {
let mut out = Vec::new();
out.extend(shared_python_candidates());
out.extend(plugin_local_python_candidates());
out.extend(system_python_candidates());
out
}

pub fn is_spawn_error_message(message: &str) -> bool {
message.contains("Process spawn failed")
}
Loading
Loading