Skip to content
Closed
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
4 changes: 4 additions & 0 deletions docs/explanation/container-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ Two named volumes are shared between both containers: one for TLS certificates (

Environment variables configured via `env` or `env_file` in the workspace config or the `[defaults]` section are passed to the opencode container alongside the system variables required for dind connectivity. Values are literal strings — no host environment variable expansion is performed.

jailoc also injects two variables for programmatic detection: `JAILOC=1` (a fixed marker indicating the process is running inside a jailoc container) and `JAILOC_WORKSPACE=<name>` (the workspace name). These allow agents, scripts, and tooling to detect the sandboxed context without inspecting the filesystem or environment. Both keys are reserved and cannot be overridden via `env` or `env_file`.

When `jailoc attach` connects a TUI to the running server, it sets the host terminal title to `jailoc | <workspace>` via an OSC 0 escape sequence and passes `OPENCODE_DISABLE_TERMINAL_TITLE=1` to the OpenCode TUI process so it does not overwrite the title. On detach the title is cleared.

## The entrypoint sequence

The entrypoint script is bind-mounted into the container at runtime by jailoc and runs as root. It performs three distinct phases before handing off to the agent process.
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ Each entry must be in `KEY=VALUE` format (key cannot be empty, must contain `=`)
| `DOCKER_CERT_PATH` | DinD TLS certs |
| `DOCKER_TLS_VERIFY` | DinD TLS verification |
| `SSH_AUTH_SOCK` | SSH agent passthrough |
| `JAILOC` | jailoc sandbox marker |
| `JAILOC_WORKSPACE` | workspace identity |

### `env_file`

Expand Down
23 changes: 21 additions & 2 deletions internal/cmd/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
Expand All @@ -17,6 +18,13 @@ import (
"github.com/seznam/jailoc/internal/workspace"
)

// setTerminalTitle writes an OSC 0 escape sequence to w, setting the terminal
// title to title. An empty title clears it. w is typically os.Stdout, but is
// taken as a parameter for testability.
func setTerminalTitle(w io.Writer, title string) {
_, _ = fmt.Fprintf(w, "\033]0;%s\007", title)
}

func attachHostArgs(serverURL, password, dir string) []string {
args := []string{"attach", serverURL}
if password != "" {
Expand All @@ -42,13 +50,19 @@ func attachOnHost(ctx context.Context, ws *workspace.Resolved, dir string) error
return fmt.Errorf("resolve opencode binary: %w", err)
}

if term.IsTerminal(int(os.Stdout.Fd())) { //nolint:gosec // Fd() fits in int on all supported platforms
setTerminalTitle(os.Stdout, "jailoc | "+ws.Name)
defer setTerminalTitle(os.Stdout, "")
}

serverArg := fmt.Sprintf("http://localhost:%d", ws.Port)
password := os.Getenv("OPENCODE_SERVER_PASSWORD")
args := attachHostArgs(serverArg, password, dir)
cmd := exec.Command(binary, args...) //nolint:gosec // binary name is from ResolveBinary, args are controlled
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), "OPENCODE_DISABLE_TERMINAL_TITLE=1")

