Skip to main content

Exec Session Provider

还没翻译译自英文(docs.gascity.com)本页 44 段都还没译

Gas City's exec session provider delegates each runtime.Provider operation to a user-supplied script. This allows any terminal multiplexer or process manager to be used as a session backend without writing Go code.

Usage

Set the GC_SESSION environment variable to exec:<script>:

# Absolute path
export GC_SESSION=exec:/path/to/gc-session-screen

# PATH lookup
export GC_SESSION=exec:gc-session-screen

Pack-Declared Runtimes

A pack can ship (or install) a runtime executable and bind it to a selection name in pack.toml:

[runtimes.cloudflare]
command = "scripts/gc-runtime-cloudflare"   # pack-relative, or PATH name
protocol = 0

City composition registers the name into the runtime selection registry, so city.toml selects it like a builtin:

[session]
provider = "cloudflare"

Rules:

  • A command containing a path separator resolves relative to the pack directory; a bare name resolves on PATH at session start.
  • protocol declares the RPP version the executable speaks (version 0 is the only version today); any other value fails composition.
  • Name collisions with builtin runtimes or other packs are composition errors — no silent shadowing. Identical re-declarations of the same pack reached through a diamond import graph dedupe.
  • The pack-runtimes doctor check verifies each declared executable is installed and that its RPP handshake works. An executable with no protocol op is the version-0 floor and passes; a present-but-broken handshake fails.
  • Config reload enforces the same registration rules, and rebuilds the session provider when the declaration behind the selected name changes (the executable binding is fixed at provider construction).
  • gc runtime check <name> and gc runtime conformance <name> both resolve the declared name and run against the pack's executable: check is the smoke test, conformance the full requirement-coded suite.

Calling Convention

The script receives the operation name as its first argument:

<script> <operation> <session-name> [args...]

No shell invocation — the script is exec'd directly.

Exit Codes

CodeMeaning
0Success
1Failure (stderr contains error message)
2Unknown operation (treated as success — forward compatible)

Exit code 2 is the forward-compatibility mechanism. When Gas City adds new operations in the future, old scripts return exit 2 and the provider treats it as a no-op success. Scripts only need to implement the operations they care about.

Operations

OperationInvocationStdinStdout
startscript start <name>JSON config—
stopscript stop <name>——
interruptscript interrupt <name>——
is-runningscript is-running <name>—true or false
attachscript attach <name>tty passthroughtty passthrough
process-alivescript process-alive <name>process names (1/line)true or false
nudgescript nudge <name>message text—
set-metascript set-meta <name> <key>value on stdin—
get-metascript get-meta <name> <key>—value (empty = not set)
remove-metascript remove-meta <name> <key>——
peekscript peek <name> <lines>—captured text
list-runningscript list-running <prefix>—one name per line
get-last-activityscript get-last-activity <name>—RFC3339 or empty
protocolscript protocol—handshake JSON (see below)
is-attachedscript is-attached <name>—true or false
execscript exec <name>commandcombined output (op exit == command exit)
provisionscript provision <name>JSON config—

Box without agent (the un-weld). provision is start MINUS the agent launch: it creates/prepares the box (PreStart, SessionSetup, SessionSetupScript, SessionLive — every box step start runs EXCEPT spawning the agent in tmux) and returns. The controller then launches the agent itself by exec-ing tmux new-session / respawn-pane -k over the exec op, so a launch-only config change relaunches the agent in the warm box instead of reprovisioning (B2.3). A pack opts in by declaring proc.provision (and proc.exec, since the controller drives the launch over exec); without it, the welded start op provisions and launches as before, and the controller issues no provision/launch. The op is gated by the RPP-PROVISION-001 conformance requirement.

