From 4cdd7c7bd69c1adf085a2630975b113b5a318d54 Mon Sep 17 00:00:00 2001 From: Vitalii Valkov Date: Tue, 11 Aug 2026 05:27:27 +0200 Subject: [PATCH 1/2] A shell is named, not implied by the connection Adds SESSION_KIND_SHELL_ATTACH with the two parameters that distinguish it -- shell_id naming the shell and shell_cwd applied only when one has to be created -- and the layout methods that hold what a client reopens to. SHELL_ATTACH is a separate kind rather than a flag on SHELL so the command stays a function of the kind and its parameters, and a client cannot get one behavior while the ticket describes the other. SHELL keeps its meaning and its command. SandboxLayout is a versioned document rather than a row per tab. Reordering, closing and opening are one write, and the fields this is expected to grow -- split geometry, sizes, focus -- arrive without a method each. The version is what makes two devices safe: the loser refetches rather than silently overwriting. Identity is never a request field on the external methods. It comes from authenticated context, so there is no call that reads or writes another person's tabs. SetSandboxLayoutDirectories is internal and version-free. The orchestrator is its sole writer, touching one field immediately before a stop, and failing on a concurrent reorder would lose the snapshot for no benefit. Additive throughout; buf breaking is clean against main. --- proto/agynio/api/agents/v1/agents.proto | 97 +++++++++++++++++++ proto/agynio/api/gateway/v1/agents.proto | 7 ++ proto/agynio/api/gateway/v1/terminal.proto | 8 ++ .../terminal_proxy/v1/terminal_proxy.proto | 28 +++++- 4 files changed, 139 insertions(+), 1 deletion(-) diff --git a/proto/agynio/api/agents/v1/agents.proto b/proto/agynio/api/agents/v1/agents.proto index b0d414e..6bd53a4 100644 --- a/proto/agynio/api/agents/v1/agents.proto +++ b/proto/agynio/api/agents/v1/agents.proto @@ -45,6 +45,11 @@ service AgentsService { rpc UpdateSandboxRuntimeState(UpdateSandboxRuntimeStateRequest) returns (UpdateSandboxRuntimeStateResponse); rpc UpdateSandboxLastSession(UpdateSandboxLastSessionRequest) returns (UpdateSandboxLastSessionResponse); + // --- Sandbox Layouts --- + rpc GetSandboxLayout(GetSandboxLayoutRequest) returns (GetSandboxLayoutResponse); + rpc SetSandboxLayout(SetSandboxLayoutRequest) returns (SetSandboxLayoutResponse); + rpc SetSandboxLayoutDirectories(SetSandboxLayoutDirectoriesRequest) returns (SetSandboxLayoutDirectoriesResponse); + // --- Agent Instances --- rpc CreateInstance(CreateInstanceRequest) returns (CreateInstanceResponse); rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse); @@ -838,6 +843,98 @@ message UpdateSandboxLastSessionResponse { Sandbox sandbox = 1; } +// =========================================================================== +// Sandbox Layout +// =========================================================================== + +// One identity's set of open shells in one sandbox — what a client reopens to +// find its work where it left it. +// +// The service never looks in the container. Attaching creates a shell that +// does not exist, so a layout naming a shell the container lost is not an +// inconsistency to reconcile but the ordinary case after a restart. +message SandboxLayout { + string sandbox_id = 1; + + // Whose layout this is. Always the caller's own — resolved from + // authenticated context, never accepted as input on the external methods. + string identity_id = 2; + + // Incremented on every write. A writer supplies the version it read and is + // rejected with FailedPrecondition when it no longer matches. + int64 version = 3; + + // Ordered. Position is display order. + repeated SandboxTab tabs = 4; +} + +message SandboxTab { + // Opaque, client-generated, ^[A-Za-z0-9_-]{1,64}$. Passed verbatim as + // shell_id on a SHELL_ATTACH terminal session. + string shell_id = 1; + + // Assigned when the tab is opened and never reused within the layout. The + // last-resort display name, so closing one tab does not rename the others. + int32 number = 2; + + // A name the user gave this tab. Unset means the client derives one — the + // title the shell announces, else its directory. The derived name is never + // stored: it goes stale the moment the shell changes directory. + optional string name_override = 3; + + // Last known working directory, absolute. Supplied as shell_cwd and applied + // only when the shell has to be created. Written by the Orchestrator before + // a planned stop, which is the last moment it can be read and the first it + // is about to be needed. + optional string cwd = 4; + + // When a session last attached. Orders "the one I was just in". + optional google.protobuf.Timestamp last_attached_at = 5; +} + +message GetSandboxLayoutRequest { + string sandbox_id = 1; +} + +message GetSandboxLayoutResponse { + // A sandbox never worked in returns an empty layout at version 0 rather than + // NotFound — no client needs to tell those apart. + SandboxLayout layout = 1; +} + +message SetSandboxLayoutRequest { + string sandbox_id = 1; + + // The version the caller read. A mismatch is FailedPrecondition; the caller + // refetches and reapplies. + int64 version = 2; + + repeated SandboxTab tabs = 3; +} + +message SetSandboxLayoutResponse { + SandboxLayout layout = 1; +} + +// Internal only. Writes cwd onto the tabs of every layout of one sandbox, +// matching by shell_id and ignoring ids it does not find. +// +// Version-free: it touches one field the caller is the sole writer of, and +// failing on a concurrent tab reorder would lose the snapshot for no benefit. +message SetSandboxLayoutDirectoriesRequest { + string sandbox_id = 1; + repeated ShellDirectory directories = 2; +} + +message ShellDirectory { + string shell_id = 1; + string cwd = 2; +} + +message SetSandboxLayoutDirectoriesResponse { + int32 tabs_updated = 1; +} + // =========================================================================== // Volume // =========================================================================== diff --git a/proto/agynio/api/gateway/v1/agents.proto b/proto/agynio/api/gateway/v1/agents.proto index adae111..ea857a5 100644 --- a/proto/agynio/api/gateway/v1/agents.proto +++ b/proto/agynio/api/gateway/v1/agents.proto @@ -36,6 +36,13 @@ service AgentsGateway { rpc DeleteSandbox(agynio.api.agents.v1.DeleteSandboxRequest) returns (agynio.api.agents.v1.DeleteSandboxResponse); rpc EnsureSandboxRunning(agynio.api.agents.v1.EnsureSandboxRunningRequest) returns (agynio.api.agents.v1.EnsureSandboxRunningResponse); + // --- Sandbox Layouts --- + // Always the caller's own layout: identity comes from authenticated context + // and is not a request field, so there is no call that reads or writes + // another person's tabs. + rpc GetSandboxLayout(agynio.api.agents.v1.GetSandboxLayoutRequest) returns (agynio.api.agents.v1.GetSandboxLayoutResponse); + rpc SetSandboxLayout(agynio.api.agents.v1.SetSandboxLayoutRequest) returns (agynio.api.agents.v1.SetSandboxLayoutResponse); + // --- Agent Instances --- rpc CreateInstance(agynio.api.agents.v1.CreateInstanceRequest) returns (agynio.api.agents.v1.CreateInstanceResponse); rpc GetInstance(agynio.api.agents.v1.GetInstanceRequest) returns (agynio.api.agents.v1.GetInstanceResponse); diff --git a/proto/agynio/api/gateway/v1/terminal.proto b/proto/agynio/api/gateway/v1/terminal.proto index f35127a..365ded2 100644 --- a/proto/agynio/api/gateway/v1/terminal.proto +++ b/proto/agynio/api/gateway/v1/terminal.proto @@ -24,6 +24,14 @@ message CreateTerminalSessionRequest { // The directory to serve. Meaningful only when kind is SYNC. Must be // absolute. string sync_root = 5; + + // Names the shell to attach to. Required when kind is SHELL_ATTACH and + // ignored otherwise. Opaque, ^[A-Za-z0-9_-]{1,64}$. + string shell_id = 6; + + // Working directory for the shell, applied only when it has to be created. + // Optional; must be absolute when set. + string shell_cwd = 7; } message CreateTerminalSessionResponse { diff --git a/proto/agynio/api/terminal_proxy/v1/terminal_proxy.proto b/proto/agynio/api/terminal_proxy/v1/terminal_proxy.proto index 1c1b2c8..a391bdf 100644 --- a/proto/agynio/api/terminal_proxy/v1/terminal_proxy.proto +++ b/proto/agynio/api/terminal_proxy/v1/terminal_proxy.proto @@ -18,7 +18,8 @@ enum SessionKind { // ends up in a ticket. SESSION_KIND_UNSPECIFIED = 0; - // A login shell on a PTY, with the platform binaries on PATH. + // A login shell on a PTY, with the platform binaries on PATH. Ephemeral: the + // PTY closes with the connection, as a dropped SSH connection does. SESSION_KIND_SHELL = 1; // A caller-supplied command on a PTY. Carries no more privilege than SHELL — @@ -28,6 +29,15 @@ enum SessionKind { // The workspace sync endpoint. Non-TTY: no PTY, no resize, and stdout kept // separate from stderr so diagnostics cannot corrupt protocol frames. SESSION_KIND_SYNC = 3; + + // Attach to a persistent shell, creating it when it does not exist. The PTY + // belongs to a process in the container and outlives this session; the next + // session attaches to the same one. A second session displaces the first — + // one shell carries one attachment. + // + // A separate kind rather than a flag on SHELL, so the command stays a + // function of the kind and its parameters. + SESSION_KIND_SHELL_ATTACH = 4; } message IssueTicketRequest { @@ -48,6 +58,22 @@ message IssueTicketRequest { // absolute; it is normalized before being bound into the ticket, and // resolved against the real filesystem inside the container. string sync_root = 6; + + // Names the shell to attach to. Required when kind is SHELL_ATTACH and + // ignored otherwise. Client-generated and opaque, matching + // ^[A-Za-z0-9_-]{1,64}$ — '.' and ':' are excluded because the multiplexer + // behind a shell reads them as target separators. + // + // Not a capability: anyone who may open a shell in a container can reach + // every shell in it from inside. Keeping one identity's shells out of + // another's view is the layout owner's decision, not this service's. + string shell_id = 7; + + // Working directory for the shell, applied only when it has to be created + // and ignored when it already exists — so a caller may pass its stored value + // on every attach. Optional. Must be absolute; normalized before being bound + // into the ticket, as sync_root is. + string shell_cwd = 8; } message IssueTicketResponse { From d388b29dd6bcda7f20024c41f4ddadbe93387ed8 Mon Sep 17 00:00:00 2001 From: Vitalii Valkov Date: Tue, 11 Aug 2026 22:00:12 +0200 Subject: [PATCH 2/2] Comments state the contract, not the platform The proto comments explained service behavior -- attach-or-create semantics, why a document carries a version, which component is the sole writer of a field, what a client should do on conflict. None of that is the interface's to say, and stating it here duplicates it into a place that cannot be kept correct. What remains is the wire contract: what a field is, whether it is required, its pattern, and what an absent value means. The reasoning lives in the change doc in agynio/architecture. --- proto/agynio/api/agents/v1/agents.proto | 45 ++++++------------- proto/agynio/api/gateway/v1/agents.proto | 5 +-- proto/agynio/api/gateway/v1/terminal.proto | 8 ++-- .../terminal_proxy/v1/terminal_proxy.proto | 30 ++++--------- 4 files changed, 29 insertions(+), 59 deletions(-) diff --git a/proto/agynio/api/agents/v1/agents.proto b/proto/agynio/api/agents/v1/agents.proto index 6bd53a4..3b7082c 100644 --- a/proto/agynio/api/agents/v1/agents.proto +++ b/proto/agynio/api/agents/v1/agents.proto @@ -847,21 +847,15 @@ message UpdateSandboxLastSessionResponse { // Sandbox Layout // =========================================================================== -// One identity's set of open shells in one sandbox — what a client reopens to -// find its work where it left it. -// -// The service never looks in the container. Attaching creates a shell that -// does not exist, so a layout naming a shell the container lost is not an -// inconsistency to reconcile but the ordinary case after a restart. +// One identity's ordered set of shells in one sandbox. message SandboxLayout { string sandbox_id = 1; - // Whose layout this is. Always the caller's own — resolved from - // authenticated context, never accepted as input on the external methods. + // Whose layout this is. Resolved from authenticated context on the external + // methods; not accepted as request input. string identity_id = 2; - // Incremented on every write. A writer supplies the version it read and is - // rejected with FailedPrecondition when it no longer matches. + // Incremented on every write. int64 version = 3; // Ordered. Position is display order. @@ -869,26 +863,20 @@ message SandboxLayout { } message SandboxTab { - // Opaque, client-generated, ^[A-Za-z0-9_-]{1,64}$. Passed verbatim as - // shell_id on a SHELL_ATTACH terminal session. + // Opaque, client-generated, ^[A-Za-z0-9_-]{1,64}$. Matches shell_id on a + // SHELL_ATTACH terminal session. string shell_id = 1; - // Assigned when the tab is opened and never reused within the layout. The - // last-resort display name, so closing one tab does not rename the others. + // Assigned when the tab is opened; not reused within a layout. int32 number = 2; - // A name the user gave this tab. Unset means the client derives one — the - // title the shell announces, else its directory. The derived name is never - // stored: it goes stale the moment the shell changes directory. + // A caller-supplied name. Unset means the client derives one. optional string name_override = 3; - // Last known working directory, absolute. Supplied as shell_cwd and applied - // only when the shell has to be created. Written by the Orchestrator before - // a planned stop, which is the last moment it can be read and the first it - // is about to be needed. + // Working directory, absolute. optional string cwd = 4; - // When a session last attached. Orders "the one I was just in". + // When a session last attached to this shell. optional google.protobuf.Timestamp last_attached_at = 5; } @@ -897,16 +885,14 @@ message GetSandboxLayoutRequest { } message GetSandboxLayoutResponse { - // A sandbox never worked in returns an empty layout at version 0 rather than - // NotFound — no client needs to tell those apart. + // Empty at version 0 when no layout exists. SandboxLayout layout = 1; } message SetSandboxLayoutRequest { string sandbox_id = 1; - // The version the caller read. A mismatch is FailedPrecondition; the caller - // refetches and reapplies. + // The version the caller read. A mismatch returns FailedPrecondition. int64 version = 2; repeated SandboxTab tabs = 3; @@ -916,11 +902,8 @@ message SetSandboxLayoutResponse { SandboxLayout layout = 1; } -// Internal only. Writes cwd onto the tabs of every layout of one sandbox, -// matching by shell_id and ignoring ids it does not find. -// -// Version-free: it touches one field the caller is the sole writer of, and -// failing on a concurrent tab reorder would lose the snapshot for no benefit. +// Internal only. Sets cwd on tabs of every layout of one sandbox, matched by +// shell_id. Unmatched ids are ignored. Not version-checked. message SetSandboxLayoutDirectoriesRequest { string sandbox_id = 1; repeated ShellDirectory directories = 2; diff --git a/proto/agynio/api/gateway/v1/agents.proto b/proto/agynio/api/gateway/v1/agents.proto index ea857a5..564d00b 100644 --- a/proto/agynio/api/gateway/v1/agents.proto +++ b/proto/agynio/api/gateway/v1/agents.proto @@ -37,9 +37,8 @@ service AgentsGateway { rpc EnsureSandboxRunning(agynio.api.agents.v1.EnsureSandboxRunningRequest) returns (agynio.api.agents.v1.EnsureSandboxRunningResponse); // --- Sandbox Layouts --- - // Always the caller's own layout: identity comes from authenticated context - // and is not a request field, so there is no call that reads or writes - // another person's tabs. + // Operate on the caller's own layout; identity comes from authenticated + // context and is not a request field. rpc GetSandboxLayout(agynio.api.agents.v1.GetSandboxLayoutRequest) returns (agynio.api.agents.v1.GetSandboxLayoutResponse); rpc SetSandboxLayout(agynio.api.agents.v1.SetSandboxLayoutRequest) returns (agynio.api.agents.v1.SetSandboxLayoutResponse); diff --git a/proto/agynio/api/gateway/v1/terminal.proto b/proto/agynio/api/gateway/v1/terminal.proto index 365ded2..9da0088 100644 --- a/proto/agynio/api/gateway/v1/terminal.proto +++ b/proto/agynio/api/gateway/v1/terminal.proto @@ -25,12 +25,12 @@ message CreateTerminalSessionRequest { // absolute. string sync_root = 5; - // Names the shell to attach to. Required when kind is SHELL_ATTACH and - // ignored otherwise. Opaque, ^[A-Za-z0-9_-]{1,64}$. + // Names the shell. Required when kind is SHELL_ATTACH, ignored otherwise. + // Opaque, ^[A-Za-z0-9_-]{1,64}$. string shell_id = 6; - // Working directory for the shell, applied only when it has to be created. - // Optional; must be absolute when set. + // Working directory for the shell. Optional; applied when the shell is + // created. Must be absolute when set. string shell_cwd = 7; } diff --git a/proto/agynio/api/terminal_proxy/v1/terminal_proxy.proto b/proto/agynio/api/terminal_proxy/v1/terminal_proxy.proto index a391bdf..bfab652 100644 --- a/proto/agynio/api/terminal_proxy/v1/terminal_proxy.proto +++ b/proto/agynio/api/terminal_proxy/v1/terminal_proxy.proto @@ -18,8 +18,7 @@ enum SessionKind { // ends up in a ticket. SESSION_KIND_UNSPECIFIED = 0; - // A login shell on a PTY, with the platform binaries on PATH. Ephemeral: the - // PTY closes with the connection, as a dropped SSH connection does. + // A login shell on a PTY. The PTY closes with the connection. SESSION_KIND_SHELL = 1; // A caller-supplied command on a PTY. Carries no more privilege than SHELL — @@ -30,13 +29,9 @@ enum SessionKind { // separate from stderr so diagnostics cannot corrupt protocol frames. SESSION_KIND_SYNC = 3; - // Attach to a persistent shell, creating it when it does not exist. The PTY - // belongs to a process in the container and outlives this session; the next - // session attaches to the same one. A second session displaces the first — - // one shell carries one attachment. - // - // A separate kind rather than a flag on SHELL, so the command stays a - // function of the kind and its parameters. + // A PTY on a named shell that outlives the session. Attaches to shell_id, + // creating it when it does not exist. A second session on the same shell + // displaces the first. SESSION_KIND_SHELL_ATTACH = 4; } @@ -59,20 +54,13 @@ message IssueTicketRequest { // resolved against the real filesystem inside the container. string sync_root = 6; - // Names the shell to attach to. Required when kind is SHELL_ATTACH and - // ignored otherwise. Client-generated and opaque, matching - // ^[A-Za-z0-9_-]{1,64}$ — '.' and ':' are excluded because the multiplexer - // behind a shell reads them as target separators. - // - // Not a capability: anyone who may open a shell in a container can reach - // every shell in it from inside. Keeping one identity's shells out of - // another's view is the layout owner's decision, not this service's. + // Names the shell. Required when kind is SHELL_ATTACH, ignored otherwise. + // Client-generated and opaque, matching ^[A-Za-z0-9_-]{1,64}$. string shell_id = 7; - // Working directory for the shell, applied only when it has to be created - // and ignored when it already exists — so a caller may pass its stored value - // on every attach. Optional. Must be absolute; normalized before being bound - // into the ticket, as sync_root is. + // Working directory for the shell. Optional; meaningful only when kind is + // SHELL_ATTACH. Applied when the shell is created and ignored when it + // already exists. Must be absolute. string shell_cwd = 8; }