err = runCommandWithContext(ctx, cmd, func() error {
if cmd.Process == nil {
Expand All @@ -59,7 +73,7 @@ func attachOnHost(ctx context.Context, ws *workspace.Resolved, dir string) error
return attachResult(ctx, err)
}

func attachExec(ctx context.Context, client *docker.Client, dir string) error {
func attachExec(ctx context.Context, client *docker.Client, workspaceName, dir string) error {
fd := int(os.Stdin.Fd()) //nolint:gosec // Fd() fits in int on all supported platforms
oldState, err := term.MakeRaw(fd)
if err != nil {
Expand All @@ -79,8 +93,13 @@ func attachExec(ctx context.Context, client *docker.Client, dir string) error {
close(sigCh)
}()

if term.IsTerminal(int(os.Stdout.Fd())) { //nolint:gosec // Fd() fits in int on all supported platforms
setTerminalTitle(os.Stdout, "jailoc | "+workspaceName)
defer setTerminalTitle(os.Stdout, "")
}

serverURL := fmt.Sprintf("http://localhost:%d", workspace.BasePort)
err = client.Exec(ctx, attachExecArgs(serverURL, dir), os.Stdin, os.Stdout, os.Stderr)
err = client.Exec(ctx, attachExecArgs(serverURL, dir), []string{"OPENCODE_DISABLE_TERMINAL_TITLE=1"}, os.Stdin, os.Stdout, os.Stderr)
return attachResult(ctx, err)
}

Expand Down
29 changes: 29 additions & 0 deletions internal/cmd/attach_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"bytes"
"context"
"errors"
"os/exec"
Expand All @@ -10,6 +11,34 @@ import (
"time"
)

func TestSetTerminalTitle(t *testing.T) {
t.Parallel()

t.Run("sets title", func(t *testing.T) {
t.Parallel()

var buf bytes.Buffer
setTerminalTitle(&buf, "jailoc | myworkspace")
got := buf.String()
want := "\033]0;jailoc | myworkspace\007"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
})

t.Run("clears title with empty string", func(t *testing.T) {
t.Parallel()

var buf bytes.Buffer
setTerminalTitle(&buf, "")
got := buf.String()
want := "\033]0;\007"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
})
}

func TestAttachHostArgs(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ var rootCmd = &cobra.Command{
switch mode {
case config.ModeExec:
_, _ = color.New(color.FgCyan).Printf("Attaching to workspace %s (exec mode)...\n", ws.Name)
attachErr = attachExec(attachCtx, client, targetPath)
attachErr = attachExec(attachCtx, client, ws.Name, targetPath)
default:
_, _ = color.New(color.FgCyan).Printf("Attaching to workspace %s (remote mode)...\n", ws.Name)
attachErr = attachOnHost(attachCtx, ws, targetPath)
Expand Down
2 changes: 2 additions & 0 deletions internal/compose/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ func TestGenerateComposeSinglePath(t *testing.T) {
assertContains(t, rendered, "- \"4111:4096\"")
assertContains(t, rendered, "- /Users/test/work/project:/Users/test/work/project")
assertContains(t, rendered, "- OPENCODE_SERVER_PASSWORD=secret")
assertContains(t, rendered, "- JAILOC=1")
assertContains(t, rendered, "- JAILOC_WORKSPACE=alpha")
assertContains(t, rendered, "opencode-data-alpha")
assertContains(t, rendered, "opencode-cache-alpha")
assertContains(t, rendered, "working_dir: /Users/test/work/project")
Expand Down
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ var reservedEnvKeys = map[string]bool{
"DOCKER_CERT_PATH": true,
"DOCKER_TLS_VERIFY": true,
"SSH_AUTH_SOCK": true,
"JAILOC": true,
"JAILOC_WORKSPACE": true,
}

var forbiddenMountPrefixes = []string{
Expand Down
2 changes: 2 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,8 @@ func TestValidateEnvReservedKeys(t *testing.T) {
"DOCKER_TLS_CERTDIR",
"DOCKER_CERT_PATH",
"DOCKER_TLS_VERIFY",
"JAILOC",
"JAILOC_WORKSPACE",
Comment thread
speedo17 marked this conversation as resolved.
}

for _, key := range reserved {
Expand Down
3 changes: 2 additions & 1 deletion internal/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ func (c *Client) TailLogs(ctx context.Context, n int, w io.Writer) error {
return nil
}

func (c *Client) Exec(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
func (c *Client) Exec(ctx context.Context, args []string, env []string, stdin io.Reader, stdout, stderr io.Writer) error {
var stdinRC io.ReadCloser
if stdin != nil {
if rc, ok := stdin.(io.ReadCloser); ok {
Expand Down Expand Up @@ -285,6 +285,7 @@ func (c *Client) Exec(ctx context.Context, args []string, stdin io.Reader, stdou
exitCode, err := svc.Exec(ctx, "jailoc-"+c.workspace, api.RunOptions{
Service: "opencode",
Command: args,
Environment: env,
Tty: true,
Interactive: stdin != nil,
Index: 1,
Expand Down
2 changes: 2 additions & 0 deletions internal/embed/assets/docker-compose.yml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ services:
{{- end}}
environment:
- OPENCODE_LOG=debug
- JAILOC=1
- JAILOC_WORKSPACE={{.WorkspaceName}}
- OPENCODE_SERVER_PASSWORD={{.OpenCodePassword}}
- DOCKER_HOST=tcp://dind:2376
- DOCKER_TLS_CERTDIR=/certs
Expand Down
Loading