patchwork-os 0.2.0-beta.1 → 0.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deploy/deploy-dashboard.sh +25 -1
- package/deploy/macos/README.md +153 -0
- package/deploy/macos/com.patchwork.bridge.plist.template +54 -0
- package/deploy/macos/com.patchwork.tunnel.plist.template +76 -0
- package/deploy/macos/install-mac-bridge.sh +244 -0
- package/deploy/macos/uninstall-mac-bridge.sh +22 -0
- package/dist/approvalHttp.d.ts +14 -0
- package/dist/approvalHttp.js +172 -1
- package/dist/approvalHttp.js.map +1 -1
- package/dist/approvalQueue.d.ts +27 -2
- package/dist/approvalQueue.js +44 -7
- package/dist/approvalQueue.js.map +1 -1
- package/dist/automation.d.ts +34 -3
- package/dist/automation.js +85 -10
- package/dist/automation.js.map +1 -1
- package/dist/bridge.js +3 -1
- package/dist/bridge.js.map +1 -1
- package/dist/claudeOrchestrator.js +5 -2
- package/dist/claudeOrchestrator.js.map +1 -1
- package/dist/commands/recipe.js +10 -1
- package/dist/commands/recipe.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -1
- package/dist/connectors/baseConnector.js +25 -3
- package/dist/connectors/baseConnector.js.map +1 -1
- package/dist/drivers/gemini/index.d.ts +22 -0
- package/dist/drivers/gemini/index.js +240 -129
- package/dist/drivers/gemini/index.js.map +1 -1
- package/dist/drivers/local/index.d.ts +17 -0
- package/dist/drivers/local/index.js +99 -0
- package/dist/drivers/local/index.js.map +1 -1
- package/dist/drivers/openai/index.js +30 -2
- package/dist/drivers/openai/index.js.map +1 -1
- package/dist/extensionClient.d.ts +8 -0
- package/dist/extensionClient.js +24 -2
- package/dist/extensionClient.js.map +1 -1
- package/dist/fp/automationInterpreter.d.ts +9 -1
- package/dist/fp/automationInterpreter.js +151 -34
- package/dist/fp/automationInterpreter.js.map +1 -1
- package/dist/fp/automationProgram.d.ts +30 -0
- package/dist/fp/automationProgram.js.map +1 -1
- package/dist/fp/automationState.d.ts +23 -4
- package/dist/fp/automationState.js +28 -4
- package/dist/fp/automationState.js.map +1 -1
- package/dist/fp/interpreterContext.d.ts +66 -1
- package/dist/fp/interpreterContext.js +140 -1
- package/dist/fp/interpreterContext.js.map +1 -1
- package/dist/fp/policyParser.js +29 -1
- package/dist/fp/policyParser.js.map +1 -1
- package/dist/oauth.d.ts +9 -0
- package/dist/oauth.js +33 -0
- package/dist/oauth.js.map +1 -1
- package/dist/patchworkConfig.d.ts +16 -0
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/recipes/scheduler.d.ts +7 -0
- package/dist/recipes/scheduler.js +30 -13
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/schema.d.ts +6 -0
- package/dist/recipes/tools/file.js +5 -2
- package/dist/recipes/tools/file.js.map +1 -1
- package/dist/recipes/yamlRunner.d.ts +17 -0
- package/dist/recipes/yamlRunner.js +60 -3
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +3 -1
- package/dist/recipesHttp.js +9 -3
- package/dist/recipesHttp.js.map +1 -1
- package/dist/server.d.ts +46 -1
- package/dist/server.js +178 -3
- package/dist/server.js.map +1 -1
- package/dist/streamableHttp.d.ts +9 -4
- package/dist/streamableHttp.js +17 -9
- package/dist/streamableHttp.js.map +1 -1
- package/dist/tools/openInBrowser.js +6 -1
- package/dist/tools/openInBrowser.js.map +1 -1
- package/dist/tools/utils.js +7 -4
- package/dist/tools/utils.js.map +1 -1
- package/package.json +2 -2
|
@@ -39,9 +39,17 @@ echo "==> Copying tarball to VPS..."
|
|
|
39
39
|
scp "$TARBALL" "$VPS:/tmp/patchwork-dashboard.tar.gz"
|
|
40
40
|
|
|
41
41
|
echo "==> Deploying on VPS..."
|
|
42
|
+
# Pass secrets as positional args (NOT inside the heredoc body) so the
|
|
43
|
+
# single-quoted heredoc still preserves remote-shell `$X` references but
|
|
44
|
+
# the operator's local env reaches the VPS. Without this, the previous
|
|
45
|
+
# `${PATCHWORK_BRIDGE_TOKEN:-REPLACE_ME}` inside the heredoc evaluated on
|
|
46
|
+
# the remote, where the var doesn't exist, and always wrote REPLACE_ME.
|
|
42
47
|
# shellcheck disable=SC2087
|
|
43
|
-
ssh "$VPS" bash <<'REMOTE'
|
|
48
|
+
ssh "$VPS" bash -s -- "${PATCHWORK_BRIDGE_TOKEN:-REPLACE_ME}" "${DASHBOARD_PASSWORD:-}" <<'REMOTE'
|
|
44
49
|
set -euo pipefail
|
|
50
|
+
# `${N:-}` so an empty/missing positional arg doesn't trip `set -u`.
|
|
51
|
+
PATCHWORK_BRIDGE_TOKEN="${1:-REPLACE_ME}"
|
|
52
|
+
DASHBOARD_PASSWORD="${2:-}"
|
|
45
53
|
REMOTE_DIR="/opt/patchwork-dashboard"
|
|
46
54
|
PM2_NAME="patchwork-dashboard"
|
|
47
55
|
PORT=3200
|
|
@@ -52,10 +60,26 @@ if pm2 list | grep -q "$PM2_NAME"; then
|
|
|
52
60
|
pm2 delete "$PM2_NAME" || true
|
|
53
61
|
fi
|
|
54
62
|
|
|
63
|
+
# Preserve .env.local across the deploy. Without this stash/restore, the
|
|
64
|
+
# `rm -rf "$REMOTE_DIR"` below blows away every secret the operator pasted
|
|
65
|
+
# (VAPID, PATCHWORK_PUSH_TOKEN, custom DASHBOARD_PASSWORD), and the "if
|
|
66
|
+
# already exists, preserve" branch later in this script never fires —
|
|
67
|
+
# the file no longer exists by then.
|
|
68
|
+
ENV_BACKUP=""
|
|
69
|
+
if [ -f "$REMOTE_DIR/.env.local" ]; then
|
|
70
|
+
ENV_BACKUP="$(mktemp /tmp/patchwork-env.XXXXXX)"
|
|
71
|
+
cp -p "$REMOTE_DIR/.env.local" "$ENV_BACKUP"
|
|
72
|
+
fi
|
|
73
|
+
|
|
55
74
|
# Wipe and recreate deploy dir
|
|
56
75
|
rm -rf "$REMOTE_DIR"
|
|
57
76
|
mkdir -p "$REMOTE_DIR"
|
|
58
77
|
|
|
78
|
+
if [ -n "$ENV_BACKUP" ] && [ -f "$ENV_BACKUP" ]; then
|
|
79
|
+
cp -p "$ENV_BACKUP" "$REMOTE_DIR/.env.local"
|
|
80
|
+
rm -f "$ENV_BACKUP"
|
|
81
|
+
fi
|
|
82
|
+
|
|
59
83
|
# Extract
|
|
60
84
|
tar -xzf /tmp/patchwork-dashboard.tar.gz -C "$REMOTE_DIR"
|
|
61
85
|
rm /tmp/patchwork-dashboard.tar.gz
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# macOS launchd setup for the bridge
|
|
2
|
+
|
|
3
|
+
Make the local `claude-ide-bridge` and the SSH reverse tunnel to your
|
|
4
|
+
self-hosted dashboard persistent on your Mac:
|
|
5
|
+
|
|
6
|
+
- Auto-start at login
|
|
7
|
+
- Auto-restart on crash (bridge has its own `--watch` supervisor; launchd
|
|
8
|
+
is the supervisor of the supervisor)
|
|
9
|
+
- Auto-reconnect on network change / sleep / wake (autossh)
|
|
10
|
+
- No tmux session to keep alive, no terminal to leave open
|
|
11
|
+
|
|
12
|
+
## One-time setup
|
|
13
|
+
|
|
14
|
+
1. **Install the patchwork CLI globally** (not via `npm link`). LaunchAgents
|
|
15
|
+
run in a tighter sandbox; symlinked installs into `~/Documents` /
|
|
16
|
+
`~/Desktop` / `~/Downloads` fail with `EPERM`:
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g patchwork-os
|
|
19
|
+
# or, from a local repo:
|
|
20
|
+
# npm pack && npm install -g patchwork-os-*.tgz
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
2. **Make sure you can SSH to the VPS without a password prompt.** The
|
|
24
|
+
tunnel runs unattended; if SSH would prompt for a key passphrase,
|
|
25
|
+
add the key to your agent (`ssh-add ~/.ssh/id_ed25519`) or use a
|
|
26
|
+
passphrase-less key.
|
|
27
|
+
|
|
28
|
+
3. **(Recommended) Pin the SSH target in `~/.ssh/config` first.** Using
|
|
29
|
+
the public domain as the SSH target is fragile — DNS drifts during
|
|
30
|
+
redeploys (CDN edges, transient IPs), so SSH lands on a different
|
|
31
|
+
machine and the host key changes. Add an alias once and use it
|
|
32
|
+
everywhere:
|
|
33
|
+
|
|
34
|
+
```ssh-config
|
|
35
|
+
# ~/.ssh/config
|
|
36
|
+
Host pw-bridge
|
|
37
|
+
HostName 185.167.97.141 # your VPS IP — stable across DNS shifts
|
|
38
|
+
User wesh # or root, whatever your VPS allows
|
|
39
|
+
IdentityFile ~/.ssh/id_ed25519
|
|
40
|
+
ServerAliveInterval 30
|
|
41
|
+
ServerAliveCountMax 3
|
|
42
|
+
UserKnownHostsFile ~/.ssh/known_hosts.patchwork
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Verify it works once: `ssh pw-bridge "echo ok"`. The host key is
|
|
46
|
+
accepted into the dedicated `known_hosts.patchwork` file and stays
|
|
47
|
+
there even if you `ssh-keygen -R` your global known_hosts later.
|
|
48
|
+
|
|
49
|
+
4. **Run the installer** with the alias (cleanest):
|
|
50
|
+
```bash
|
|
51
|
+
VPS_HOST=pw-bridge bash deploy/macos/install-mac-bridge.sh
|
|
52
|
+
```
|
|
53
|
+
The installer detects the alias via `ssh -G` and lets ssh_config
|
|
54
|
+
own user + identity + hostname resolution. On a VPS rebuild, you
|
|
55
|
+
only update `~/.ssh/config` — the LaunchAgents pick it up.
|
|
56
|
+
|
|
57
|
+
Without an alias (interactive, prompts for VPS host):
|
|
58
|
+
```bash
|
|
59
|
+
bash deploy/macos/install-mac-bridge.sh
|
|
60
|
+
# → prompts; recommend the IP (185.167.97.141) over the domain
|
|
61
|
+
```
|
|
62
|
+
Or fully env-driven:
|
|
63
|
+
```bash
|
|
64
|
+
VPS_HOST=185.167.97.141 VPS_USER=root \
|
|
65
|
+
BRIDGE_PORT=63906 VPS_PORT=3285 \
|
|
66
|
+
bash deploy/macos/install-mac-bridge.sh
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
5. **Sync the bridge token to the VPS** (the installer prints the exact
|
|
70
|
+
`ssh … sed … pm2 restart` command — copy-paste).
|
|
71
|
+
|
|
72
|
+
## What runs after install
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
~/Library/LaunchAgents/com.patchwork.bridge.plist
|
|
76
|
+
~/Library/LaunchAgents/com.patchwork.tunnel.plist
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Both are loaded into the per-user `gui/$UID` launchd domain at install
|
|
80
|
+
time and at every login.
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
[claude-ide-bridge --port 63906 --fixed-token <…> --watch]
|
|
84
|
+
│
|
|
85
|
+
│ 127.0.0.1:63906
|
|
86
|
+
▼
|
|
87
|
+
[autossh -R 127.0.0.1:3285:localhost:63906]
|
|
88
|
+
│
|
|
89
|
+
│ encrypted SSH reverse tunnel
|
|
90
|
+
▼
|
|
91
|
+
VPS:3285 ──── nginx ──── dashboard `/api/bridge/*`
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Logs
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
tail -f ~/Library/Logs/patchwork-bridge.log
|
|
98
|
+
tail -f ~/Library/Logs/patchwork-tunnel.log
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Status
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
launchctl print gui/$UID/com.patchwork.bridge
|
|
105
|
+
launchctl print gui/$UID/com.patchwork.tunnel
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Restart manually
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
launchctl kickstart -k gui/$UID/com.patchwork.bridge
|
|
112
|
+
launchctl kickstart -k gui/$UID/com.patchwork.tunnel
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Uninstall
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
bash deploy/macos/uninstall-mac-bridge.sh
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Troubleshooting
|
|
122
|
+
|
|
123
|
+
- **`com.patchwork.bridge` exits with status 78 / `EPERM`** — the bridge
|
|
124
|
+
CLI is installed via `npm link` and points into `~/Documents`. The
|
|
125
|
+
macOS sandbox blocks LaunchAgents from reading that tree. Reinstall
|
|
126
|
+
globally as a real package (see step 1).
|
|
127
|
+
- **`com.patchwork.tunnel` keeps respawning** — check the SSH key is
|
|
128
|
+
added to `ssh-agent` (`ssh-add -l`). LaunchAgents run before the
|
|
129
|
+
user's shell rc, so an interactive `ssh-add` from `.zshrc` doesn't
|
|
130
|
+
apply. Use a passphrase-less key, or store the key in macOS Keychain
|
|
131
|
+
with `ssh-add --apple-use-keychain`.
|
|
132
|
+
- **Tunnel succeeds but dashboard says offline** — VPS port already in
|
|
133
|
+
use by an old leftover bridge. SSH to the VPS and `pkill -f "autossh\|sshd: .*\@notty"`,
|
|
134
|
+
or pick a different `VPS_PORT`.
|
|
135
|
+
- **Dashboard 401s on every bridge call** — `PATCHWORK_BRIDGE_TOKEN`
|
|
136
|
+
on the VPS doesn't match the `--fixed-token` value the bridge is
|
|
137
|
+
using. Re-run the install script's printed `sed … pm2 restart`
|
|
138
|
+
command.
|
|
139
|
+
- **`Host key for X has changed and you have requested strict
|
|
140
|
+
checking`** every now and then — the SSH target is moving. Two
|
|
141
|
+
causes:
|
|
142
|
+
1. **DNS drift.** The public domain resolves to a different IP than
|
|
143
|
+
it did last time (CDN edge, redeploy churn). `dig +short
|
|
144
|
+
bridge.your.tld` vs your known VPS IP — if those differ, switch
|
|
145
|
+
the `VPS_HOST` to the IP or a `~/.ssh/config` alias and re-run
|
|
146
|
+
the installer.
|
|
147
|
+
2. **VPS rebuild.** A reimage regenerates `/etc/ssh/ssh_host_*_key`
|
|
148
|
+
files. Either back them up before the rebuild and restore after,
|
|
149
|
+
or accept the new key once with `ssh-keygen -R <host> && ssh
|
|
150
|
+
<host>`. The launchd tunnel uses `StrictHostKeyChecking=accept-new`
|
|
151
|
+
(only auto-trusts on first connect, not after change), so you'll
|
|
152
|
+
see the warning, accept the new key interactively once, and the
|
|
153
|
+
tunnel resumes.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.patchwork.bridge</string>
|
|
7
|
+
|
|
8
|
+
<key>ProgramArguments</key>
|
|
9
|
+
<array>
|
|
10
|
+
<string>{{BRIDGE_BIN}}</string>
|
|
11
|
+
<string>--port</string>
|
|
12
|
+
<string>{{BRIDGE_PORT}}</string>
|
|
13
|
+
<string>--fixed-token</string>
|
|
14
|
+
<string>{{BRIDGE_TOKEN}}</string>
|
|
15
|
+
<string>--watch</string>
|
|
16
|
+
<string>--workspace</string>
|
|
17
|
+
<string>{{WORKSPACE}}</string>
|
|
18
|
+
</array>
|
|
19
|
+
|
|
20
|
+
<key>RunAtLoad</key>
|
|
21
|
+
<true/>
|
|
22
|
+
<!-- KeepAlive is redundant with `--watch` (which has its own
|
|
23
|
+
exponential-backoff supervisor and recovers from crashes faster
|
|
24
|
+
than launchd's per-second relaunch). Leave it true as belt-and-
|
|
25
|
+
suspenders in case `--watch` itself dies (its supervisor process
|
|
26
|
+
is a single Node, and that's the one launchd is keeping alive). -->
|
|
27
|
+
<key>KeepAlive</key>
|
|
28
|
+
<true/>
|
|
29
|
+
|
|
30
|
+
<key>StandardOutPath</key>
|
|
31
|
+
<string>{{HOME}}/Library/Logs/patchwork-bridge.log</string>
|
|
32
|
+
<key>StandardErrorPath</key>
|
|
33
|
+
<string>{{HOME}}/Library/Logs/patchwork-bridge.log</string>
|
|
34
|
+
|
|
35
|
+
<key>WorkingDirectory</key>
|
|
36
|
+
<string>{{WORKSPACE}}</string>
|
|
37
|
+
|
|
38
|
+
<key>EnvironmentVariables</key>
|
|
39
|
+
<dict>
|
|
40
|
+
<!-- launchd's default PATH doesn't include Homebrew. Bridge spawns
|
|
41
|
+
child processes (claude CLI, git, npm) via PATH lookup, so
|
|
42
|
+
missing /opt/homebrew/bin breaks half the tools. -->
|
|
43
|
+
<key>PATH</key>
|
|
44
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
45
|
+
<key>HOME</key>
|
|
46
|
+
<string>{{HOME}}</string>
|
|
47
|
+
</dict>
|
|
48
|
+
|
|
49
|
+
<!-- Throttle: don't relaunch faster than every 10 s on rapid crash
|
|
50
|
+
loops, prevents spinning up if the binary is broken. -->
|
|
51
|
+
<key>ThrottleInterval</key>
|
|
52
|
+
<integer>10</integer>
|
|
53
|
+
</dict>
|
|
54
|
+
</plist>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.patchwork.tunnel</string>
|
|
7
|
+
|
|
8
|
+
<key>ProgramArguments</key>
|
|
9
|
+
<array>
|
|
10
|
+
<string>{{AUTOSSH_BIN}}</string>
|
|
11
|
+
<!-- AUTOSSH_PORT=0 disables the legacy monitoring-port heartbeat in
|
|
12
|
+
favor of SSH's own ServerAliveInterval/CountMax. Modern autossh
|
|
13
|
+
docs recommend this; the monitoring port is unreliable behind
|
|
14
|
+
NAT and consumes another tunnel slot. -->
|
|
15
|
+
<string>-M</string>
|
|
16
|
+
<string>0</string>
|
|
17
|
+
<!-- -N: no remote command -T: no pseudo-tty -->
|
|
18
|
+
<string>-N</string>
|
|
19
|
+
<string>-T</string>
|
|
20
|
+
<string>-R</string>
|
|
21
|
+
<string>127.0.0.1:{{VPS_PORT}}:localhost:{{BRIDGE_PORT}}</string>
|
|
22
|
+
<!-- ServerAliveInterval=30 + CountMax=3 = ssh kills the link if
|
|
23
|
+
the server doesn't respond for 90 s, autossh restarts. Critical
|
|
24
|
+
for laptop sleep/wake and Wi-Fi switches. -->
|
|
25
|
+
<string>-o</string>
|
|
26
|
+
<string>ServerAliveInterval=30</string>
|
|
27
|
+
<string>-o</string>
|
|
28
|
+
<string>ServerAliveCountMax=3</string>
|
|
29
|
+
<!-- ExitOnForwardFailure=yes makes autossh exit + retry if the
|
|
30
|
+
remote port is already bound. Otherwise the tunnel "succeeds"
|
|
31
|
+
but no traffic flows. -->
|
|
32
|
+
<string>-o</string>
|
|
33
|
+
<string>ExitOnForwardFailure=yes</string>
|
|
34
|
+
<!-- Don't auto-add new host keys (security: surface key changes
|
|
35
|
+
instead of silently trusting). Initial setup expects the user
|
|
36
|
+
to have already SSH'd at least once and accepted the key. -->
|
|
37
|
+
<string>-o</string>
|
|
38
|
+
<string>StrictHostKeyChecking=accept-new</string>
|
|
39
|
+
<string>-i</string>
|
|
40
|
+
<string>{{SSH_KEY}}</string>
|
|
41
|
+
<!-- SSH_TARGET is rendered by install-mac-bridge.sh:
|
|
42
|
+
- "user@host" form when host is an IP or hostname
|
|
43
|
+
- bare alias when host matches a `~/.ssh/config` Host entry
|
|
44
|
+
(so ssh_config's User + HostName + IdentityFile resolution
|
|
45
|
+
works as designed; passing user@alias would override the
|
|
46
|
+
config's User directive and confuse the alias case). -->
|
|
47
|
+
<string>{{SSH_TARGET}}</string>
|
|
48
|
+
</array>
|
|
49
|
+
|
|
50
|
+
<key>RunAtLoad</key>
|
|
51
|
+
<true/>
|
|
52
|
+
<key>KeepAlive</key>
|
|
53
|
+
<true/>
|
|
54
|
+
|
|
55
|
+
<key>StandardOutPath</key>
|
|
56
|
+
<string>{{HOME}}/Library/Logs/patchwork-tunnel.log</string>
|
|
57
|
+
<key>StandardErrorPath</key>
|
|
58
|
+
<string>{{HOME}}/Library/Logs/patchwork-tunnel.log</string>
|
|
59
|
+
|
|
60
|
+
<key>EnvironmentVariables</key>
|
|
61
|
+
<dict>
|
|
62
|
+
<key>PATH</key>
|
|
63
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
64
|
+
<key>HOME</key>
|
|
65
|
+
<string>{{HOME}}</string>
|
|
66
|
+
<!-- AUTOSSH_GATETIME=0: don't require a "stable" connection time
|
|
67
|
+
before counting reconnect attempts. Default 30 s makes startup
|
|
68
|
+
after a long sleep slower than necessary. -->
|
|
69
|
+
<key>AUTOSSH_GATETIME</key>
|
|
70
|
+
<string>0</string>
|
|
71
|
+
</dict>
|
|
72
|
+
|
|
73
|
+
<key>ThrottleInterval</key>
|
|
74
|
+
<integer>10</integer>
|
|
75
|
+
</dict>
|
|
76
|
+
</plist>
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install-mac-bridge.sh — make the bridge + reverse tunnel persistent on macOS
|
|
3
|
+
# via launchd LaunchAgents (auto-start at login, auto-restart on crash,
|
|
4
|
+
# auto-reconnect on network change via autossh).
|
|
5
|
+
#
|
|
6
|
+
# Usage (interactive):
|
|
7
|
+
# bash deploy/macos/install-mac-bridge.sh
|
|
8
|
+
#
|
|
9
|
+
# Usage (env-driven, idempotent re-install):
|
|
10
|
+
# VPS_HOST=185.167.97.141 VPS_USER=root BRIDGE_PORT=63906 \
|
|
11
|
+
# VPS_PORT=3285 BRIDGE_TOKEN="$(uuidgen | tr '[:upper:]' '[:lower:]')" \
|
|
12
|
+
# bash deploy/macos/install-mac-bridge.sh
|
|
13
|
+
#
|
|
14
|
+
# Tip: prefer the VPS IP (or a `~/.ssh/config` Host alias) over the
|
|
15
|
+
# public domain. The domain may resolve via DNS that drifts, putting
|
|
16
|
+
# you on a different machine after a redeploy and breaking host-key
|
|
17
|
+
# verification. The IP is whatever your deploy script targets — a
|
|
18
|
+
# stable identity across rebuilds.
|
|
19
|
+
#
|
|
20
|
+
# Using a ~/.ssh/config alias (cleanest):
|
|
21
|
+
# # in ~/.ssh/config:
|
|
22
|
+
# # Host pw-bridge
|
|
23
|
+
# # HostName 185.167.97.141
|
|
24
|
+
# # User wesh
|
|
25
|
+
# # IdentityFile ~/.ssh/id_ed25519
|
|
26
|
+
# VPS_HOST=pw-bridge VPS_USER=wesh \
|
|
27
|
+
# bash deploy/macos/install-mac-bridge.sh
|
|
28
|
+
# # → tunnel.plist uses `pw-bridge` as the SSH target (User from
|
|
29
|
+
# # ssh_config wins over the plist's user@host form when an alias
|
|
30
|
+
# # exists; we still set both so the resolution is explicit).
|
|
31
|
+
#
|
|
32
|
+
# After install:
|
|
33
|
+
# tail -f ~/Library/Logs/patchwork-bridge.log
|
|
34
|
+
# tail -f ~/Library/Logs/patchwork-tunnel.log
|
|
35
|
+
#
|
|
36
|
+
# Update VPS .env.local PATCHWORK_BRIDGE_TOKEN to match the printed token,
|
|
37
|
+
# then `pm2 restart patchwork-dashboard` on the VPS.
|
|
38
|
+
#
|
|
39
|
+
# Uninstall: bash deploy/macos/uninstall-mac-bridge.sh
|
|
40
|
+
|
|
41
|
+
set -euo pipefail
|
|
42
|
+
|
|
43
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
44
|
+
# Sanity checks
|
|
45
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
if [[ "$OSTYPE" != "darwin"* ]]; then
|
|
48
|
+
echo "This script is for macOS (launchd). Detected: $OSTYPE" >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
if ! command -v claude-ide-bridge >/dev/null 2>&1; then
|
|
53
|
+
echo "claude-ide-bridge not found on PATH." >&2
|
|
54
|
+
echo "Install with: npm install -g patchwork-os" >&2
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Real-path resolution: a symlinked global install (npm link) breaks the
|
|
59
|
+
# LaunchAgent because launchd's sandbox follows the symlink target into
|
|
60
|
+
# ~/Documents and EPERMs on file IO. Recommend a real install.
|
|
61
|
+
BRIDGE_REAL="$(readlink -f "$(command -v claude-ide-bridge)" 2>/dev/null || \
|
|
62
|
+
python3 -c "import os,sys; print(os.path.realpath('$(command -v claude-ide-bridge)'))")"
|
|
63
|
+
case "$BRIDGE_REAL" in
|
|
64
|
+
*Documents*|*Desktop*|*Downloads*)
|
|
65
|
+
echo "⚠️ claude-ide-bridge is symlinked into a sandboxed user-data folder:"
|
|
66
|
+
echo " $BRIDGE_REAL"
|
|
67
|
+
echo " LaunchAgent startup will fail with EPERM under the macOS sandbox."
|
|
68
|
+
echo " Fix: cd to the patchwork-os repo, run \`npm pack && npm install -g patchwork-os-*.tgz\`"
|
|
69
|
+
echo " Or: \`npm install -g patchwork-os\` from the public registry."
|
|
70
|
+
echo ""
|
|
71
|
+
read -p " Continue anyway? [y/N] " -r yn
|
|
72
|
+
[[ "$yn" =~ ^[Yy]$ ]] || exit 1
|
|
73
|
+
;;
|
|
74
|
+
esac
|
|
75
|
+
|
|
76
|
+
if ! command -v autossh >/dev/null 2>&1; then
|
|
77
|
+
echo "autossh not found. Installing via Homebrew…"
|
|
78
|
+
if ! command -v brew >/dev/null 2>&1; then
|
|
79
|
+
echo "Homebrew not found. Install from https://brew.sh and re-run." >&2
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
82
|
+
brew install autossh
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
86
|
+
# Gather config (env-driven with interactive fallback)
|
|
87
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
VPS_HOST="${VPS_HOST:-}"
|
|
90
|
+
VPS_USER="${VPS_USER:-$USER}"
|
|
91
|
+
BRIDGE_PORT="${BRIDGE_PORT:-63906}"
|
|
92
|
+
VPS_PORT="${VPS_PORT:-3285}"
|
|
93
|
+
BRIDGE_TOKEN="${BRIDGE_TOKEN:-}"
|
|
94
|
+
WORKSPACE="${WORKSPACE:-$PWD}"
|
|
95
|
+
SSH_KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519}"
|
|
96
|
+
[[ -f "$SSH_KEY" ]] || SSH_KEY="$HOME/.ssh/id_rsa"
|
|
97
|
+
|
|
98
|
+
if [[ -z "$VPS_HOST" ]]; then
|
|
99
|
+
cat <<'PROMPT'
|
|
100
|
+
|
|
101
|
+
VPS target — three choices, in increasing order of stability:
|
|
102
|
+
|
|
103
|
+
(1) Public domain e.g. bridge.your.tld
|
|
104
|
+
DNS-resolved each connection. Drifts during redeploys → host-key churn.
|
|
105
|
+
|
|
106
|
+
(2) VPS IP e.g. 185.167.97.141
|
|
107
|
+
Stable per VPS. Doesn't survive a VPS rebuild but doesn't drift between.
|
|
108
|
+
|
|
109
|
+
(3) ~/.ssh/config alias e.g. pw-bridge
|
|
110
|
+
Most stable. Pins IP + identity in one place; the rest of the
|
|
111
|
+
stack (this script, deploy.sh, manual ssh) all use the alias.
|
|
112
|
+
Update one ssh_config entry on a rebuild, everything follows.
|
|
113
|
+
|
|
114
|
+
PROMPT
|
|
115
|
+
read -p "VPS host (IP or ssh alias preferred): " VPS_HOST
|
|
116
|
+
fi
|
|
117
|
+
if [[ -z "$VPS_HOST" ]]; then
|
|
118
|
+
echo "VPS host is required." >&2
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# When VPS_HOST is a ~/.ssh/config alias, ssh resolves both User and
|
|
123
|
+
# HostName from the config, so VPS_USER becomes redundant. The plist
|
|
124
|
+
# still passes user@host to be explicit (and to support the IP-direct
|
|
125
|
+
# case where ssh_config has nothing). If `ssh -G $VPS_HOST` reports a
|
|
126
|
+
# resolved hostname different from VPS_HOST, treat that as confirmation
|
|
127
|
+
# the value is an alias.
|
|
128
|
+
if ssh -G "$VPS_HOST" 2>/dev/null | awk '$1 == "hostname"' | grep -qv "^hostname $VPS_HOST$"; then
|
|
129
|
+
echo "Detected '$VPS_HOST' is a ~/.ssh/config alias — host + identity resolved from there."
|
|
130
|
+
IS_SSH_ALIAS=1
|
|
131
|
+
else
|
|
132
|
+
IS_SSH_ALIAS=0
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if [[ -z "$BRIDGE_TOKEN" ]]; then
|
|
136
|
+
if command -v uuidgen >/dev/null 2>&1; then
|
|
137
|
+
BRIDGE_TOKEN="$(uuidgen)"
|
|
138
|
+
else
|
|
139
|
+
BRIDGE_TOKEN="$(python3 -c 'import uuid; print(uuid.uuid4())')"
|
|
140
|
+
fi
|
|
141
|
+
echo "Generated bridge token: $BRIDGE_TOKEN"
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
if [[ ! -f "$SSH_KEY" ]]; then
|
|
145
|
+
echo "SSH key not found: $SSH_KEY" >&2
|
|
146
|
+
echo "Either set SSH_KEY=... or generate one with \`ssh-keygen -t ed25519\`." >&2
|
|
147
|
+
exit 1
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
151
|
+
# Render templates
|
|
152
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
155
|
+
LAUNCH_AGENTS="$HOME/Library/LaunchAgents"
|
|
156
|
+
LOGS="$HOME/Library/Logs"
|
|
157
|
+
BRIDGE_BIN="$(command -v claude-ide-bridge)"
|
|
158
|
+
AUTOSSH_BIN="$(command -v autossh)"
|
|
159
|
+
|
|
160
|
+
mkdir -p "$LAUNCH_AGENTS" "$LOGS"
|
|
161
|
+
|
|
162
|
+
# SSH target string: bare alias when VPS_HOST is a `~/.ssh/config` Host
|
|
163
|
+
# entry, "user@host" form otherwise. The alias case lets ssh_config own
|
|
164
|
+
# user + identity + hostname resolution, which is the most stable setup
|
|
165
|
+
# (one place to update on a VPS rebuild).
|
|
166
|
+
if [ "$IS_SSH_ALIAS" = "1" ]; then
|
|
167
|
+
SSH_TARGET="$VPS_HOST"
|
|
168
|
+
echo "SSH target: $SSH_TARGET (resolved via ~/.ssh/config)"
|
|
169
|
+
else
|
|
170
|
+
SSH_TARGET="$VPS_USER@$VPS_HOST"
|
|
171
|
+
echo "SSH target: $SSH_TARGET"
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
render() {
|
|
175
|
+
local tmpl="$1" out="$2"
|
|
176
|
+
sed \
|
|
177
|
+
-e "s|{{BRIDGE_BIN}}|$BRIDGE_BIN|g" \
|
|
178
|
+
-e "s|{{AUTOSSH_BIN}}|$AUTOSSH_BIN|g" \
|
|
179
|
+
-e "s|{{BRIDGE_PORT}}|$BRIDGE_PORT|g" \
|
|
180
|
+
-e "s|{{VPS_PORT}}|$VPS_PORT|g" \
|
|
181
|
+
-e "s|{{BRIDGE_TOKEN}}|$BRIDGE_TOKEN|g" \
|
|
182
|
+
-e "s|{{VPS_HOST}}|$VPS_HOST|g" \
|
|
183
|
+
-e "s|{{SSH_USER}}|$VPS_USER|g" \
|
|
184
|
+
-e "s|{{SSH_TARGET}}|$SSH_TARGET|g" \
|
|
185
|
+
-e "s|{{SSH_KEY}}|$SSH_KEY|g" \
|
|
186
|
+
-e "s|{{WORKSPACE}}|$WORKSPACE|g" \
|
|
187
|
+
-e "s|{{HOME}}|$HOME|g" \
|
|
188
|
+
"$tmpl" >"$out"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
BRIDGE_PLIST="$LAUNCH_AGENTS/com.patchwork.bridge.plist"
|
|
192
|
+
TUNNEL_PLIST="$LAUNCH_AGENTS/com.patchwork.tunnel.plist"
|
|
193
|
+
|
|
194
|
+
render "$SCRIPT_DIR/com.patchwork.bridge.plist.template" "$BRIDGE_PLIST"
|
|
195
|
+
render "$SCRIPT_DIR/com.patchwork.tunnel.plist.template" "$TUNNEL_PLIST"
|
|
196
|
+
|
|
197
|
+
# Tighten permissions — these contain the bridge token.
|
|
198
|
+
chmod 600 "$BRIDGE_PLIST" "$TUNNEL_PLIST"
|
|
199
|
+
|
|
200
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
201
|
+
# (Re)load
|
|
202
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
# bootout removes the old service if present (idempotent re-install).
|
|
205
|
+
# Errors are non-fatal — first run won't have anything to bootout.
|
|
206
|
+
launchctl bootout "gui/$UID" "$BRIDGE_PLIST" 2>/dev/null || true
|
|
207
|
+
launchctl bootout "gui/$UID" "$TUNNEL_PLIST" 2>/dev/null || true
|
|
208
|
+
|
|
209
|
+
launchctl bootstrap "gui/$UID" "$BRIDGE_PLIST"
|
|
210
|
+
launchctl bootstrap "gui/$UID" "$TUNNEL_PLIST"
|
|
211
|
+
|
|
212
|
+
launchctl enable "gui/$UID/com.patchwork.bridge"
|
|
213
|
+
launchctl enable "gui/$UID/com.patchwork.tunnel"
|
|
214
|
+
|
|
215
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
216
|
+
# Status
|
|
217
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
cat <<INFO
|
|
220
|
+
|
|
221
|
+
────────────────────────────────────────────────────────────────────────
|
|
222
|
+
Installed.
|
|
223
|
+
|
|
224
|
+
Bridge: 127.0.0.1:$BRIDGE_PORT
|
|
225
|
+
Tunnel: -> $SSH_TARGET:$VPS_PORT
|
|
226
|
+
Token: $BRIDGE_TOKEN
|
|
227
|
+
|
|
228
|
+
Logs:
|
|
229
|
+
~/Library/Logs/patchwork-bridge.log
|
|
230
|
+
~/Library/Logs/patchwork-tunnel.log
|
|
231
|
+
|
|
232
|
+
Status:
|
|
233
|
+
launchctl print gui/$UID/com.patchwork.bridge
|
|
234
|
+
launchctl print gui/$UID/com.patchwork.tunnel
|
|
235
|
+
|
|
236
|
+
Update the VPS dashboard's PATCHWORK_BRIDGE_TOKEN to match the token
|
|
237
|
+
above, then \`pm2 restart patchwork-dashboard\` on the VPS so the new
|
|
238
|
+
token takes effect:
|
|
239
|
+
|
|
240
|
+
ssh $SSH_TARGET 'sed -i "s|^PATCHWORK_BRIDGE_TOKEN=.*|PATCHWORK_BRIDGE_TOKEN=$BRIDGE_TOKEN|" /opt/patchwork-dashboard/.env.local && pm2 restart patchwork-dashboard'
|
|
241
|
+
|
|
242
|
+
Uninstall: bash deploy/macos/uninstall-mac-bridge.sh
|
|
243
|
+
────────────────────────────────────────────────────────────────────────
|
|
244
|
+
INFO
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# uninstall-mac-bridge.sh — stop + remove the patchwork bridge and tunnel
|
|
3
|
+
# LaunchAgents installed by install-mac-bridge.sh.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
if [[ "$OSTYPE" != "darwin"* ]]; then
|
|
8
|
+
echo "macOS only." >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
BRIDGE_PLIST="$HOME/Library/LaunchAgents/com.patchwork.bridge.plist"
|
|
13
|
+
TUNNEL_PLIST="$HOME/Library/LaunchAgents/com.patchwork.tunnel.plist"
|
|
14
|
+
|
|
15
|
+
# bootout: stop + remove the service registration. `|| true` so the
|
|
16
|
+
# script doesn't fail when one of them isn't installed.
|
|
17
|
+
launchctl bootout "gui/$UID" "$BRIDGE_PLIST" 2>/dev/null || true
|
|
18
|
+
launchctl bootout "gui/$UID" "$TUNNEL_PLIST" 2>/dev/null || true
|
|
19
|
+
|
|
20
|
+
rm -f "$BRIDGE_PLIST" "$TUNNEL_PLIST"
|
|
21
|
+
|
|
22
|
+
echo "Uninstalled. Logs preserved at ~/Library/Logs/patchwork-{bridge,tunnel}.log"
|
package/dist/approvalHttp.d.ts
CHANGED
|
@@ -39,6 +39,20 @@ export interface ApprovalHttpDeps {
|
|
|
39
39
|
pushServiceToken?: string;
|
|
40
40
|
/** Public base URL of this bridge (e.g. https://mybridge.example.com). Embedded in push payload as callback base. */
|
|
41
41
|
pushServiceBaseUrl?: string;
|
|
42
|
+
/**
|
|
43
|
+
* ntfy.sh topic for direct phone-path approvals via action buttons. When set
|
|
44
|
+
* AND `pushServiceBaseUrl` is set (so the action URLs can reach the bridge),
|
|
45
|
+
* each queued approval is published to the topic with Approve / Reject HTTP
|
|
46
|
+
* action buttons that POST back to the bridge with the single-use approval
|
|
47
|
+
* token. Independent of `pushServiceUrl` — ntfy can be the sole phone path
|
|
48
|
+
* (no FCM/APNS relay needed) or run alongside it.
|
|
49
|
+
*/
|
|
50
|
+
ntfyTopic?: string;
|
|
51
|
+
/**
|
|
52
|
+
* ntfy server. Defaults to `https://ntfy.sh`. Set to a self-hosted instance
|
|
53
|
+
* (e.g. `https://ntfy.your-domain.tld`) for auth/private-topic deployments.
|
|
54
|
+
*/
|
|
55
|
+
ntfyServer?: string;
|
|
42
56
|
/**
|
|
43
57
|
* Optional ActivityLog used to compute passive risk personalization
|
|
44
58
|
* signals (`src/approvalSignals.ts`). When omitted, personalSignals are
|