Skip to content
Merged
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
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,24 @@ See [demo setup and PostgreSQL metadata storage](site/README.md).

### Host and clients

The **host** owns the agent loop, model credentials, thread state, and workspace.
Browsers and attached terminals are **clients** that watch and steer that thread.
Both use the host's model configuration. Neither needs a personal model key.
The **host** is the tress server at the attach URL. It runs the agent and owns
the model credentials, thread state, and workspace access. Browser tabs,
terminals connected through `tress setup` or `tress attach`, and API connections
are **clients** attached to it. Both browser and terminal use the host's model
configuration; neither needs a personal model key. The thread sidebar and
`/status` show these roles separately.

The server operator supplies the host address. Anyone holding a private thread
link or session ID can join that thread. Setup connects to an existing host;
it does not deploy a server or turn the terminal into a shareable host.

The host label defaults to the server address. `TRESS_HOST_LABEL` can replace
that text with a friendly display label such as `Alice's Mac`; it does not
change networking, permissions, or host ownership. The person running or
deploying tress currently assigns the host. Clients cannot promote themselves
or transfer the host role. See [hosting and client communication](docs/hosting.md)
for the supported modes and complete setup flow.

The optional terminal UI has a persistent `❯` composer, live ready/working status,
and a file preview toggled with `/files` or Ctrl-F. Your draft stays intact
while the host streams. With an empty input, Tab/Shift-Tab changes files;
Expand Down
28 changes: 25 additions & 3 deletions crates/tress/src/attach.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ fn thread_url(input: &str) -> String {
}
}

fn host_origin(input: &str) -> String {
reqwest::Url::parse(input).map_or_else(
|_| input.to_owned(),
|url| url.origin().ascii_serialization(),
)
}

