Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
67cbac0
Keep Desktop login valid for its backend lifetime
askac Sep 15, 2026
9dec47b
Prepare Desktop 0.5.2 with Core 2.13.1-dev
askac Sep 15, 2026
6e2fa47
Highlight Agent Info copy failures
askac Sep 15, 2026
b936070
Clarify Files copy progress and download results
askac Sep 15, 2026
3e90e5e
Add user-managed SSH TCP tunnels
askac Sep 15, 2026
f4c32c3
Fix SSH session editing and add browser popout fallback
askac Sep 15, 2026
f601889
Edit Direct SSH settings inline with shared node fields
askac Sep 15, 2026
ef672e7
Preview terminal color schemes in Settings
askac Sep 16, 2026
8d84f87
Advertise terminal RGB capabilities without changing TERM
askac Sep 16, 2026
690fad2
Distinguish direct and routed SSH connection panes
askac Sep 16, 2026
ed2bcdb
Add one-click Agent Mint action
askac Sep 17, 2026
9ce1c87
Keep Files actions visible in short windows
askac Sep 19, 2026
7a79107
Add cross-tab Agent transfer queue
askac Sep 19, 2026
ed7e4df
Preserve agent targets and bound capture waits
askac Sep 19, 2026
cf9958d
Define Agent terminology and translate review copy
askac Sep 19, 2026
b522feb
Add table-driven Agent language preview
askac Sep 19, 2026
fe5e07f
Localize browser access and Agent approvals
askac Sep 19, 2026
0dab589
Localize connection forms and SSH login
askac Sep 19, 2026
ee9e855
Localize SSH route and profile editors
askac Sep 19, 2026
ceb6258
Localize settings transfer and report partial imports
askac Sep 19, 2026
69e80a3
Clarify and localize settings preference actions
askac Sep 19, 2026
f8695ca
Clarify and localize SSH tunnel controls
askac Sep 19, 2026
1fd4d52
Clarify Agent Tunnel renewal and verification copy
askac Sep 19, 2026
7a3c92c
Localize General and Appearance preferences
askac Sep 19, 2026
77514dd
Clarify and localize Server runtime settings
askac Sep 19, 2026
6d42af8
Complete browser localization and action feedback
askac Sep 19, 2026
2f806ca
Plan Desktop copy review and localization
askac Sep 20, 2026
2c304d7
Clarify rejected and uncertain toolbar actions
askac Sep 20, 2026
e1220f3
Add Desktop language settings and bilingual toolbar
askac Sep 20, 2026
e2b4179
Localize Desktop browser access and diagnostics
askac Sep 20, 2026
27fdebb
Keep Desktop open when recording save fails
askac Sep 20, 2026
b83f5f1
Localize Desktop setup and Core recovery
askac Sep 20, 2026
3886155
Localize port, download, installer and paste notices
askac Sep 20, 2026
b8fae88
Verify Windows localization and installer payload
askac Sep 20, 2026
98e490d
Simplify Agent connection and restore Desktop copy
askac Sep 20, 2026
ca5c319
Sync Desktop language with Core Settings
askac Sep 21, 2026
5fe4b52
Draft eight-language interface translations
askac Sep 21, 2026
b6bd3a9
Make Agent Info URL the primary copy action
askac Sep 22, 2026
a51cf56
Show startup feedback and restore window state
askac Sep 23, 2026
63cb323
Add Windows network routing for WSL SSH
askac Sep 23, 2026
2117c8d
Collapse Windows SSH networking under Advanced
askac Sep 23, 2026
0c8e7f4
Release Desktop 0.5.3 with Core 2.14.0
askac Sep 23, 2026
5939a1f
Update downloads for Core 2.14.0 and Desktop 0.5.3
askac Sep 24, 2026
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
7 changes: 7 additions & 0 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ jobs:

- name: Run headless smoke tests
run: tools/.venv_linux/bin/python scripts/run_smoke_tests.py

- uses: actions/setup-node@v4
with:
node-version: '24'