The connection primitive (slim RPP). exec is the connection op (RPP-CONN-001): a carrier drives a box through exec rather than via dedicated driving ops, and any runtime that declares an env.* capability already implements it. It is optional for now — conformance verifies it only when present (the output reaches the caller and the op exit mirrors the command's exit code) — and becomes required as Gas City moves its own input delivery and observation onto exec. The dedicated driving ops (interrupt, nudge, peek, clear-scrollback, send-keys) are reproducible over exec and are deliberately NOT conformance requirements: gc now drives input and reads output over exec (via the tmux carrier) when a runtime implements it, and falls back to the dedicated driving ops when exec is unsupported (RPP-CONN-001 answered exit 2). So a runtime that ships exec + tmux-in-box needs none of the driving ops, while one that implements only the driving ops keeps working via the fallback. (watch-startup is a streaming op the request/response exec connection cannot carry, so it stays a dedicated op.)

Protocol Handshake (protocol)

The protocol operation declares which Runtime Provider Protocol version the script speaks and which optional capabilities it implements:

{"version": 0, "capabilities": ["report-attachment", "report-activity"]}

Scripts that do not implement protocol (exit 2) are treated as version 0 with no optional capabilities — every pre-handshake script remains valid. Unknown capability strings are ignored, so scripts may declare capabilities for newer Gas City versions without breaking older ones. Malformed handshake JSON is an error: capability probes fall back to the no-capability behavior and the failure is reported by conformance and doctor checks.

Capabilities:

CapabilityEffect
report-attachmentis-attached <name> is called and trusted; without it, sessions always read as detached and is-attached is never invoked.
report-activityget-last-activity <name> results are treated as meaningful for idle/health decisions.
proc.execThe exec op's process exit code carries the in-box command's exit code, so an exec-op exit of 2 is read as the command's own exit 2 rather than the "unknown op" sentinel (ErrExecUnsupported). Lets the carrier drive input/output over exec; without it, gc uses the dedicated driving ops (the fallback path).
proc.provisionThe script implements the box-without-agent provision op (see Operations), so the controller provisions the box, then launches the agent over exec (the un-weld). Without it, start provisions and launches in one op.
proc.streamReserved (connection-plane family, parallel to env.*): declares the persistent bidirectional stream connection op (ACP over a stream, tmux pipe-pane). Sets CanStream. The stream op and its capability-gated conformance entry land with the connection rewrite.
tty.attachReserved: declares an interactive PTY attach connection op. Sets CanAttachTTY.

The handshake runs once per provider instance and is cached.

Start Config (JSON on stdin)

The start operation receives a JSON object on stdin:

{
  "work_dir": "/path/to/working/directory",
  "command": "claude --dangerously-skip-permissions",
  "env": {"GC_AGENT": "mayor", "GC_CITY": "/home/user/bright-lights"},
  "lifecycle": "one_shot",
  "process_names": ["claude", "node"],
  "nudge": "initial prompt text",
  "pre_start": ["mkdir -p /workspace", "git clone repo /workspace"]
}

All fields are optional (omitted when empty).

Startup Hints

The JSON config contains fields that the tmux provider uses for multi-step startup orchestration. The exec provider itself is fire-and-forget — it calls script start and returns immediately. Scripts may handle these hints or ignore them:

  • process_names — the tmux adapter polls for these process names to appear in the session's process tree (30s timeout) before considering the agent "started." A script can implement this by polling its backend's process tree after session creation, or ignore it for fire-and-forget behavior (like the subprocess provider does).

  • lifecycle — "one_shot" marks a short-lived command that is expected to exit after handling its prompt. Providers should not require a persistent post-start process for one-shot starts.

  • nudge — text that the tmux adapter types into the session after the agent is ready. Scripts that support interactive input can handle this in start (type the text after session creation) or leave it to the separate nudge operation which gc calls after start returns.

  • pre_start — array of shell commands to run on the target filesystem before the session is created. Used for directory preparation, worktree creation, or other setup that must exist before the agent starts. Scripts should execute each command in the target environment before creating the tmux session. Non-fatal: warn on stderr if a command fails, but don't abort start.

  • session_setup — array of shell commands to run on the target filesystem after the session is created and ready, before returning. Scripts should execute each command inside the session environment (e.g. kubectl exec -- sh -c '<cmd>' for K8s, docker exec -- sh -c '<cmd>' for Docker, or plain sh -c '<cmd>' for local providers). Non-fatal: warn on stderr if a command fails, but don't abort start.

  • session_setup_script — path to a script on the orchestrator filesystem, run after session_setup commands. For remote providers (K8s, Docker), read the file locally and pipe its contents into the session (e.g. kubectl exec -i -- sh < script). For local providers, run directly via sh -c. Non-fatal like session_setup.

Fields that are not included in the JSON (gc-internal, not part of the exec protocol):

  • ready_prompt_prefix — prompt prefix for readiness detection (gc polls via peek after start returns)
  • ready_delay_ms — fixed delay fallback (gc sleeps after start returns)
  • emits_permission_warning — bypass-permissions dialog handling
  • fingerprint_extra — config change detection metadata

The distinction: readiness polling and delay are the caller's responsibility. Session setup commands are the script's responsibility — they run on the target filesystem, not the orchestrator.

Conventions

  • stdin for values: set-meta, nudge, and start pass data on stdin to avoid shell quoting and argument length limits.
  • stdout for results: is-running, process-alive return true/false. get-meta returns the value or empty for unset. list-running returns one name per line.
  • Idempotent stop: stop must succeed (exit 0) even if the session doesn't exist.
  • Best-effort interrupt/nudge: Return 0 even if the session doesn't exist.
  • Empty = unsupported: get-last-activity returning empty stdout means the backend doesn't support activity tracking (zero time in Go).

Writing Your Own Script

  1. Start with contrib/session-scripts/gc-session-screen as a template.
  2. Implement the operations your backend supports.
  3. Return exit 2 for operations you don't support.
  4. Validate with gc runtime check ./your-script — it runs the protocol handshake, the required lifecycle round-trip (start, is-running, stop, idempotent stop), exercises every capability the handshake declares, and probes optional operations (absent ones are reported, not failed). It exits non-zero if any check fails, so CI can gate on it.
  5. Test with GC_SESSION=exec:./your-script gc start <city>.

Minimal script (start/stop/is-running only)

#!/bin/sh
op="$1"
name="$2"
case "$op" in
  start)     cat > /dev/null; my-mux new "$name" ;;
  stop)      my-mux kill "$name" 2>/dev/null; exit 0 ;;
  is-running) my-mux list | grep -q "^${name}$" && echo true || echo false ;;
  *)         exit 2 ;;
esac

Environment Variables

Scripts can use GC_EXEC_STATE_DIR (if set) as a directory for sidecar state files (metadata, wrappers). If not set, scripts should use a reasonable default under $TMPDIR or /tmp.

Shipped Scripts

See contrib/session-scripts/ for maintained implementations:

  • gc-session-screen — GNU screen backend. Dependencies: screen, jq, bash.