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
2 changes: 2 additions & 0 deletions .changes/unreleased/controlled-session-terminal-defaults.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
kind: Changed
body: Default controlled-session terminals to 80 columns by 24 rows when both terminal-dimension options are omitted.
19 changes: 11 additions & 8 deletions docs/CONTROLLED_SESSION_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1786,16 +1786,17 @@ Add the following public host command:
```text
reploy controlled-session run \
--controller-dir DIR --workload-dir DIR \
[--endpoint ID ...] --columns N --rows N \
[--endpoint ID ...] [--columns N --rows N] \
[--output-file FILE | --output-dir DIR] \
[TIMEOUT OPTIONS] -- CONTROLLER_COMMAND [ARG ...]
```

It maps those exact selections into `RunCurrentControlledSessionV1`; the
workload command remains the declared persistent shell. Endpoint flags are
repeatable and optional, dimensions are required and range from 1 through
65535, and the output options retain their existing mutually exclusive
controller-only semantics.
workload always runs `/bin/sh` in the selected image. Endpoint flags are
repeatable and optional. Initial dimensions default to 80 columns by 24 rows;
overrides require both flags, with each value ranging from 1 through 65535.
The output options retain their existing mutually exclusive controller-only
semantics.

The timeout options, defaults, and inclusive override bounds are:

Expand Down Expand Up @@ -1909,9 +1910,11 @@ covers separate controller and workload staging, exact host invocation,
controller client and attachment use, the strict JSON Lines stream, endpoint
and output grants, lifecycle and failure handling, result and exit semantics,
security defaults, and the initial Linux/Docker limitations. Focused OmegaFlow
recording, sandboxed-agent, and security-inspection profiles demonstrate the
same generic boundary without adding profile-specific Reploy authority. The
site introduction and Capabilities navigation expose the guide.
recording, sandboxed-agent, and security-inspection examples demonstrate the
same generic boundary without adding integration-specific Reploy authority.
The sandboxed-agent example places the agent and project in the workload while
leaving only a small trusted session driver in the controller. The site
introduction and Capabilities navigation expose the guide.

### Independent Pre-release Runtime Fixes

Expand Down
27 changes: 14 additions & 13 deletions internal/cli/controlled_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
const controlledSessionRunResultSchemaV1 = "reploy-controlled-session-run-result-v1"