/// The CLI keeps the access ID separate; Statewire uses the scoped endpoint.
fn session_url(input: &str, id: &str) -> Result<String, String> {
let mut url = reqwest::Url::parse(&thread_url(input)).map_err(|error| error.to_string())?;
Expand Down Expand Up @@ -491,11 +498,14 @@ async fn run_inner(
}
}
Some(Command::Status) => {
let host = host_origin(&url);
let detail = snapshot.as_ref().map_or_else(
|| format!("{connection}\nWaiting for workspace"),
|| format!(
"{connection}\nRole terminal client\nHost {host}\nWaiting for workspace"
),
|state| {
let mut detail = format!(
"{connection}\nAgent {}\nRuns {} completed\nFiles {}",
"{connection}\nRole terminal client\nHost {host}\nAgent {}\nRuns {} completed\nFiles {}",
state.status,
state.runs,
state.files.len()
Expand All @@ -514,6 +524,9 @@ async fn run_inner(
if !connected {
detail.push_str("\nUse /reconnect to sync.");
}
detail.push_str(
"\n\nThe host runs the agent and controls workspace access.\nHost assignment is set by the server operator; clients cannot take over the host role yet.",
);
detail
},
);
Expand Down Expand Up @@ -715,9 +728,18 @@ fn emit(entry: &Entry, printed: &mut Printed, style: &crate::Style, complete: bo
#[cfg(test)]
mod tests {
use super::{
client_status, render, session_url, thread_url, ConnectedClient, Printed, WorkspaceInfo,
client_status, host_origin, render, session_url, thread_url, ConnectedClient, Printed,
WorkspaceInfo,
};

#[test]
fn status_identifies_the_host_origin_without_the_thread_path() {
assert_eq!(
host_origin("https://demo.example:8443/api/sessions/secret"),
"https://demo.example:8443"
);
}

#[test]
fn pwd_uses_the_hosts_scoped_root_and_does_not_claim_virtual_files_are_local() {
let workspace = WorkspaceInfo {
Expand Down
118 changes: 118 additions & 0 deletions docs/hosting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Hosting tress

In tress, the **host** is the process that owns the agent. A **client** is a
view or controller connected to that process. The device where someone types
is not necessarily the host.

## Supported modes

| Command or app | Host | Clients | Shareable? |
| --- | --- | --- | --- |
| `tress` after `tress setup` | The saved site host | Terminal and other attached clients | Yes |
| `tress ask <prompt>` in hosted mode | The saved site host | Sends one prompt, then exits; other clients stay attached | Yes |
| `tress --local` | The native terminal process | That terminal only | No |
| `tress --local ask <prompt>` | The native process for one run | None | No |
| tress site | The Node/Farm server | Browser tabs, `tress attach`, and API connections | Yes |
| Embedded engine | The integrating application | Defined by that application | Application-specific |

By default, `tress` connects to the saved host and session. Run `tress setup`
once to choose them; the terminal does not need a personal model or Harness
key. See the [setup guide](setup.md) for installation and release availability.

Explicit `tress --local` makes the terminal process responsible for the model
credential, agent loop, and current directory. It does not start a network
endpoint. A browser or another terminal cannot attach to it. You can save
local mode as your default with `tress setup --local`.

Running `tress attach <url>` always makes that terminal a client. The current
CLI does not have a `tress serve` command for turning a terminal session into a
shareable host.

The included site is the supported shareable host. Its server runs the Rust
agent through WebAssembly, stores the model credential, selects the workspace,
and owns the shared thread. Opening the site does not make the browser the
host; the browser connects to the already-running server.

## Start a site host

Follow the [site setup](../site/README.md#run-locally), then place server-only
settings in `site/.env.local`:

```env
ANTHROPIC_API_KEY="your-key"
TRESS_HOST_LABEL="team dev server"
```

`TRESS_HOST_LABEL` is optional. It only changes the friendly text displayed for
the server. It does not change the URL, networking, permissions, or ownership.
Without it, the UI displays the server address, such as `localhost:5311`.

Restart the server after changing environment variables. For a deployment, set
the same variables in the hosting platform's server environment rather than in
a file committed to Git.

When someone opens the site, it creates a private session and attaches that
browser as a client. The page supplies the command for attaching a terminal to
the same thread:

```sh
tress attach https://your-tress-host.example -s <private-session-id>
```

To save that host and thread for future invocations:

```sh
tress setup --host https://your-tress-host.example -s <private-session-id>
tress
```

The session ID grants access to the thread. Treat the complete browser link and
attach command as private credentials.

## How communication works

```text
browser tab ───────┐
tress attach ──────┼─ Statewire HTTP commands + streamed state ── tress site host
API client ────────┘ │
├─ model provider
└─ host workspace
```

Each client receives replicated thread state: transcript entries, run status,
file previews, workspace metadata, and live presence. A prompt or command goes
to the host. The host runs the agent and tools, mutates the shared state, and
streams those changes back to every attached client. Disconnecting a client
does not move the agent or its workspace and does not stop an in-flight run.

The model key remains on the host. An attached terminal operates on the host's
workspace, not the terminal client's current directory.

## Labels and identity

The host has one optional display label configured by the server operator:

```env
TRESS_HOST_LABEL="Alice's Mac"
```

Client labels require no setup and are currently generated from the connection:

- browsers: `Chrome on macOS`, `Firefox on Linux`, and similar;
- attached terminals: `tress terminal`;
- other HTTP integrations: `API client`.

Every client also has a generated connection ID so two clients with the same
automatic label can still be distinguished. Custom client labels are not yet
supported. Labels and IDs are descriptive presence information, not an
authentication or permission system.

## Public deployments

The demo uses private session IDs for access and includes configurable daily
run/session limits, session expiry, and idle managed-connection cleanup. See
the [host diagnostics and limits](../site/README.md#host-diagnostics-and-demo-limits)
for defaults and configuration. Add your own authentication and ownership rules
when integrating it with an application. PostgreSQL stores session metadata,
shared counters, and serverless relay state; Harness stores the managed
conversation. Local in-memory threads still require a long-lived host.
28 changes: 26 additions & 2 deletions site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,29 @@ the default memory demo instead.
example files, and reset the run count. Reset is blocked while a run is active. In local, overlay, and remote modes,
reset clears the conversation and keeps workspace files.

## Host and client roles

The host and clients are different roles:

- The **host** is the tress server at the attach URL. It runs the agent and
controls model credentials, thread state, and workspace access.
- A **client** is a browser tab, `tress attach` terminal, or API connection
attached to one thread. Clients can watch and steer that thread.

Open the thread sidebar to see the active host and the live clients by type and
short connection ID. `/status` presents the same topology in the browser and
terminal. The host label defaults to the server address, such as
`localhost:5311`. A deployment may set `TRESS_HOST_LABEL` in `site/.env.local`
to show friendlier text such as `Alice's Mac` or `team dev server`. This is only
a display label: it does not change networking, permissions, or who is the host.

The deployment operator currently assigns the host. Anyone holding a private
thread link or session ID can connect as a client, but clients cannot promote
themselves into the host role. Peer election or transferring host ownership
would require a separate coordination and credential-transfer design. The
[hosting guide](../docs/hosting.md) explains standalone terminal sessions,
shareable site hosts, labels, and the Statewire communication flow.

## Slash commands

`tress attach http://localhost:5311 -s <id>` uses plain terminal output by default.
Expand Down Expand Up @@ -263,8 +286,9 @@ directly. Existing shared demo history and files are not moved or deleted.

The server chooses the workspace. The browser and `tress attach` keep the same
protocol and UI in every mode. Set these in `site/.env.local` and restart the
host after changing modes. Native `tress` without `attach` still uses its own
current directory and native shell with approvals.
host after changing modes. Explicit `tress --local` uses the terminal's current
directory and native shell with approvals; hosted `tress` uses the server's
workspace and model credentials.

### Local directory

Expand Down
5 changes: 5 additions & 0 deletions site/src/app/api/mode/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const GET = async (request: Request) => {
const session = await resolveDemoSession(request, true, undefined, owner);
const workspace = workspaceConfig(session?.thread.id);
const mode = threadMode(request.url);
const configuredHostLabel = process.env.TRESS_HOST_LABEL?.trim();
const backend =
process.env.TRESS_SERVERLESS === "1"
? undefined
Expand All @@ -31,6 +32,10 @@ export const GET = async (request: Request) => {
return Response.json(
{
...mode,
host: {
label: configuredHostLabel || new URL(request.url).host,
runtime: mode.kind === "cloud" ? "managed" : "local",
},
...(session
? {
session: sessionInfo(session),
Expand Down
Loading
Loading