- name: Check UI translation lookup
run: node tests/ui_i18n_smoke.cjs
225 changes: 183 additions & 42 deletions README.md

Large diffs are not rendered by default.

35 changes: 16 additions & 19 deletions agent_tunnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import base64
import hashlib
import secrets
import select
import re
import shlex
import socket
Expand All @@ -14,14 +13,14 @@

from flask import Flask, jsonify, request
from werkzeug.serving import WSGIRequestHandler, make_server
from ssh_forwarding import forwarding_for, relay_tcp


TUNNEL_IO_TIMEOUT = 30
TUNNEL_COMMAND_TIMEOUT = 45
TUNNEL_MAX_CHANNELS = 16
TUNNEL_MAX_REQUEST_BYTES = 1024 * 1024
TUNNEL_MONITOR_INTERVAL = 1
TUNNEL_FORWARD_POLL_SECONDS = 1
TUNNEL_REMOTE_PYTHON = 'python3'
TUNNEL_HELPERS = ('cli', 'input', 'jsonl', 'repl', 'scp', 'shcmd', 'type', 'rsfile', 'mcp', 'tunnel_runtime')
TUNNEL_SKILLS = ('standterm-external-agent-skill', 'standterm-file-transfer', 'standterm-privileged-hitl')
Expand All @@ -46,6 +45,7 @@ def __init__(self, bridge, sid, app_dir, *, build_info, dispatch, revoke):
self.revoke = revoke
self.id = 'tun_' + secrets.token_urlsafe(18)
self.transport = bridge.ssh.get_transport()
self.forwarding = forwarding_for(self.transport) if self.transport else None
self.runtime = None
self.port = None
self.active = False
Expand Down Expand Up @@ -228,27 +228,24 @@ def command():
def _accept(self, channel, origin, destination):
with self.lock:
if (not self.active or destination != ('127.0.0.1', self.port)
or len(self._channels) >= TUNNEL_MAX_CHANNELS):
or len(self._channels) >= TUNNEL_MAX_CHANNELS
or not self.forwarding.connections.acquire(blocking=False)):
channel.close()
return
self._channels.add(channel)
threading.Thread(target=self._forward, args=(channel,), daemon=True).start()
try:
threading.Thread(target=self._forward, args=(channel,), daemon=True).start()
except Exception:
with self.lock:
self._channels.discard(channel)
self.forwarding.connections.release()
channel.close()

def _forward(self, channel):
local = None
try:
local = socket.create_connection(('127.0.0.1', self._server.server_port), TUNNEL_IO_TIMEOUT)
local.settimeout(TUNNEL_IO_TIMEOUT)
channel.settimeout(TUNNEL_IO_TIMEOUT)
while self.active:
readable, _, _ = select.select([channel, local], [], [], TUNNEL_FORWARD_POLL_SECONDS)
if not readable:
continue
for source in readable:
data = source.recv(65536)
if not data:
return
(local if source is channel else channel).sendall(data)
relay_tcp(channel, local, lambda: not self.active)
except (OSError, EOFError):
pass
finally:
Expand All @@ -257,6 +254,7 @@ def _forward(self, channel):
local.close()
with self.lock:
self._channels.discard(channel)
self.forwarding.connections.release()

def start(self):
with self.setup_lock:
Expand Down Expand Up @@ -312,9 +310,8 @@ def _monitor(self):
return

def _cancel_forward(self):
if self.port and self.transport.is_active():
# A cancel acknowledgement is not needed to fence access locally.
self.transport.global_request('cancel-tcpip-forward', ('127.0.0.1', self.port), wait=False)
if self.port:
self.forwarding.cancel_remote(self.port, self)

def _request_forward(self):
completed = threading.Event()
Expand All @@ -323,7 +320,7 @@ def _request_forward(self):

def run():
try:
self.port = self.transport.request_port_forward('127.0.0.1', 0, handler=self._accept)
self.port = self.forwarding.request_remote(0, self, self._accept, self._closed.is_set)
if self._closed.is_set():
self._cancel_forward()
except Exception as exc:
Expand Down
Loading
Loading