const (
controlledSessionDefaultColumns = 80
controlledSessionDefaultRows = 24
controlledSessionDefaultStartupTimeout = 30 * time.Second
controlledSessionDefaultTerminationGrace = 5 * time.Second
controlledSessionDefaultControllerFinalizationTimeout = 5 * time.Minute
Expand Down Expand Up @@ -147,6 +149,8 @@ func runControlledSession(args []string, stdout io.Writer, stderr io.Writer, glo

func parseControlledSessionRunOptions(args []string) (controlledSessionRunCLIOptions, error) {
options := controlledSessionRunCLIOptions{
Columns: controlledSessionDefaultColumns,
Rows: controlledSessionDefaultRows,
StartupTimeout: controlledSessionDefaultStartupTimeout,
TerminationGrace: controlledSessionDefaultTerminationGrace,
ControllerFinalizationTimeout: controlledSessionDefaultControllerFinalizationTimeout,
Expand Down Expand Up @@ -246,11 +250,8 @@ func parseControlledSessionRunOptions(args []string) (controlledSessionRunCLIOpt
if strings.TrimSpace(options.WorkloadDir) == "" {
return controlledSessionRunCLIOptions{}, fmt.Errorf("--workload-dir is required")
}
if !columnsSet {
return controlledSessionRunCLIOptions{}, fmt.Errorf("--columns is required")
}
if !rowsSet {
return controlledSessionRunCLIOptions{}, fmt.Errorf("--rows is required")
if columnsSet != rowsSet {
return controlledSessionRunCLIOptions{}, fmt.Errorf("--columns and --rows must be provided together")
}
if options.OutputFile != "" && options.OutputDir != "" {
return controlledSessionRunCLIOptions{}, fmt.Errorf("--output-file and --output-dir are mutually exclusive")
Expand Down Expand Up @@ -368,7 +369,7 @@ func printControlledSessionShortUsage(output io.Writer) {
}

func printControlledSessionRunShortUsage(output io.Writer) {
fmt.Fprintln(output, "Usage: reploy controlled-session run --controller-dir DIR --workload-dir DIR [--endpoint ID ...] --columns N --rows N [--output-file FILE | --output-dir DIR] [TIMEOUT OPTIONS] -- CONTROLLER_COMMAND [ARG ...]")
fmt.Fprintln(output, "Usage: reploy controlled-session run --controller-dir DIR --workload-dir DIR [--endpoint ID ...] [--columns N --rows N] [--output-file FILE | --output-dir DIR] [TIMEOUT OPTIONS] -- CONTROLLER_COMMAND [ARG ...]")
}

func printControlledSessionHelp(output io.Writer) {
Expand All @@ -388,19 +389,19 @@ func printControlledSessionRunHelp(output io.Writer) {
printControlledSessionRunShortUsage(output)
fmt.Fprint(output, strings.TrimLeft(`

The workload runs its declared persistent shell. CONTROLLER_COMMAND and all
The workload runs /bin/sh in its selected image. CONTROLLER_COMMAND and all
arguments after -- run only in the selected controller deployment.

Required options:
--controller-dir DIR Controller deployment directory
--workload-dir DIR Workload deployment directory
--columns N Initial terminal columns, 1 through 65535
--rows N Initial terminal rows, 1 through 65535
--controller-dir DIR Prepared controller deployment directory
--workload-dir DIR Prepared workload deployment directory

Optional selections:
--endpoint ID Grant a declared workload endpoint; repeatable
--output-file FILE Publish one controller-created file
--output-dir DIR Retain a controller output directory
--columns N Columns 1-65535; default 80; requires --rows
--rows N Rows 1-65535; default 24; requires --columns
--output-file FILE Publish one controller-created file to the host
--output-dir DIR Retain the controller output directory on the host

Timeouts:
--startup-timeout DURATION Default 30s; 15s through 5m
Expand Down
32 changes: 31 additions & 1 deletion internal/cli/controlled_session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ func TestParseControlledSessionRunOptionsMapsExactSelectionsAndDefaults(t *testi
}
}

func TestParseControlledSessionRunOptionsDefaultsTerminalDimensions(t *testing.T) {
options, err := parseControlledSessionRunOptions([]string{
"--controller-dir=controller", "--workload-dir=workload", "--", "record",
})
if err != nil {
t.Fatal(err)
}
if options.Columns != controlledSessionDefaultColumns || options.Rows != controlledSessionDefaultRows {
t.Fatalf("dimensions = %dx%d, want %dx%d", options.Columns, options.Rows, controlledSessionDefaultColumns, controlledSessionDefaultRows)
}
}

func TestParseControlledSessionRunOptionsAppliesInclusiveTimeoutBounds(t *testing.T) {
options, err := parseControlledSessionRunOptions([]string{
"--controller-dir=c", "--workload-dir=w", "--columns=1", "--rows=65535",
Expand Down Expand Up @@ -66,7 +78,8 @@ func TestParseControlledSessionRunOptionsRejectsUsageErrors(t *testing.T) {
{name: "missing delimiter", args: base[:4], want: "must follow --"},
{name: "missing command", args: base[:5], want: "CONTROLLER_COMMAND"},
{name: "missing controller", args: []string{"--workload-dir=w", "--columns=80", "--rows=24", "--", "controller"}, want: "--controller-dir is required"},
{name: "missing columns", args: []string{"--controller-dir=c", "--workload-dir=w", "--rows=24", "--", "controller"}, want: "--columns is required"},
{name: "only rows", args: []string{"--controller-dir=c", "--workload-dir=w", "--rows=24", "--", "controller"}, want: "--columns and --rows must be provided together"},
{name: "only columns", args: []string{"--controller-dir=c", "--workload-dir=w", "--columns=80", "--", "controller"}, want: "--columns and --rows must be provided together"},
{name: "zero rows", args: []string{"--controller-dir=c", "--workload-dir=w", "--columns=80", "--rows=0", "--", "controller"}, want: "1 through 65535"},
{name: "large columns", args: []string{"--controller-dir=c", "--workload-dir=w", "--columns=65536", "--rows=24", "--", "controller"}, want: "1 through 65535"},
{name: "both outputs", args: []string{"--controller-dir=c", "--workload-dir=w", "--columns=80", "--rows=24", "--output-dir=d", "--output-file=f", "--", "controller"}, want: "mutually exclusive"},
Expand Down Expand Up @@ -205,6 +218,23 @@ func TestControlledSessionRunUsageErrorLeavesStdoutEmpty(t *testing.T) {
}
}

func TestControlledSessionRunHelpDocumentsOptionalDefaultDimensions(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Main([]string{"controlled-session", "run", "--help"}, &stdout, &stderr)
if code != 0 || stderr.String() != "" {
t.Fatalf("help code=%d stderr=%q", code, stderr.String())
}
for _, want := range []string{
"[--columns N --rows N]",
"Columns 1-65535; default 80; requires --rows",
"Rows 1-65535; default 24; requires --columns",
} {
if !strings.Contains(stdout.String(), want) {
t.Fatalf("help is missing %q:\n%s", want, stdout.String())
}
}
}

func TestControlledSessionRunSuccessPredicateRejectsIncompleteResults(t *testing.T) {
codeZero := 0
base := successfulControlledSessionRunResultV1(&codeZero)
Expand Down
131 changes: 88 additions & 43 deletions website/docs/controlled-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ of a separate workload deployment. The controller can automate a terminal,
reach explicitly selected workload endpoints, and retain its own artifacts
without receiving implicit Docker or host authority.

Common uses include running a sandboxed agent against a project, dynamically
inspecting an application, and recording terminal-and-browser workflows.
See [Integration examples](#integration-examples) for brief examples of each.

![Controlled-session actors and operations](/img/controlled-session-overview.svg)

The initial controlled-session runtime requires a Linux host running Docker;
Docker Desktop on macOS or Windows is not supported for this capability.
Reploy supports Linux `amd64` and `arm64` controller images and installs the
Expand Down Expand Up @@ -35,7 +41,18 @@ printed JSON cannot forge control events.
## Prepare the two deployments

Create and build separate staging deployments for the controller and workload.
For example:
At minimum, select the two prepared deployments and the declared controller
command. This uses an `80` by `24` terminal and grants no workload endpoints:
Comment on lines +44 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move the run example after deployment preparation

A reader following this preparation section top-to-bottom is told to run controlled-session before the subsequent stage and build commands have created the referenced deployments, so the new minimum example fails at this point. The same invocation is already documented under Run a session; keep this section focused on creating the deployments or place the run example after the preparation steps.

Useful? React with 👍 / 👎.


```bash
reploy controlled-session run \
--controller-dir ./controller-staging \
--workload-dir ./workload-staging \
-- run-controller
```

Add only the optional selections the controller needs. A fully selected
example is:

```bash
reploy stage ./controller.blueprint.yaml --dir ./controller-staging
Expand Down Expand Up @@ -75,12 +92,23 @@ environment:
```text
reploy controlled-session run \
--controller-dir DIR --workload-dir DIR \
[--endpoint ID ...] --columns N --rows N \
[--endpoint ID ...] [--columns N --rows N] \
[--output-file FILE | --output-dir DIR] \
[TIMEOUT OPTIONS] -- CONTROLLER_COMMAND [ARG ...]
```

For example:
At minimum, select the two prepared deployments and the declared controller
command. This uses an `80` by `24` terminal and grants no workload endpoints:

```bash
reploy controlled-session run \
--controller-dir ./controller-staging \
--workload-dir ./workload-staging \
-- run-controller
```

Add only the optional selections the controller needs. A fully selected
example is:

```bash
reploy controlled-session run \
Expand All @@ -92,10 +120,20 @@ reploy controlled-session run \
-- run-controller
```

`--endpoint` is optional and repeatable. Each value must name an endpoint
declared by the exact workload generation. Terminal dimensions are required
and must be from 1 through 65535. The command and every argument after `--` run
in the controller; the workload always runs `/bin/sh` in its exact image.
The full example selects each part of the session explicitly:

| Selection | Meaning |
| --- | --- |
| `--controller-dir ./controller-staging` | Use the exact current generation in the prepared controller deployment. This image contains the controller program and receives the Reploy session client. |
| `--workload-dir ./workload-staging` | Use the exact current generation in the prepared workload deployment. Reploy starts `/bin/sh` in this image. |
| `--endpoint web` | Grant the controller access to the workload endpoint named `web`. This is optional, repeatable, and valid only for endpoints declared by the selected workload generation. |
| `--columns 120 --rows 40` | Start the workload PTY at 120 columns by 40 rows. The pair is optional; omitting both uses `80` columns by `24` rows. If overriding the size, provide both values from 1 through 65535. |
| `--output-dir ./session-artifacts` | Retain the controller's output directory at this host path after teardown. The destination is not exposed to the workload. Use `--output-file` instead when publishing one controller-created file. |
| `-- run-controller` | `--` ends the host options. `run-controller` names the native command declared in the controller blueprint; it and any following arguments run only in the controller. |

The controller and workload directories are required. Endpoint, terminal-size,
and output selections are optional. The workload always runs `/bin/sh` in its
exact image.

The optional timeouts are:

Expand Down Expand Up @@ -308,12 +346,13 @@ successful completion.

## Security defaults and limitations

Controlled sessions use Reploy's ordinary application-container sandbox. They
do not create a special weaker or stronger security tier. Both application
containers use an explicit runtime identity, a read-only root filesystem plus
declared writable storage, seccomp, `no-new-privileges`, and empty Linux
capability sets. They receive no privileged mode, host namespaces, host
devices, inherited host environment, or Docker socket.
Both the controller container and the workload container use Reploy's ordinary
application-container sandbox. Controlled sessions do not create a special
weaker or stronger security tier. Each container uses an explicit runtime
identity, a read-only root filesystem plus declared writable storage, seccomp,
`no-new-privileges`, and empty Linux capability sets. Neither receives
privileged mode, host namespaces, host devices, inherited host environment, or
the Docker socket.

Authority remains declaration-driven:

Expand All @@ -339,10 +378,43 @@ network has the coarse reachability described above. A backend that cannot
establish the required Linux sandbox fails closed instead of silently
degrading.

## Integration profiles
## Integration examples

These are example ways to build controllers on the same generic boundary, not
named Reploy resources, configuration profiles, or selectable session modes.
They do not change the session abstraction or grant integration-specific host
authority. The agent and inspection examples illustrate uses of the generic
boundary; the OmegaFlow example is backed by Reploy's conformance fixture.

These profiles use the same generic boundary. They do not change the session
abstraction or grant profile-specific host authority.
### Sandboxed agent

Place the agent runtime and the project it can modify in the workload
deployment. Keep only a small trusted session driver in the controller. The
driver starts and observes the agent through the controlled terminal, retains
transcripts or reports in the controller output directory, and uses only the
workload endpoints explicitly needed for external observation.

This placement contains the agent together with the code it executes. The
agent receives neither the controller-private session socket nor the
controller output directory, Docker socket, or host process authority. Policy
that must remain outside the agent's control belongs in the trusted controller
or host. The workload sandbox still does not provide domain-level egress
policy, and secrets placed in the workload are visible to both the agent and
the project code it runs.

### Security inspection

Place scanners and inspection orchestration in the controller and the target
application in the workload. Use named endpoints for dynamic inspection and a
controller output directory for findings and partial evidence. Treat terminal
output, files, and service responses as hostile input, and retain the full
structured result with the report so cleanup or observation failures remain
visible.

This profile supplies process separation and deny-oriented container defaults;
it is not a network IDS, HTTP policy engine, malware containment guarantee, or
content-sanitization layer. The controller/workload private-network limitation
still applies.

### OmegaFlow recording

Expand Down Expand Up @@ -377,33 +449,6 @@ artifact retention. OmegaFlow continues to own command-completion detection,
cwd reporting, action markers, terminal-to-browser handoff, browser actions,
recording policy, redaction, and media rendering.

### Sandboxed agent

Place the agent runtime and policy code in the controller deployment and the
project under test in the workload deployment. Drive its shell through the
attachment or use structured resize for a headless terminal. Grant only named
workload endpoints the agent needs and write transcripts or reports to a
controller output directory.

The split keeps project code and terminal output untrusted without giving the
agent a Docker socket or host process authority. It does not make the trusted
agent controller safe from the data and endpoints explicitly granted to it,
and it does not provide domain-level egress policy.

### Security inspection

Place scanners and inspection orchestration in the controller and the target
application in the workload. Use named endpoints for dynamic inspection and a
controller output directory for findings and partial evidence. Treat terminal
output, files, and service responses as hostile input, and retain the full
structured result with the report so cleanup or observation failures remain
visible.

This profile supplies process separation and deny-oriented container defaults;
it is not a network IDS, HTTP policy engine, malware containment guarantee, or
content-sanitization layer. The controller/workload private-network limitation
still applies.

## Compatibility fixtures

The public golden fixtures in
Expand Down
Loading
Loading