Skip to content

Releases: obutora/stagent

stagent v0.14.0

Choose a tag to compare

@obutora obutora released this 14 Oct 14:28

Added

  • Tasks: a task can be tied to a name instead of an Issue or PR (kind: name, Task.name): a worktree on a branch of that name (or the app's)
    made from the base without gh, with the same copied files, shared
    directories, setup and 片付け as an issue's. Its key is the checkout
    and the name (an issue's number never matches it); it is never
    in_place. task.create and task.register gain name;
    task.create refuses an invalid name, a missing worktree_folder and
    a place that is a non-empty directory (bad_request). task.preview
    gains name? and branch? and answers name_problem (invalid,
    task, folder, branch or no_folder) while the app's name is
    typed. A push about a named task's session puts its name where an
    issue's #N goes (#877).
  • config.json gains worktree_folder, the worktree folder (worktree
    の置き場): named tasks go in <folder>/<checkout name>/<name>, and
    with it set Issue and PR tasks in <folder>/<checkout name>/<number>-<slug> instead of next to the checkout. config.set
    expands a leading ~ with the host's home and refuses a path that is
    not absolute (#877).

Fixed

  • Host screen, Android: on Linux without $XDG_RUNTIME_DIR (an SSH
    session in a container without logind, su), the emulator's discovery
    file is also read where the emulator falls back to when /run/user/<uid>
    does not exist: avd/running in its user directory (~/.android, or
    $ANDROID_EMULATOR_HOME, $ANDROID_PREFS_ROOT, $ANDROID_SDK_HOME as
    the emulator takes them). Such an emulator was listed no_grpc and
    host_screen.start answered unavailable (#705).

stagent 0.14.0 — server binary for SSH Term. Built from this tag (f4509af) with go1.26.1 via scripts/release.sh (reproducible: CGO_ENABLED=0, -trimpath, no VCS stamp, empty build id); verify downloads against SHA256SUMS.

stagent v0.13.0

Choose a tag to compare

@obutora obutora released this 12 Oct 01:52

Added

  • Host screen: an iOS simulator takes text of any script, Japanese
    included (it took ASCII only and answered other text unsupported;
    #649, #665). The text is put on the simulator's clipboard (xcrun simctl pbcopy) and pasted by what the app in front may do, read
    (never written) from the simulator's TCC.db: an app allowed to paste
    from other apps gets ⌘V, and keys (the Return of enter, the next
    text) wait 3 s after it, while iOS drops them under its "pasted from"
    banner; otherwise ASCII is typed after the 英数 key (LANG2), and other
    text is pasted with the edit menu's Paste, opened by tapping the user's
    cursor (text gains x?, y?). An app that never asked first gets a
    ⌘V iOS refuses, which shows its 「ほかのアプリからペースト」 in Settings.
    host_screen.input answers such a text {via: "cmd_v" | "edit_menu" | "keys", app?}; without the edit menu's Paste it fails with the new
    error paste_by_hand, the text left on the clipboard. The companion
    gains accessibility_info and list_apps in stagent's subset of
    idb.proto.

Fixed

  • Host screen, Android: the discovery file an emulator leaves behind
    when it stops (avd/running/pid_<pid>.ini) is no longer read once its
    <pid> is no running process (signal 0 on Linux and macOS, the
    process's handle on Windows; the file is left as it is). An emulator
    started with -port only on the serial of such a file was listed
    connectable: true with the file's gRPC port, and host_screen.start
    answered {}, then gone for good; it is now no_grpc. start of
    an emulator whose gRPC does not answer while adb devices lists its
    serial is unavailable (no_grpc) instead of {} and gone; one
    adb does not list still waits for its AVD to run again (#679).

stagent 0.13.0 — server binary for SSH Term. Built from this tag (c177d9b) with go1.27.1 via scripts/release.sh (reproducible: CGO_ENABLED=0, -trimpath, no VCS stamp, empty build id); verify downloads against SHA256SUMS.

stagent v0.12.0

Choose a tag to compare

@obutora obutora released this 11 Oct 04:27

Added

  • host_screen.list: on a host where stagent runs in Windows' WSL, the
    browser and android sections have note: {code: "wsl", message}
    (Section<T> gains note?), since the browsers and emulators on the
    Windows side are not visible from WSL (#675).
  • Host screen: an Android emulator or iOS simulator that stops no longer
    ends the stream. After state: "gone" stagent looks for it every 3
    seconds and, once it runs again, connects anew and sends state: "live" and frames in the same tab: an emulator by its AVD on any
    serial (Target has avd; grpc.port and grpc.token read again),
    a simulator by its udid (its companion started again). start of
    one that is not running answers {} and sends gone, then waits the
    same way; so does an iOS start while simctl fails to list (a
    timeout, say), which was not_found. Only no Xcode is still
    not_found (#662).
  • Host screen: host_screen.pause and host_screen.resume (no params,
    {}) stop and restart capturing while the app does not show the tab
    (another tab, the app in the background, the screen off; #634).
    pause drops the frame kept and stops the target's capture: a
    browser's Page.stopScreencast (its WebSocket stays open), an Android
    emulator's streamScreenshot cut, an iOS simulator's MJPEG video
    stream closed (the companion kept, asked describe every 5/3 s so it
    does not shut down idle). resume sends one full frame at once, then
    streams again. Like ack, both are answered at once, never behind a
    slow input; without a stream → bad_request; pause while paused
    and resume while not paused → {}. Acks are answered while paused
    (#651).

Changed

  • host_screen.input on an Android emulator pastes every text, ASCII
    too (gRPC setClipboard, then KEYCODE_PASTE), instead of adb shell input text, which a Japanese Gboard typed as romaji to kana ("wifi
    test" became "ウィフぃてst"). enter alone sends only KEYCODE_ENTER
    and leaves the clipboard alone (#672).
  • Host screen: an Android emulator's image with the same pixels as the
    last one is dropped before JPEG encoding, so a still display sends no
    frame (the emulator sends about 20 a second); an iOS simulator's MJPEG
    is quality 0.5 (was 0.7) at scale 0.5 and fps 0 (#662).
  • Host screen: at most 10 frames a second are sent. A frame captured
    within 100 ms of the last one sent waits in the place that keeps the
    newest frame (the one used while two frames wait for their ack), and
    is sent once 100 ms passed (#651).

Fixed

  • Host screen, Android: an emulator-<port> that adb devices shows
    offline without a discovery file (any process listening on an odd
    port of 5555–5585) is no longer listed as a no_grpc emulator, nor
    asked emu avd name (which held host_screen.list up ~2 s);
    host_screen.start finds no emulator there. Booting emulators (offline
    with a file) and ones without a file in the device state are still
    listed (#673).
  • host_screen.list lists browsers on Linux (WSL too): Chromium writes
    its command line over its arguments as one space-joined string, so no
    browser's --remote-debugging-port / --remote-debugging-pipe was
    found and none was listed. Such a command line is now read for its
    executable and switches (split at --; a --user-data-dir with
    spaces is found, child processes with --type= are still left out);
    NUL-separated command lines read as before (#671).

stagent 0.12.0 — server binary for SSH Term. Built from this tag (0eb7f98) with go1.27.1 via scripts/release.sh (reproducible: CGO_ENABLED=0, -trimpath, no VCS stamp, empty build id); verify downloads against SHA256SUMS.

stagent v0.11.0

Choose a tag to compare

@obutora obutora released this 11 Oct 01:28

Added

  • Host screen (ホストの画面, ADR 0008): host_screen.list,
    host_screen.start, host_screen.ack, host_screen.stop and the
    notifications host_screen.frame and host_screen.state show a page of
    a browser an agent drives in an app tab, over the tab's own bridge. The
    browsers listed are the user's Chromium-family browsers started with
    --remote-debugging-port (0 read from DevToolsActivePort), each
    port checked with /json/version, with their page targets and the tool
    told by the profile path. A page is shown with one
    Page.captureScreenshot and then Page.startScreencast (JPEG, quality
    70); at most two frames wait for the app's ack, of the frames taken
    meanwhile only the newest is kept, and a stalled app gets the
    screencast paused and resumed with a full frame. Served to bridges
    under a coding agent too (#640).
  • host_screen.list: a browser started with --remote-debugging-pipe
    only is listed with connectable: false and reason: "pipe_only"
    (with its tool), and cdp_ports names ports on 127.0.0.1 by hand: each
    that answers /json/version as CDP is listed with manual: true, in
    one item with a browser found on the same port; a port outside 1–65535
    is bad_request (#642).
  • Host screen: Android emulators. host_screen.list has an android
    section: the emulators of adb devices -l (never a physical device),
    matched by serial with the files they advertise their gRPC port in
    (avd/running/pid_<pid>.ini), with the AVD name; one without gRPC
    (started with -port only) is connectable: false, reason: "no_grpc", and a host without adb gets missing: {code: "adb_not_found"}. host_screen.start {target: {kind: "android", serial}} shows the emulator through its gRPC streamScreenshot
    (Bearer grpc.token, RGB888 at half resolution, encoded to JPEG
    quality 70); width and height are the display's px and swap when
    it turns, and a stopped emulator sends state: "gone" (#643).
  • Host screen of an iOS simulator (macOS only, capability
    host_screen_ios): host_screen.list has an ios section with the
    booted iOS simulators (xcrun simctl; missing xcode_not_found
    without Xcode, simctl_failed with simctl's error as the message when
    it fails or runs out of time, a simulator companion_not_found without
    idb_companion), and host_screen.start {target: {kind: "ios", udid}}
    shows one through an idb_companion started for the stream on a private
    Unix socket (no TCP port): its MJPEG video_stream (half resolution,
    quality 0.7, redrawn screens only), sized in points. The companion is
    stopped with the stream (stop, another start, the bridge exiting;
    --idle-shutdown-time should stagent die), and a simulator shut down
    sends state: "gone". Other hosts answer unsupported (#645).
  • host_screen.input operates the page shown: tap, drag, scroll,
    text and navigate (back, forward, reload, url), points as
    fractions of the frame turned into CSS px with the screencast's
    metadata (deviceWidth, deviceHeight, offsetTop,
    pageScaleFactor). A browser gets only Input.* (insertText for
    text in any script, an Enter key event for enter), Page.navigate,
    Page.reload and the history's Page.getNavigationHistory and
    Page.navigateToHistoryEntry; key is unsupported. Inputs
    run in order on start's queue while acks are answered at once, so a
    slow input never stops the frames (#641).
  • host_screen.input operates an Android emulator: tap, drag and
    scroll (one swipe from the cursor) through gRPC sendTouch in the
    display's px, turned back with the emulator's rotation; key back,
    home, recents through adb shell input keyevent (works with
    hw.keyboard=no, no gRPC sendKey); text through adb shell input text when printable ASCII, else gRPC setClipboard and
    KEYCODE_PASTE, with KEYCODE_ENTER for enter. navigate is
    unsupported (#644).
  • host_screen.input operates an iOS simulator: tap, drag, scroll
    (one swipe from the cursor), text of ASCII and key: "home", as HID
    events on the stream's idb_companion hid stream, kept open and
    reopened when the companion ends it (no program per input). Points are
    fractions of the frame in pt; text is typed as key presses of a US
    keyboard (Shift for capitals and the upper symbols), enter as Return.
    back, recents, navigate and text with a character other than
    ASCII (Japanese input is pending, #649) are unsupported. A drag cut
    short (the bridge ending) lifts its touch, and stopping the stream lets
    the companion perform what was sent (up to a second) before stopping
    it (#646).

stagent 0.11.0 — server binary for SSH Term. Built from this tag (0208b4f) with go1.27.1 via scripts/release.sh (reproducible: CGO_ENABLED=0, -trimpath, no VCS stamp, empty build id); verify downloads against SHA256SUMS.

stagent v0.10.1

Choose a tag to compare

@obutora obutora released this 10 Oct 01:59

Fixed

  • Windows: a task's shared directories (worktree.sharedDirectories) of a
    source checkout on a network share (a UNC path or a mapped drive) are
    no longer junctions. An administrator could make one there, but the
    share's server resolves it as its own path, so the copy stage said
    linked and the worktree's directory did not open. Such a directory is
    now a directory symbolic link, or copied with the one warning line when
    that cannot be made either (#531).
  • follow: an omp chat showed the conversation of another agent in the
    same directory, the one the PC was just working in, while the terminal
    showed its own: an omp that has not written since it started (omp -r
    after picking a session, omp -c, a session resumed or switched to in
    omp, a new session before its first answer), and on macOS, where
    stagent cannot see open files, any omp once the other agent wrote last.
    The session omp records for the agent's terminal
    (~/.omp/agent/terminal-sessions/) now decides, and a new session not
    written yet shows no conversation instead of a guess.

stagent 0.10.1 — server binary for SSH Term. Built from this tag (5e66294) with go1.27.1 via scripts/release.sh (reproducible: CGO_ENABLED=0, -trimpath, no VCS stamp, empty build id); verify downloads against SHA256SUMS.

stagent v0.10.0

Choose a tag to compare

@obutora obutora released this 09 Oct 13:10

Added

  • github.status {items[{repo, kind, number, branch?}]} (Windows,
    capability github) reports the state gh gives each issue or pull
    request (OPEN, CLOSED, MERGED), and for an issue with branch
    the state of that branch's pull request as pr_state (the answer
    echoing branch), so the app can
    mark tasks whose issue was closed or whose pull request was merged;
    github.activity only lists open ones. Items gh cannot answer are left
    out (#502).
  • Tasks (タスク): an Issue or PR worked on in a worktree of a source
    checkout, or in the checkout itself (in_place). The daemon keeps them
    in state/tasks.json (mode 0600; only the daemon writes it, only
    uninstall --level purge removes it), keyed by the checkout's
    canonical path and the number. task.list answers every task in the
    order they were created, watch gains tasks[], and task.updated
    (the whole Task) and task.removed {id} follow the changes. A ready
    task whose worktree directory is gone or that git worktree list no
    longer lists becomes missing on task.list; a preparing task whose
    prepared session ended or never registered becomes failed with
    error: "interrupted", also after a daemon restart, and a task still
    removing when the daemon starts is ready with that error, keeping
    its stage. A change tasks.json cannot take is not made: the call
    answers internal. After a daemon restart the bridge's restored watch
    sends every task as task.updated and gone ones as task.removed
    (#502).
  • Session.task_id: the daemon ties a session to the worktree task whose
    worktree holds its cwd (also live sessions, when the task is
    registered), and to an in_place task only its prepared session and
    sessions started with the new session.spawn {task_id} (#502).
  • Local IPC for the bridge and stagent task: task.register (atomic by
    the key; a failed task is prepared again, another number's in_place
    task on the checkout is the new error in_use), task.progress {id, stage, error?, session_id?} and task.forget {id}, refused with
    agent_refused to a coding agent's process tree like config.set.
    stop on a task already removing is busy, so only one removal runs;
    stage agent with an error from the prepared session fails a ready
    task. Error codes not_clean and busy are reserved for task.remove
    (#502).
  • Pushes of a session tied to a task are titled <host> · #N <title> on
    ntfy, the generic webhook and chat destinations; a digest of several is
    not (#502).
  • task.create (bridge, spawn queue) creates a task and starts its first
    session: the daemon registers it ({task, session?, existed}: a ready
    task starts nothing, a preparing one answers its live prepared
    session, a failed one is prepared again), the bridge makes the empty
    directory <parent of the checkout>/<checkout name>-<N> and starts the
    session there (in the checkout itself for in_place), so its cwd is
    the task's place from the start. In it the internal stagent task run <id> -- <command…> fetches, makes the branch (an issue's <N>-<slug>
    or the given branch, linked with gh issue develop, made locally
    from base when gh fails; a PR's head branch with gh pr checkout in
    a detached worktree), adds or adopts the worktree, runs orca.yaml's
    scripts.setup (login shell; on Windows a .cmd runner, or Git for
    Windows' bash for a #! script) and then becomes the agent, reporting
    each stage with task.progress and showing everything in the
    terminal. A failing stage leaves the task failed without an agent —
    also when the agent itself cannot be started after stage agent;
    task.create again carries on. An empty command opens a terminal.
    Refused with agent_refused to a coding agent's process tree (#502).
  • A task's prepared session reports the harness of the command stagent task run runs (Session.harness), and a Codex there gets
    --no-daemon like any other (#502).
  • A worktree task gets its files to copy and shared directories in the
    new copy stage of stagent task run, between worktree and setup:
    orca.yaml's worktree.sharedDirectories of the source checkout that
    are gitignored are linked (a symbolic link; on Windows a junction, else
    a directory symbolic link; one inside another listed one goes with it),
    then the gitignored files the checkout's
    .worktreeinclude (Orca's rules: literal paths — a line with *, ?
    or [ is invalid —, at most 1,000, a file
    up to 256 KiB) and the app's patterns (config.json
    repos.<checkout>.worktree_include, git pathspec globs) select are
    copied, up to 2 GiB and 50,000 entries. Tracked, not ignored, missing,
    unmatched, invalid and over-the-limit entries and what lies in a linked
    directory are skipped with a line in the terminal (tracked ones
    silently); a destination that exists is left alone, so a retry copies
    only what is missing. A directory that cannot be linked is one warning
    line and what is listed under it is copied instead; nothing here fails
    the task (#502).
  • task.preview {repo, worktree_include?} (bridge, its own queue,
    allowed for agent descendants) answers what a new worktree of the
    checkout would get, resolved by the same code: copies (at most 500,
    truncated), links, skipped with the reason (tracked,
    not_ignored, missing, no_match, under_link, invalid,
    limit), total_bytes, the checkout's orca.yaml setup and
    orca_error. worktree_include tries patterns that are not saved
    (#502).
  • config.json repos.<checkout>.worktree_include, set one checkout at
    a time with config.set (null removes one); config.set refuses
    with bad_request a key that is not an absolute clean path and a
    pattern that is empty, a negation, absolute or has a .. or .git
    component (#502).
  • task.remove {id, force?, stop_sessions?, skip_archive?, dry_run?}
    (bridge, its own queue) removes a task (片付け). dry_run reports the
    worktree's uncommitted changes as git status --porcelain lines
    (changes[], ignored files and shared-directory links left out),
    the branch's commits on no remote (unpushed) and the task's live
    sessions (sessions[]). Changes need force (not_clean), live
    sessions stop_sessions (busy): they get hangup, then kill after 5 s,
    and sessions bound to the task meanwhile are stopped too. A preparing
    task is busy without stop_sessions; with it its sessions are stopped
    first (it fails, interrupted) and the removal goes on, in_place too.
    Then orca.yaml's scripts.archive of the source checkout runs in the
    worktree — in a session of the source checkout running the internal
    stagent task remove <id>, so its output shows in a terminal — unless
    skip_archive; only the shared directories' links are unlinked, git worktree remove runs, and the branch is deleted when git sees it
    merged or gh says its pull request was merged at the branch's commit
    (a squash merge; a PR task's pull request is looked up by its number,
    so a fork's is found too), otherwise kept and named in kept_branch; remote
    branches are never deleted. Each stage is Task.stage while
    removing; a failing one (archive, git worktree remove) takes the
    task back with its error, keeping that stage (the app offers to skip
    the archive when it is archive), and answers internal; a second
    removal of a task being removed is busy. The reply comes when
    the removal has finished. A missing task is forgotten and its
    worktree pruned, an in_place one only forgotten. Refused with
    agent_refused to a coding agent's process tree (#502).
  • task.forget takes kept_branch? and task.removed carries it, so
    every watcher learns the branch a removal kept (#502).

Fixed

  • A chat message no longer lands on claude's workspace trust prompt
    (Accessing workspace:, shown at startup in a folder not trusted yet).
    Its options have no numbers, so the holder did not see a menu, and the
    message's Enter picked the highlighted No, exit, ending claude. Now
    session.input refuses the message with menu_open while the prompt is
    up, as on Codex's folder trust prompt; the user answers it in the
    terminal, and claude then sends the initial prompt it held. stagent
    writes no trust (#502).
  • session.spawn of a program that ends at once no longer fails with
    holder exited during start when the holder ended with it before the
    bridge reached it: the answer is the session the daemon lists. The same
    race made task.create report a task run that failed at once as a
    start failure, and task.remove an archive that failed at once (#502).
  • Under proot's fake root (proot -0, an Ubuntu guest on Android), the
    chat view and the agent probe find the agents: stagent took its
    own uid from geteuid, which proot fakes, and every process's from
    /proc/<pid>/status, which proot leaves at the kernel's uid, so every
    process counted as another user's. On Linux stagent now reads its own
    uids from /proc/self/status too; elsewhere the uids agree and nothing
    changes (#529).

stagent 0.10.0 — server binary for SSH Term. Built from this tag (abda34b) with go1.27.1 via scripts/release.sh (reproducible: CGO_ENABLED=0, -trimpath, no VCS stamp, empty build id); verify downloads against SHA256SUMS.

stagent v0.9.0

Choose a tag to compare

@obutora obutora released this 09 Oct 12:26

Added

  • The GitHub screen works on native Windows hosts, which have no sh for
    the app's scripts: on Windows the bridge announces github and serves
    github.repos (the git working trees the user worked in last, newest
    first, with the fields of the app's sh script; candidates are the
    working directories of the user's processes under their SSH connections
    and holders, read from the PEB, the sessions' cwd and the latest 50
    conversations' cwd; at most 20 are inspected) and github.activity
    (gh's open pull requests and issues, the branch's pull request, the
    repository's description; gh: missing with package_manager: winget
    or unauthenticated). Both run git and gh on their own queue, so the
    daemon and spawns never wait for them, and answer agent descendants
    too. Linux and macOS bridges neither announce nor serve them. A bare
    PowerShell a holder starts (a shell: true session of a host whose
    sshd runs PowerShell) gets a prompt wrapper that keeps the process's
    working directory at the shell's location, so Set-Location / sl /
    cd.. move the repository the GitHub screen sees; the session keeps
    its command as given. The shell wrapper's PowerShell profile block has
    the same prompt wrapper, for the terminals sshd starts in PowerShell
    (an update rewrites the block on hosts that have it) (#502).

stagent 0.9.0 — server binary for SSH Term. Built from this tag (064c4ab) with go1.27.1 via scripts/release.sh (reproducible: CGO_ENABLED=0, -trimpath, no VCS stamp, empty build id); verify downloads against SHA256SUMS.

stagent v0.8.0

Choose a tag to compare

@obutora obutora released this 08 Oct 01:41

Added

  • Pushes can go to Discord, Slack and Telegram: notify.chat {enabled, url}, a chat destination next to the generic webhook (both can be on
    and both get every push). The one-line url names the service — a
    Discord webhook (https://discord.com/api/webhooks/<id>/<token>,
    discord://<id>/<token>), a Slack Incoming Webhook
    (https://hooks.slack.com/services/…, slack://<a>/<b>/<c>) or a
    Telegram bot and chat (tgram://<bot token>/<chat id>) — and stagent
    builds each service's message itself: the reason's emoji, the
    host-labelled title and the body, as a Discord embed (yellow for a
    warning, no mentions), Slack text (&<> escaped, no unfurling) or
    Telegram plain text (no link preview). config.set refuses any other
    url, Slack Workflow webhooks included, with bad_request. The url
    is a secret: it never shows in last_error (channel chat),
    notify.test errors, stagent doctor or the log. The bridge announces
    push_chat (#471).
  • notify.test {channels?: ["ntfy" | "webhook" | "chat"]} tests only the
    channels named; without channels, every enabled one as before. An
    unknown name is bad_request, named channels that are all off
    not_configured (#471).
  • notify.click_page: the open page (https://sshterm.iru-yo.com/open,
    set by the app) that chat destinations and the generic webhook link to,
    with h=<host_id> and s=<session id> in its fragment
    (<click_page>#h=…&s=…). A push of one session gets an "Open in SSH
    Term" link (in notify.lang): a link button on Discord
    (?with_components=true) and Telegram (inline keyboard), a link at the
    end of the text on Slack; a digest, notify.test and a push of no
    session get none. The generic webhook's JSON gains click_url (the
    host only, #h=…, for a digest and notify.test); its other keys stay.
    No click_page, no link. ntfy's Click still uses notify.click_base.
    config.set refuses a click_page that is not an absolute https URL
    without query and fragment with bad_request (#472).
  • On Discord and Telegram, the chat destination replaces and resolves
    like ntfy: the next message of a session is sent first, then the
    older one deleted (Discord DELETE …/messages/<id>, Telegram
    deleteMessage; one Telegram no longer deletes after 48 hours is
    marked resolved instead), and when the session settles its message is
    edited, not deleted: ☑️ in place of the reason's emoji, "(resolved)"
    after the title in notify.lang (ja (解決済み)), the body as it
    was and the same "Open in SSH Term" button (Discord: grey embed,
    components sent again; Telegram: editMessageText with reply_markup
    sent again). A digest's message is marked resolved once all its
    sessions have settled and is never deleted by a later message of one
    of them. The unresolved messages survive a daemon restart in
    state/chat-messages.json (mode 0600: ids, sent time and the SHA-256
    of notify.chat.url, never the URL or its token); a session gone over
    the restart has its message marked resolved at once. Changing
    notify.chat.url or removing chat forgets the messages of the old
    URL without editing them. A failed delete or edit is never retried nor
    a failed push (last_error): a 404 lets the message go, a 429 or a
    connection error lets it go with one log line. Slack messages are only
    sent, and the generic webhook still gets no clear (#473).
  • A failed push to the chat destination says why: last_error.chat gains
    kind (revoked, unreachable, rate_limited, rejected, server,
    network; Telegram by error_code) and its error adds the service's
    short reason after the status line (404 Not Found: Unknown Webhook (10015); Discord's message and code, Telegram's description,
    Slack's error body; 200 characters at most, dropped whole if it holds
    the URL or a token). A queued push answered 429 with a wait of 5 s or
    less is sent once more after the wait (notify.test is not); only a
    failed resend is recorded. Pushes keep going after a failure, and the
    log shows only the first of a run of the same kind. config.set
    changing notify.chat.url removes last_error.chat; stagent doctor
    names the kind and, for revoked, says to set a new URL from the app
    (#474).

stagent 0.8.0 — server binary for SSH Term. Built from this tag (00e7220) with go1.27.1 via scripts/release.sh (reproducible: CGO_ENABLED=0, -trimpath, no VCS stamp, empty build id); verify downloads against SHA256SUMS.

stagent v0.7.2

Choose a tag to compare

@obutora obutora released this 07 Oct 13:36

Fixed

  • A chat message whose turn to be written comes after an approval menu
    came up is no longer typed into the menu. The holder looked for a menu
    only when session.input arrived, then wrote the message's text from
    its input queue without looking again, so a menu shown in between could
    take the text's first characters as its keys. The queue now looks again
    right before writing the text and drops the whole message (text and
    Enter) if a menu is up; session.input answers a chat message only once
    its text is written, and with menu_open when it was dropped, so the
    app sends it again after the menu as it does for one refused on arrival
    (#423).
  • A chat message sent while Codex shows its update notice or folder trust
    prompt at startup is no longer typed into it. The holder refused chat
    messages only on approval menus (1. Yes…), so the message's Enter
    picked 1. Update now (Codex updated itself) or 1. Trust and continue
    (the folder was trusted). session.input with paste and submit now
    fails with menu_open while any menu is on the screen — numbered
    options with one under the cursor and a key hint below them, as the app
    reads menus — and an Enter that would land on one is held as for an
    approval (#440).
  • A numbered prompt sent to Codex no longer reads as a menu while Codex
    works on it. Codex's status line (• Working (3s • esc to interrupt))
    blinks its bullet to ◦, which the holder did not take for a
    transcript entry, so the line's esc read as a menu's key hint under
    the prompt's numbered lines and chat messages were refused with
    menu_open (#440).
  • Windows: a \\.\pipe\stagent-<SID> another user created first with a
    DACL that refuses the user is now reported with its owner — blocked
    in hello and stagent doctor, foreign_owner for requests — instead
    of failing as a plain access denied. stagent reads the owner by name
    (READ_CONTROL only); a pipe that refuses that too still fails as
    before, and one the user owns, or BUILTIN\Administrators owns (an
    administrator's elevated daemon dialed unelevated), keeps its access
    denied (#422).

stagent 0.7.2 — server binary for SSH Term. Built from this tag (3e63066) with go1.26.1 via scripts/release.sh (reproducible: CGO_ENABLED=0, -trimpath, no VCS stamp, empty build id); verify downloads against SHA256SUMS.

stagent v0.7.1

Choose a tag to compare

@obutora obutora released this 07 Oct 03:06

Security

  • On macOS, stagent now also refuses connections from processes in a
    sandbox (Seatbelt), as Claude Code and Codex run their commands. The
    macOS sandbox has no PID namespace, so a sandboxed command allowed to
    reach ~/.ssh-term/agent/run (sandbox.network.allowUnixSockets)
    could double fork (or setsid) out of the agent's process tree, and
    so past the parent walk of 0.7.0, while staying sandboxed. Such a
    connection, and one handed to such a child, now gets agent_refused
    as a holder or the daemon answers an agent's; hooks and reads are
    still served. stagent asks sandbox_check, which Apple does not
    document; if a macOS lacks it or it answers otherwise, this check is
    left out and the parent walk alone decides, as in 0.7.0 (ADR 0004,
    #415).

stagent 0.7.1 — server binary for SSH Term. Built from this tag (1406849) with go1.26.1 via scripts/release.sh (reproducible: CGO_ENABLED=0, -trimpath, no VCS stamp, empty build id); verify downloads against SHA256SUMS.